IlmHamroh
JavaScript Full-stack/29-qism. Backend integratsiyalar: fayl, to'lov, bot, realtime48/69-dars19 daqiqa
Mundarija (24)

Cron va rejali vazifalar: vaqt zonasi va ikki nusxa muammosi

Qisqacha: "Har kuni 03:30 da yetim fayllarni tozala" — bu cron ifodasi 30 3 * * *. Ikki tuzoq bor. Birinchisi — vaqt zonasi: server UTC'da bo'lsa, 03:30 Toshkentda emas, 08:30 da ishlaydi; kanon timezone: "Asia/Tashkent" bilan to'g'ri vaqtni oldi (oldingi kun 22:30Z). Ikkinchisi — ikki nusxa: API ikkita jarayonda ishlasa, har biri o'z cron'ini yurgizadi — kanonda 3 soniyada 6 marta o'rniga 3 bo'lishi kerak edi. Yechimlar: Postgres advisory lock (3 marta) yoki BullMQ job scheduler (jadval Redis'da bitta, har tikni bitta worker oladi) — kanon ikkinchisini tanladi.

Bu darsda

  • Cron ifodasini o'qiysiz va yozasiz, o'zingiz kichik tahlilchi yasaysiz.
  • croner va node-cron farqini, xato ifodaga munosabatini bilasiz.
  • Vaqt zonasi va yozgi vaqt tuzoqlarini tushunasiz.
  • Bir nechta server nusxasida vazifa takrorlanishini o'lchov bilan ko'rib, ikki yechimni solishtirasiz.
  • OS cron, systemd timer va BullMQ scheduler orasidan tanlaysiz; navbat.ts modulini yakunlab commit qilasiz.

Oldin bilishingiz kerak: BullMQ asoslari, Date va vaqt zonalari, SQL'da sana va vaqt zonalari, Qulflar: row, table va advisory.

Oldingi darsda BullMQ'ning ilg'or imkoniyatlarini ko'rdik. Endi vaqtga bog'langan ishlar — "har kuni", "har 5 daqiqa", "ish kunlari 09:00".

1. Nega bu kerak?

Loyihada muntazam ishlar yig'ilib qoldi:

  • Yetim fayllar — yuklangan, lekin hech qaysi vazifaga biriktirilmagan fayllar (Fayl metama'lumotini DB'da boshqarish). Har kuni tunda tozalanadi.
  • Qolib ketgan bildirishnomalar — yuborilishi kerak bo'lib, biror sabab bilan osilganlari. Har 5 daqiqada tekshiriladi.
  • Outbox tozalash — tarqatilgan eski hodisalar (Transactional outbox).
  • Ertalabki xulosa — ish kunlari 09:00 da "bugungi vazifalaringiz".

Bular so'rovga javob emas, soatga javob. setInterval bilan qilsa bo'ladimi? Server qayta ishga tushsa hisob yo'qoladi, "har kuni 03:30" ni intervalda ifodalash noqulay, ikki nusxada esa ikki marta ishlaydi. Shuning uchun cron.

2. Cron ifodasi

2.1 Besh maydon

text
┌──────── daqiqa (0–59)
│ ┌────── soat (0–23)
│ │ ┌──── oy kuni (1–31)
│ │ │ ┌── oy (1–12)
│ │ │ │ ┌ hafta kuni (0–6, 0 = yakshanba)
│ │ │ │ │
30 3 * * *
Belgi Ma'nosi Misol
* har qanday * * * * * — har daqiqa
5 aniq qiymat 0 9 * * * — har kuni 09:00
1,15 ro'yxat 0 0 1,15 * * — oyning 1 va 15-kuni
1-5 oraliq 0 9 * * 1-5 — dushanba–juma 09:00
*/5 qadam */5 * * * * — har 5 daqiqa

Ba'zi kutubxonalar (croner, node-cron) oldiga oltinchi — soniya maydonini qo'shishga ruxsat beradi: * * * * * * — har soniya. Kanon ikki nusxa tajribasida shuni ishlatdi.

2.2 O'z tahlilchimiz

Cron tahlilchisi qanday ishlashini ko'raylik. Bu o'quv versiyasi: oy kuni va hafta kuni bir vaqtda berilganda standart cron ularni "yoki" bilan birlashtiradi, bizda esa "va" — kutubxona ishlating.

js
// O'quv cron tahlilchisi: "daqiqa soat kun oy hafta_kuni"
// Qo'llanadi: *, son, ro'yxat (1,15), oraliq (1-5), qadam (*/5).
const CHEGARA = [[0, 59], [0, 23], [1, 31], [1, 12], [0, 6]];

function maydon(matn, [min, max]) {
  const qiymatlar = new Set();
  for (const qism of matn.split(",")) {
    const [oraliq, qadamM] = qism.split("/");
    const qadam = Number(qadamM ?? 1);
    let [a, b] = oraliq === "*"
      ? [min, max] : oraliq.split("-").map(Number);
    b ??= a;
    if (a < min || b > max || a > b) {
      throw new Error(`noto'g'ri: ${qism}`);
    }
    for (let x = a; x <= b; x += qadam) qiymatlar.add(x);
  }
  return qiymatlar;
}

// ofsetDaq — zona siljishi: Toshkent UTC+5 = 300 daqiqa
function keyingilar(ifoda, boshlash, soni, ofsetDaq) {
  const m = ifoda.split(" ").map((q, i) => maydon(q, CHEGARA[i]));
  const natija = [];
  let t = Math.ceil(boshlash.getTime() / 60_000 + 1) * 60_000;
  while (natija.length < soni) {
    const d = new Date(t + ofsetDaq * 60_000); // mahalliy vaqt
    const q = [d.getUTCMinutes(), d.getUTCHours(), d.getUTCDate(),
      d.getUTCMonth() + 1, d.getUTCDay()];
    if (q.every((x, i) => m[i].has(x))) natija.push(new Date(t));
    t += 60_000;
  }
  return natija.map((d) => d.toISOString());
}

const bosh = new Date("2026-10-27T20:00:00Z");
const qator = (i, s) => console.log(i, s.join(", "));
qator("UTC     ", keyingilar("30 3 * * *", bosh, 2, 0));
qator("Toshkent", keyingilar("30 3 * * *", bosh, 2, 300));
qator("ish kuni", keyingilar("0 9 * * 1-5", bosh, 3, 300));
try {
  keyingilar("61 * * * *", bosh, 1, 0);
} catch (e) {
  console.log(e.message);
}

Konsolda:

text
UTC      2026-10-28T03:30:00.000Z, 2026-10-29T03:30:00.000Z
Toshkent 2026-10-27T22:30:00.000Z, 2026-10-28T22:30:00.000Z
ish kuni 2026-10-28T04:00:00.000Z, 2026-10-29T04:00:00.000Z, 2026-10-30T04:00:00.000Z
noto'g'ri: 61

Bitta zona siljishini (+300 daqiqa) qo'llash faqat yozgi vaqtsiz zonalar uchun to'g'ri — Toshkent shunday. Haqiqiy kutubxona IANA zona bazasidan (dunyodagi har zona qachon qancha siljiganini yozib boradigan ochiq ro'yxat) foydalanadi (Intl.DateTimeFormat ham shu bazani ishlatadi).

