Mundarija (24)
- Bu darsda
- 1. Nega bu kerak?
- 2. Cron ifodasi
- 2.1 Besh maydon
- 2.2 O'z tahlilchimiz
- 2.3 Kanon: croner va node-cron
- 3. Vaqt zonasi
- 3.1 03:30 qaysi 03:30?
- 3.2 Yozgi vaqt
- 4. Ikki nusxa muammosi
- 4.1 O'lchov
- 4.2 Yechim 1: Postgres advisory lock
- 4.3 Yechim 2: BullMQ job scheduler
- 5. OS cron, systemd timer yoki ilova ichida
- 6. Infratuzilma darajasida: S3 lifecycle
- 7. Loyiha qadami: navbat.ts (3-qism) — jadvallar() va commit
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): bo'shliqlarni to'ldiring
- 2-mashq (o'rta): ikki nusxa va qulf
- 3-mashq (qiyin): ish vaqti eslatmasi
- 10. Real ishda
- Xulosa
- Manbalar
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; kanontimezone: "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.tsmodulini 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
┌──────── 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.
// 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:
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: 61Bitta 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:
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 otadiChiqishning birinchi ikki qatorini modelimiz natijasi bilan, oxirgi qatorni esa ikki kutubxona bo'yicha solishtiring:
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: 61Nima 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:
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: 0Nima 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):
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:
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';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.
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:
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):
/** 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):
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:
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/ROLLBACKda 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
// 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:
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
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:
2026-11-02T04:00:00.000Z
2026-11-02T04:15:00.000Z
2026-11-02T04:30:00.000Z
2026-11-02T04:45:00.000ZIfoda */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/scheduledekoratorlari 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
AbortIncompleteMultipartUploadni 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
- croner (
nextRuns,timezone,protect): https://github.com/Hexagon/croner - node-cron: https://github.com/node-cron/node-cron
- BullMQ — Job Schedulers: https://docs.bullmq.io/guide/job-schedulers
- PostgreSQL 18 — Advisory Lock Functions: https://www.postgresql.org/docs/18/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
- systemd.timer (
Persistent=): https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html - Amazon S3 — Lifecycle configuration elements (
AbortIncompleteMultipartUpload): https://docs.aws.amazon.com/AmazonS3/latest/userguide/intro-lifecycle-rules.html - IANA Time Zone Database: https://www.iana.org/time-zones
- Kanon tajribasi (IlmHamroh, 2026-10-27): croner 10.0.1, node-cron 4.6.0, bullmq 6.3.19, pg 8.23.1, Redis 8.10.2 va MinIO RELEASE.2025-09-07 (Docker), PostgreSQL 18.6, Node 24.21.0. Advisory lock SQL bloki (PostgreSQL 17.6), cron tahlilchisi va mashq yechimlari — biz ishga tushirdik (2026-10-28).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!