2.3 Kanon: croner va node-cron

Kanon ikki mashhur kutubxonani sinadi — croner 10.0.1 va node-cron 4.6.0. Xuddi shu ifodalar, boshlanish 2026-10-27T20:00:00Z:

js
import { Cron } from "croner";
import cron from "node-cron";

for (const tz of ["UTC", "Asia/Tashkent"]) {
  const c = new Cron("30 3 * * *", { timezone: tz, paused: true });
  const n = c.nextRuns(3, boshlash).map((d) => d.toISOString());
  console.log(tz, n);
  c.stop();
}
console.log(cron.validate("61 * * * *"));
new Cron("61 * * * *", { paused: true }); // xato otadi

Chiqishning birinchi ikki qatorini modelimiz natijasi bilan, oxirgi qatorni esa ikki kutubxona bo'yicha solishtiring:

text
UTC            2026-10-28T03:30:00.000Z, 2026-10-29T03:30:00.000Z, 2026-10-30T03:30:00.000Z
Asia/Tashkent  2026-10-27T22:30:00.000Z, 2026-10-28T22:30:00.000Z, 2026-10-29T22:30:00.000Z
'0 9 * * 1-5' (ish kunlari 09:00 Toshkent): 2026-10-28T04:00:00.000Z, 2026-10-29T04:00:00.000Z, 2026-10-30T04:00:00.000Z
node-cron validate('61 * * * *'): false | croner noto'g'ri ifoda: CronPattern: Invalid value for minute: 61

Nima bo'ldi? Vaqtlar bizning modelimiz bilan bir xil. Farq — xato ifodada: node-cron validate() faqat false qaytaradi, croner esa qaysi maydon va qanday qiymat noto'g'ri ekanini aytadi: Invalid value for minute: 61. Konfiguratsiyadan kelgan ifodani tekshirishda bu farq katta.

croner 10 node-cron 4
Bog'liqlik yo'q yo'q
Vaqt zonasi timezone timezone
Keyingi ishga tushishlar nextRuns(n) yo'q (hujjat bo'yicha)
Xato ifoda aniq xabar bilan xato validate() → false
Ustma-ust ishlash himoyasi protect: true (hujjat bo'yicha) —

protect — oldingi ishga tushish hali tugamagan bo'lsa, keyingisini o'tkazib yuborish. Har daqiqalik ish 90 soniya davom etsa, himoyasiz ikki nusxa parallel ishlaydi (kanon sinamagan).

3. Vaqt zonasi

3.1 03:30 qaysi 03:30?

Serverlar ko'pincha UTC'da ishlaydi (SQL'da sana va vaqt zonalari darsida Postgres'ni ham UTC'ga qo'ygan edik). 30 3 * * * zonasiz yozilsa — server zonasi bo'yicha: UTC 03:30 = Toshkent 08:30. "Tunda tozalash" ish vaqti boshlanishida ishlaydi.

Kanon chiqishiga qarang: Toshkent bo'yicha 03:30 — 2026-10-27T22:30:00Z, ya'ni UTC bo'yicha oldingi kun 22:30. Vaqt zonasini doim aniq yozing: timezone: "Asia/Tashkent" (croner, node-cron), tz: "Asia/Tashkent" (BullMQ). Shunda server qaysi zonada turgani ahamiyatsiz.

3.2 Yozgi vaqt

O'zbekistonda yozgi vaqt 1992 yildan beri yo'q — Asia/Tashkent doim UTC+5 (kanon qaydi, IANA zona bazasi). Lekin foydalanuvchilar boshqa zonada bo'lsa (masalan Europe/Berlin), yiliga ikki marta:

  • bahorda 02:00 → 03:00 sakraydi — 02:30 vazifasi o'tkazib yuborilishi mumkin;
  • kuzda 03:00 → 02:00 qaytadi — 02:30 ikki marta keladi.

Kutubxonalar buni turlicha hal qiladi (hujjat bo'yicha). Eng xavfsiz yo'l — muhim vazifalarni 01:00–03:00 oralig'iga qo'ymaslik va ishni idempotent qilish.

Tekshirib ko'ring: Ertalabki xulosa har foydalanuvchiga uning soati bo'yicha 09:00 da ketishi kerak. Bitta 0 9 * * * cron yetadimi?

Javob

Yo'q. Bitta cron bitta zonada ishlaydi. Yo'llar: har zona uchun alohida jadval yoki har 15 daqiqada ishlaydigan cron "hozir mahalliy vaqti 09:00 bo'lgan foydalanuvchilar"ni bazadan topib, ularga ish qo'shadi. Ikkinchisi ko'p zonali ilovalarda keng tarqalgan — u BullMQ asoslari darsidagi kechiktirilgan ishlarga o'xshaydi.

4. Ikki nusxa muammosi

4.1 O'lchov

Production'da API odatda bir nechta jarayonda yuradi — yuk uchun yoki deploy paytida eski va yangi nusxa bir vaqtda. Har nusxa ishga tushganda new Cron(...) chaqiradi. Kanon ikki "nusxa"ni (bitta jarayonda ikki Cron) har soniyalik cron bilan 3 soniya yurgizdi. To'g'ri javob — 3 marta (har soniyada bitta). Har qatordagi "N marta" ni shu son bilan solishtiring:

text
qulfsiz: 6 marta (har nusxa o'zi — ikki baravar)
pg_try_advisory_xact_lock bilan: 3 marta (BBB) — har soniyada bittasi
BullMQ job scheduler (2 nusxa upsert + 2 worker): 4 marta (ABAA) — birinchisi darhol, keyin har 1 s; har tikni BITTA worker oldi; o'chirgandan keyin jadvallar: 0

Nima bo'ldi? Qulfsiz — 6 marta: yetim fayllar ikki marta tozalanadi, xulosa emaili ikki marta ketadi. Qulf bilan — aynan 3. BullMQ'da 4, chunki birinchi tik darhol keldi; lekin har tikni bitta worker bajardi. Ikki yechimni ko'ramiz.

4.2 Yechim 1: Postgres advisory lock

Qulflar darsidagi advisory lock: ish boshlanishida pg_try_advisory_xact_lock(kalit) — kim birinchi olsa, o'sha bajaradi; ikkinchisi darhol false oladi va kutmaydi. Kanon kodi (formatlab):

js
const ish = (nom) => async () => {
  const c = await pool.connect();
  try {
    await c.query("BEGIN");
    const r = await c.query(
      "SELECT pg_try_advisory_xact_lock(" +
      "hashtext('cron:yetim-fayllar')) AS ok");
    if (r.rows[0].ok) { n++; kim.push(nom); await kut(100); }
    await c.query("COMMIT");
  } finally { c.release(); }
};
const a = new Cron("* * * * * *", ish("A"));
const b = new Cron("* * * * * *", ish("B"));

hashtext('…') matnli nomni advisory lock kalitiga (son) aylantiradi. _xact_ — qulf tranzaksiya oxirida o'zi ochiladi. Bitta sessiya ichida qanday ko'rinadi — biz o'z PostgreSQL 17.6 klasterimizda ishga tushirdik:

sql
BEGIN;
SELECT pg_try_advisory_xact_lock(hashtext('cron:yetim-fayllar'))
  AS birinchi;
SELECT pg_try_advisory_xact_lock(hashtext('cron:yetim-fayllar'))
  AS yana;
SELECT locktype, mode, granted
  FROM pg_locks WHERE locktype = 'advisory';
COMMIT;
SELECT count(*) AS qolgan_qulflar
  FROM pg_locks WHERE locktype = 'advisory';
text
BEGIN
 birinchi
----------
 t
(1 row)

 yana
------
 t
(1 row)

 locktype |     mode      | granted
----------+---------------+---------
 advisory | ExclusiveLock | t
(1 row)

COMMIT
 qolgan_qulflar
----------------
              0
(1 row)

Ikki narsa. Birinchisi — o'sha sessiya qulfni ikkinchi marta ham oladi (yana — t): advisory lock sessiyaga tegishli. Shuning uchun ikki nusxa alohida ulanishda bo'lishi kerak — kanon har tikda pool.connect() bilan alohida mijoz oldi. Ikkinchisi — COMMIT dan keyin qulf qolmadi.

Cheklov: bu "shu soniyada bittasi" kafolati. Nusxa A ishni 100 ms da tugatdi va qulfni ochdi; B ning soati 150 ms orqada bo'lsa, u ham qulfni oladi va ishni ikkinchi marta bajaradi. Kanonda bu bo'lmadi (BBB — har soniyada bitta), lekin soatlar farqi va ish davomiyligiga bog'liq. Mustahkamroq: "oxirgi bajarilgan vaqt" ni bazada saqlash va qulf ichida tekshirish.

4.3 Yechim 2: BullMQ job scheduler

BullMQ'da jadval Redis'da saqlanadi. upsertJobScheduler(id, takror, shablon) — bir xil id bilan necha marta chaqirilsa ham bitta jadval (upsert). Vaqti kelganda BullMQ bitta ish yaratadi, uni worker'lardan bittasi oladi — oddiy navbat qoidasi.

js
const qA = new Queue("cron", { connection, prefix: "sinov" });
const qB = new Queue("cron", { connection, prefix: "sinov" });
await qA.upsertJobScheduler("har-soniya", { every: 1000 },
  { name: "tik" });
await qB.upsertJobScheduler("har-soniya", { every: 1000 },
  { name: "tik" });
const wA = new Worker("cron", async () => { kim.push("A"); }, opt);
const wB = new Worker("cron", async () => { kim.push("B"); }, opt);

Kanon: 3.2 soniyada 4 marta (ABAA) — birinchisi darhol (every jadvali birinchi ishni darhol qo'yadi), keyin har soniyada; har tikni bitta worker oldi. Ikki nusxa upsert qildi — jadval baribir bitta. removeJobScheduler dan keyin jadvallar soni 0.

sequenceDiagram
  participant A as API nusxa A
  participant B as API nusxa B
  participant R as Redis
  participant WA as Worker A
  participant WB as Worker B
  A->>R: upsertJobScheduler yetim-fayllar
  B->>R: upsertJobScheduler yetim-fayllar
  Note over R: jadval bitta
  R->>R: 03:30 Toshkent - bitta ish yaratildi
  WA->>R: ishni oldi
  WB->>R: bo'sh - kutadi
  WA-->>R: completed
Advisory lock BullMQ job scheduler
Qayerda Postgres Redis (navbat allaqachon bor)
Kafolat shu tikda bitta har tik — bitta ish, bitta worker
Ish yiqilsa qayta urinish yo'q attempts, backoff (ishonchlilik)
Ko'rinish log Bull Board, ish tarixi
Server o'chiq edi o'tkazib yuborilgan tik yo'qoladi jadval Redis'da, keyingi tik keladi

Kanon tanlovi — BullMQ job scheduler: Redis loyihada bor, ishlar qayta urinish va kuzatuvni navbatdan meros oladi.

5. OS cron, systemd timer yoki ilova ichida

OS cron systemd timer Ilova ichida (BullMQ, croner)
Qayerda server OS Linux systemd Node jarayoni
Ko'p server har serverda alohida har serverda alohida Redis/DB orqali bitta
Log syslog, pochta journalctl ilova logi
O'tkazib yuborilgan yo'qoladi Persistent=true — ishga tushganda bajaradi sozlamaga bog'liq
Konteyner (Docker) noqulay noqulay tabiiy
Qachon zaxira, server ishlari zaxira, server ishlari ilova biznes ishlari

Bu kursda server ishlari (zaxira nusxa, sertifikat yangilash) VPS'da OS darajasida qilinadi — Rejalashtirilgan vazifalar: cron va systemd timer. Kanon ularni Windows'da sinamagan. Ilova ishlari (yetim fayllar, eslatmalar) — ilova ichida, BullMQ bilan.

6. Infratuzilma darajasida: S3 lifecycle

Ba'zi tozalashni kod yozmasdan ombor o'zi qiladi. S3'da lifecycle qoidasi: "vaqtincha/ prefiksidagi obyektlar 1 kundan keyin o'chsin". Kanon MinIO'da ikki qoidani sinadi:

text
vaqtincha/ 1 kun         → qabul qilindi
chala multipart 1 kun    → MinIO rad etdi: InvalidArgument The XML you provided was not well-formed or did not validate against our published schema
qo'llangan qoidalar: [{"ID":"vaqtincha-1-kun","Filter":{"Prefix":"vaqtincha/"},"Expiration":{"Days":1}}]

Birinchisi qabul qilindi (1 kun kutib, o'chishini tekshirib bo'lmadi). Ikkinchisi — AbortIncompleteMultipartUpload (chala qolgan multipart yuklashlarni tozalash, Object storage) — AWS S3 hujjatida bor, lekin kanon ishlatgan MinIO versiyasi uni rad etdi. Qoida bitta omborda ishlagani boshqasida ishlashini kafolatlamaydi. Kanon yetim fayllar uchun ikkala yo'lni ham saqladi: lifecycle — xavfsizlik to'ri, rejali ish — bazadagi yozuvlar bilan birga tozalash.

7. Loyiha qadami: navbat.ts (3-qism) — jadvallar() va commit

Modulning oxirgi metodi (formatlab):

ts
/** Rejali ishlar (Asia/Tashkent): yetim fayllar — har kuni 03:30,
 * qolib ketgan bildirishnomalar — har 5 daqiqa. Bir nechta API
 * nusxasi chaqirsa ham bitta jadval (upsert). */
async jadvallar(): Promise<void> {
  await this.tizim.upsertJobScheduler(
    "yetim-fayllar",
    { pattern: "30 3 * * *", tz: "Asia/Tashkent" },
    { name: "yetim-fayllar" },
  );
  await this.tizim.upsertJobScheduler(
    "qolib-ketgan",
    { every: 5 * 60_000 },
    { name: "qolib-ketgan" },
  );
}

async yop(): Promise<void> {
  await Promise.all([
    this.eslatma.close(),
    this.yetkazish.close(),
    this.tizim.close(),
  ]);
}

Ikki xil takror: pattern + tz (cron) va every (ms). Ikkala ish tizim navbatiga tushadi va ishonchlilik darsidagi SUKUT sozlamalarini oladi (5 urinish, eksponensial kutish).

Kanon testi (formatlab):

ts
test("job scheduler: upsert ikki marta — bitta jadval; " +
  "keyingi ishlash vaqti Toshkent 03:30", async () => {
  await n.jadvallar();
  await n.jadvallar();
  const s = await n.tizim.getJobSchedulers();
  assert.deepEqual(s.map((x) => x.key).sort(),
    ["qolib-ketgan", "yetim-fayllar"]);
  const y = s.find((x) => x.key === "yetim-fayllar");
  const keyingi = new Date(Number(y?.next));
  const soat = new Intl.DateTimeFormat("en-GB", {
    timeZone: "Asia/Tashkent", hour: "2-digit", minute: "2-digit",
  }).format(keyingi);
  assert.equal(soat, "03:30");
});

Test keyingi ishga tushishni Intl.DateTimeFormat bilan Toshkent soatiga aylantirib tekshiradi — server zonasidan qat'i nazar "03:30". Kanon paketi Redis bilan 82/82 o'tdi; biz Redis'siz yurgizganimizda BullMQ to'plami o'tkazib yuborildi (REDIS_URL yo'q). Modulning formatlangan nusxasi — tsc --noEmit va eslint (strictTypeChecked) 0 xato, biz tekshirdik.

navbat.ts to'rt darsda to'liq o'qildi — endi commit:

bash
git commit -m "feat(navbat): BullMQ navbatlari — eslatma, yetkazish, rejali ishlar"

vazifalar-api ga ulanganda API ishga tushishida jadvallar() chaqiriladi (har nusxa — upsert xavfsiz), tizim worker'i esa yetim-fayllar va qolib-ketgan ishlovchilarini bajaradi.

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Vazifa 5 soat farq bilan ishlaydi timezone yozilmagan, server UTC timezone/tz: "Asia/Tashkent"
Email ikki marta ketdi API ikki nusxada, har birida cron advisory lock yoki BullMQ scheduler
Xato ifoda jim ishlamaydi validate() natijasi tekshirilmagan croner (aniq xato) yoki ishga tushishda tekshiruv
Uzoq ish ustma-ust boshlandi oldingisi tugamagan protect, qulf, navbat
Advisory lock ishlamadi ikki "nusxa" bitta sessiyada har biri alohida ulanish
Server o'chganda tik yo'qoldi xotiradagi cron Redis'dagi jadval yoki systemd Persistent=true
Lifecycle qoidasi rad etildi ombor qoidani qo'llamaydi (MinIO) omboringiz hujjatini tekshiring, rejali ish bilan zaxira

9. Mashqlar

Mashqlar kurs/mashqlar/29/48-cron/ da.

1-mashq (oson): bo'shliqlarni to'ldiring

  • Ish kunlari 09:00 — 0 9 * *
  • Har 15 daqiqa — * * * *
  • Toshkent bo'yicha 03:30 UTC bo'yicha — oldingi kun
  • Tranzaksiya oxirida o'zi ochiladigan advisory lock funksiyasi — pg_try_advisory__lock
  • BullMQ'da jadvalni bir marta yaratadigan metod —
Yechim
  • 1-5 — dushanba–juma (0 — yakshanba).
  • */15 — 0, 15, 30, 45.
  • 22:30 — UTC+5.
  • xact — COMMIT/ROLLBACK da ochiladi.
  • upsertJobScheduler — bir xil id — bitta jadval.

2-mashq (o'rta): ikki nusxa va qulf

Ikki API nusxasini (A va B) har soniyada "tik" qiladigan qilib modellashtiring: 3 soniya, har soniyada avval B, keyin A. Qulf — Set (kalit cron:yetim-fayllar:<soniya>), tryLock(kalit) olsa true. Qulfsiz va qulf bilan nechta ish bajarilganini va kim bajarganini chiqaring.

Yechim
js
// Ikki API nusxasi (A va B), har biri har soniyada "tik" qiladi.
const qulflar = new Set(); // "baza"dagi advisory lock'lar o'rni

function tryLock(kalit) {
  if (qulflar.has(kalit)) return false;
  qulflar.add(kalit);
  return true;
}

function yurit(qulfBilan) {
  const kim = [];
  for (let soniya = 1; soniya <= 3; soniya++) {
    for (const nusxa of ["B", "A"]) {
      const kalit = `cron:yetim-fayllar:${soniya}`;
      if (!qulfBilan || tryLock(kalit)) kim.push(nusxa);
    }
  }
  return `${kim.length} marta (${kim.join("")})`;
}

console.log("qulfsiz:  ", yurit(false));
console.log("qulf bilan:", yurit(true));

Konsolda:

text
qulfsiz:   6 marta (BABABA)
qulf bilan: 3 marta (BBB)

Kanon raqamlari bilan bir xil: 6 va 3 (BBB). Modelda kalitga soniya qo'shildi — har tik o'z qulfi, ochilmaydi. Haqiqiy _xact_ qulfi esa tranzaksiya oxirida ochiladi, shuning uchun "tik" vaqtlari farq qilsa, kechikkan nusxa ham ishni bajarishi mumkin («Yechim 1: Postgres advisory lock» bo'limining oxiri). Kalitga davr belgisini (…:2026-10-28) qo'shib, "bajarildi" belgisini bazada saqlash shu muammoni yopadi.

3-mashq (qiyin): ish vaqti eslatmasi

"Ish kunlari 09:00 dan 18:00 gacha har 15 daqiqa" — cron ifodasini yozing. Darsdagi maydon va keyingilar funksiyalari bilan Toshkent vaqti bo'yicha 2026-10-30T12:50:00Z (juma, Toshkentda 17:50) dan keyingi 4 ishga tushishni chiqaring. Nega ular orasida katta tanaffus bor?

Yechim
js
const CHEGARA = [[0, 59], [0, 23], [1, 31], [1, 12], [0, 6]];

function maydon(matn, [min, max]) {
  const qiymatlar = new Set();
  for (const qism of matn.split(",")) {
    const [oraliq, qadamM] = qism.split("/");
    const qadam = Number(qadamM ?? 1);
    let [a, b] = oraliq === "*"
      ? [min, max] : oraliq.split("-").map(Number);
    b ??= a;
    if (a < min || b > max || a > b) {
      throw new Error(`noto'g'ri: ${qism}`);
    }
    for (let x = a; x <= b; x += qadam) qiymatlar.add(x);
  }
  return qiymatlar;
}

function keyingilar(ifoda, boshlash, soni, ofsetDaq) {
  const m = ifoda.split(" ").map((q, i) => maydon(q, CHEGARA[i]));
  const natija = [];
  let t = Math.ceil(boshlash.getTime() / 60_000 + 1) * 60_000;
  while (natija.length < soni) {
    const d = new Date(t + ofsetDaq * 60_000);
    const q = [d.getUTCMinutes(), d.getUTCHours(), d.getUTCDate(),
      d.getUTCMonth() + 1, d.getUTCDay()];
    if (q.every((x, i) => m[i].has(x))) natija.push(new Date(t));
    t += 60_000;
  }
  return natija.map((d) => d.toISOString());
}

const ifoda = "*/15 9-17 * * 1-5";
const bosh = new Date("2026-10-30T12:50:00Z");
for (const t of keyingilar(ifoda, bosh, 4, 300)) console.log(t);

Konsolda:

text
2026-11-02T04:00:00.000Z
2026-11-02T04:15:00.000Z
2026-11-02T04:30:00.000Z
2026-11-02T04:45:00.000Z

Ifoda */15 9-17 * * 1-5: soat maydoni 9-17, chunki 17:45 — oxirgi; 9-18 yozsangiz, 18:00, 18:15 … 18:45 ham kirib qoladi. Boshlanish — juma 17:50 (Toshkent): jumaning qolgan vaqti ish soatidan tashqarida, shanba va yakshanba 1-5 ga kirmaydi. Shuning uchun keyingi ishga tushish — dushanba 09:00 Toshkent = 2026-11-02T04:00Z. Ikki yarim kunlik tanaffus — xato emas, ifodaning ma'nosi. Production'da shunday ifodani yozgach, croner'ning nextRuns bilan keyingi bir necha vaqtni chiqarib ko'rish — eng arzon test.

10. Real ishda

  • Har backend'da tozalash, hisobot, sinxronlash, eslatma ishlari bor — va deyarli har biri bir kun ikki nusxa muammosiga uchraydi.
  • Kubernetes CronJob, bulut rejalashtiruvchilari (EventBridge Scheduler, Cloud Scheduler) — ilova tashqarisidagi muqobillar (hujjat bo'yicha, sinalmagan).
  • NestJS — @nestjs/schedule dekoratorlari bilan (Rejalashtirish: @nestjs/schedule); ko'p nusxa muammosi u yerda ham bor.
  • Intervyuda: "Cron vazifasi ikki serverda ikki marta ishlamasligi uchun nima qilasiz?", "Vaqt zonasi va yozgi vaqt cron'ga qanday ta'sir qiladi?", "OS cron va ilova ichidagi scheduler farqi?"

Xulosa

  • Cron: 5 maydon (ba'zan soniya bilan 6); *, ro'yxat, oraliq, qadam.
  • Zonani doim yozing: Toshkent 03:30 = UTC oldingi kun 22:30 (kanon, croner 10.0.1).
  • node-cron validate → false; croner — aniq xato (Invalid value for minute: 61).
  • Ikki nusxa: qulfsiz 6 marta; advisory lock — 3 (har tikda bitta); BullMQ job scheduler — jadval Redis'da bitta, har tikni bitta worker oladi.
  • Advisory lock sessiyaga tegishli — har nusxa alohida ulanishda.
  • Server ishlari — OS cron/systemd; ilova ishlari — BullMQ; S3 lifecycle — infratuzilma to'ri (MinIO AbortIncompleteMultipartUpload ni rad etdi).

Keyingi dars: Boshqa xabar broker'lari — RabbitMQ, NATS JetStream, pg-boss, Kafka va SQS: qachon qaysi, kanon o'lchovi va RabbitMQ'ning kutilmagan qayta yetkazish qoidasi.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!