---
title: 'Zamanlanmış Görevler'
source: 'https://academia.sh/tr/kurslar/eszamansiz-isleme/zamanlanmis-gorevler'
course: 'Önbellekleme, Kuyruklar ve Eşzamansız İşleme'
language: tr
updated: '2026-08-17T18:06:46+00:00'
license: 'CC BY-SA 4.0'
---

# Zamanlanmış Görevler

Takvimden tetiklenen işlerin tanımı ve tehlikeleri: yineleme kuralının hesaplanması, ay sonu ve saat dilimi tuzakları, sabit hız ile sabit gecikme ayrımı ve iki koşumun üst üste binmesini kira kilidiyle önleyen ölçülmüş çözüm.

Bir önceki ders işleri dışarıdan aldı: havuzun elinde bir liste vardı, onu işçilere
dağıttı. Arka plan işlerinin önemli bir bölümü ise dışarıdan gelmez, **takvimden** gelir.
Gecikme bildirimi her sabah altıda, şube stok özeti her on beş dakikada bir, aylık ödünç
raporu her ayın birinde üretilir.

Bunu "bir zamanlayıcı kur ve işi çağır" diye özetlemek yanıltıcıdır. Zamanlanmış görevin
iki zor sorusu vardır. Birincisi yinelemenin nasıl tanımlandığı: "her ayın 31'i" olmayan
bir tarihe düştüğünde ne olur, yerel saat değiştiğinde koşum kaçar mı. İkincisi
çakışmadır: bir koşum, bir sonraki tetiklenmeden bitmezse iki koşum aynı anda çalışır ve
aynı raporu iki kez üretir. Bu ders ikisini de kurar; ikincisini ölçerek çözer.

## Yineleme Tanımı

Yineleyen görev iki parçadan oluşur: **ne yapılacağı** ve **ne zaman yapılacağı**.
İkincisi bir veri yapısıdır ve tek bir işlevle sınanabilir: "şu andan sonraki koşum
hangi an".

```js
// zamanlama.mjs — yineleyen gorev tanimindan sonraki kosum anlarini uretir (hepsi UTC)
const TANIM = {
  gecikmeBildirimi: { tur: "gunluk", saat: 6, dakika: 0 },
  stokOzeti: { tur: "aralik", dakika: 15 },
  aylikRapor: { tur: "aylik", gun: 1, saat: 2, dakika: 30 },
  ayinSonu: { tur: "aylik", gun: 31, saat: 2, dakika: 30 },
};

const DK = 60_000;

function sonraki(tanim, su) {
  const d = new Date(su);
  if (tanim.tur === "aralik") {
    const adim = tanim.dakika * DK;
    return new Date(Math.floor(su / adim) * adim + adim);
  }
  if (tanim.tur === "gunluk") {
    const bugun = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(),
                           tanim.saat, tanim.dakika);
    return new Date(bugun > su ? bugun : bugun + 24 * 60 * DK);
  }
  // aylik: bu ayin gunu gecmisse sonraki ayin ayni gunu
  const buAy = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), tanim.gun,
                        tanim.saat, tanim.dakika);
  return new Date(buAy > su ? buAy
    : Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, tanim.gun, tanim.saat, tanim.dakika));
}

const BASLANGIC = Date.parse("2026-01-31T23:47:00Z");   // olcumun sabit baslangic ani
for (const [ad, tanim] of Object.entries(TANIM)) {
  let an = BASLANGIC;
  const anlar = [];
  for (let i = 0; i < 3; i++) {
    an = sonraki(tanim, an).getTime();
    anlar.push(new Date(an).toISOString().slice(0, 16).replace("T", " "));
  }
  console.log(ad.padEnd(17), anlar.join("  "));
}
```

```
gecikmeBildirimi  2026-02-01 06:00  2026-02-02 06:00  2026-02-03 06:00
stokOzeti         2026-02-01 00:00  2026-02-01 00:15  2026-02-01 00:30
aylikRapor        2026-02-01 02:30  2026-03-01 02:30  2026-04-01 02:30
ayinSonu          2026-03-03 02:30  2026-03-31 02:30  2026-05-01 02:30
```

Başlangıç anı sabit bir değişmez olarak yazıldığı için çıktı her koşumda aynıdır;
zamanlama kuralı böylece sınanabilir bir işleve dönüşür. "Şimdi" değerini işlevin
dışından vermek, zamanlamayı sınanabilir kılan tek karardır.

İlk üç satır beklendiği gibi. Dördüncü satır beklendiği gibi **değil**.

### Ay Sonu Tuzağı

`ayinSonu` tanımı "her ayın 31'i saat 02:30" diyor. Üretilen anlar 3 Mart, 31 Mart ve
1 Mayıs. Şubat'ın 31'i yok; `Date.UTC(2026, 1, 31)` taşan günü sonraki aya aktarıp
3 Mart'ı üretiyor. Nisan'ın 31'i de yok, o da 1 Mayıs'a kayıyor.

Bu, hesaplamanın hatası değil, **tanımın eksikliğidir**. Ayın son gününe bağlanan bir
görev için iki politika arasında seçim yapmak gerekir: olmayan günü **atlamak** (o ay
koşum yok) ya da ayın **son gününe kelepçelemek** (28, 29 ya da 30). Hangisinin doğru
olduğu işe bağlıdır — aylık rapor kelepçelenmeli, ayın 31'inde biten bir aboneliğin
yenilenmesi ise atlanmamalıdır. Yazılmayan politika, çalışma zamanının taşma davranışına
teslim edilmiş demektir.

### Saat Dilimi

Yukarıdaki hesabın tamamı UTC üzerinde yapıldı. Yerel saate bağlanan bir görev iki ek
sorun üretir. Yaz saati uygulamasının ileri alındığı gece, yerel saatle 02:30 **hiç
yaşanmaz**; o gece koşum kaçar. Geri alındığı gece ise 02:30 **iki kez** yaşanır; koşum
iki kez tetiklenir.

Bu yüzden zamanlama kararı iki katmana ayrılır. Görevin tetiklenme anı UTC üzerinde
tutulur ve saklanır; yerel saat yalnız kullanıcıya gösterilirken kullanılır. Yerel takvim
gerçekten gerekiyorsa (örneğin "şube kapanışından sonra") saat dilimi kimliği tanımın
parçası olur ve dönüşüm her koşumda yeniden yapılır.

## Sabit Hız ve Sabit Gecikme

Yinelemenin ikinci ayrıntısı, bir sonraki koşumun neye göre planlandığıdır.

**Sabit hız** (fixed rate) zamanlamada koşumlar takvime çakılıdır: 02:30, 03:30, 04:30.
Koşum ne kadar sürerse sürsün bir sonrakinin anı değişmez. **Sabit gecikme** (fixed
delay) zamanlamada ise bekleme, önceki koşumun **bitişinden** sayılır: koşum 40 dakika
sürdüyse ve gecikme bir saatse, sonraki koşum 04:10'da başlar.

Aradaki fark kaymadır. Kırk dakika süren saatlik bir görev, sabit gecikmeyle günde 24
değil yaklaşık 14 kez koşar ve koşum anları her gün ilerler; bu ilerlemeye **kayma**
(drift) denir. Sabit hızda kayma yoktur, ama karşılığında koşumların üst üste binmesi
mümkün hâle gelir: koşum bir saatten uzun sürerse bir sonraki tetikleme, önceki hâlâ
çalışırken gelir.

Yani sabit hız ile sabit gecikme arasındaki seçim, kayma ile çakışma arasındaki
seçimdir. Kaymayı kabul edemiyorsak çakışmayı ayrıca engellemek zorundayız.

## Çakışma

Çakışma yalnız uzun koşumdan doğmaz. İkinci ve daha sık karşılaşılan kaynak, uygulamanın
**birden çok örnek** olarak dağıtılmasıdır. Zamanlayıcı uygulamanın içinde çalışıyorsa,
üç örneğe dağıtılmış bir servis her sabah altıda gecikme bildirimini üç kez gönderir.
Üçüncü kaynak yeniden dağıtımdır: eski örnek kapanmadan yenisi ayağa kalkarsa tetikleme
ikiye çıkar.

Üçünün de çözümü aynıdır: görev, koşmadan önce **koşma hakkını** almak zorundadır.

## Kira Kilidi

Bu hakkı veren yapıya **kilit** (lock) denir. Arka plan görevlerinde kullanılan biçimi
**kira kilidi**dir (lease lock): kilit süresizce değil, belirli bir süre için verilir.
Süre şart, çünkü kilidi tutan süreç çökerse kilidi bırakacak kimse kalmaz; kira, o
durumda kilidin kendiliğinden düşmesini sağlar.

Kilidin doğruluğu tek bir koşula bağlıdır: alma işlemi **atomik** olmalıdır. İki koşum
"kilit var mı" diye sorup ikisi de "yok" cevabını alırsa kilit hiçbir işe yaramaz. Bu
yüzden sorgu ile yazma tek bir ifadede birleştirilir.

```js
// gorev.mjs — aylik odunc raporunu ureten yineleyen gorev
//   node gorev.mjs <etiket>           kilitsiz kosum
//   node gorev.mjs <etiket> --kilit   kira kilidiyle kosum
import { DatabaseSync } from "node:sqlite";
import { appendFileSync } from "node:fs";

const ETIKET = process.argv[2];
const KILITLI = process.argv.includes("--kilit");
const KIRA_MS = 5000;                              // kilidin kira suresi

const db = new DatabaseSync("zamanlama.db");
db.exec("PRAGMA busy_timeout = 5000");
db.exec(`CREATE TABLE IF NOT EXISTS kilit (
           ad TEXT PRIMARY KEY, sahip TEXT NOT NULL, bitis INTEGER NOT NULL)`);

// Kilit alma tek bir atomik yazmadir: satir yoksa olusur, varsa yalniz kirasi
// dolmussa devralinir. Iki kosum ayni anda denerse yalniz biri satiri degistirir.
const kilitAl = (ad, sahip) => {
  const simdi = Date.now();
  return db.prepare(
    `INSERT INTO kilit (ad, sahip, bitis) VALUES (?, ?, ?)
       ON CONFLICT(ad) DO UPDATE SET sahip = excluded.sahip, bitis = excluded.bitis
       WHERE kilit.bitis < ?`).run(ad, sahip, simdi + KIRA_MS, simdi).changes === 1;
};
const kilitBirak = (ad, sahip) =>
  db.prepare("DELETE FROM kilit WHERE ad = ? AND sahip = ?").run(ad, sahip);

function raporUret() {
  appendFileSync("iz.log", `basladi ${ETIKET}\n`);
  const son = Date.now() + 300;                    // 300 ms suren rapor uretimi
  while (Date.now() < son);
  appendFileSync("iz.log", `bitti ${ETIKET}\n`);
}

if (!KILITLI) raporUret();
else if (kilitAl("aylik-rapor", ETIKET)) {
  try { raporUret(); } finally { kilitBirak("aylik-rapor", ETIKET); }
} else {
  appendFileSync("iz.log", `atlandi ${ETIKET}\n`);
}
```

Üç ayrıntı kilidi doğru yapan şeydir. Kilit **adı** görev adıdır, süreç adı değil; farklı
örnekler aynı adı kullanır. Kilidi bırakma `sahip` sütununu da sorgular, böylece kirası
dolduğu için kilidi devralınmış bir koşum, başkasının kilidini silemez. Ve bırakma
`finally` içindedir, dolayısıyla hata durumunda da çalışır.

## Ölçüm

Kilidin işe yarayıp yaramadığı iddia edilmez, ölçülür. Aşağıdaki sürücü aynı görevi iki
kez aynı anda başlatır ve `iz.log` dosyasındaki başlama–bitiş imlerinden **eş zamanlı
koşum zirvesini** hesaplar.

```js
// surucu.mjs — ayni gorevi iki kez ayni anda baslatir ve izi cozumler
import { spawn } from "node:child_process";
import { writeFileSync, readFileSync, rmSync } from "node:fs";

const calistir = (argv) =>
  new Promise((c) => spawn("node", argv, { stdio: "ignore" }).on("exit", c));

function cozumle() {
  let acik = 0, zirve = 0, kosum = 0, atlanan = 0;
  for (const s of readFileSync("iz.log", "utf8").trim().split("\n")) {
    if (s.startsWith("basladi")) { acik++; kosum++; zirve = Math.max(zirve, acik); }
    else if (s.startsWith("bitti")) acik--;
    else atlanan++;
  }
  return { kosum, atlanan, zirve };
}

for (const kip of ["kilitsiz", "kilitli"]) {
  writeFileSync("iz.log", "");
  rmSync("zamanlama.db", { force: true });
  const ek = kip === "kilitli" ? ["--kilit"] : [];
  await Promise.all([
    calistir(["gorev.mjs", "A", ...ek]),
    calistir(["gorev.mjs", "B", ...ek]),
  ]);
  const { kosum, atlanan, zirve } = cozumle();
  console.log(`${kip.padEnd(9)} kosum=${kosum} atlanan=${atlanan} es zamanli zirve=${zirve}`);
}
```

```
kilitsiz  kosum=2 atlanan=0 es zamanli zirve=2
kilitli   kosum=1 atlanan=1 es zamanli zirve=1
```

Kilitsiz kipte iki koşum da rapor üretiyor ve zirve 2 çıkıyor: aylık ödünç raporu iki kez
hesaplanmış demektir. Kilitli kipte koşum sayısı 1, atlanan 1 ve zirve 1. İkinci koşum
kilidi alamadığı için işi yapmadan çıkıyor.

Atlanan koşumun **hata sayılmadığına** dikkat edin. Zamanlanmış görevlerde kilidi
alamamak beklenen bir sonuçtur; kütüklenir ama uyarı üretmez. Uyarı üreten şey, aynı
görevin uzun süre hiç koşamamış olmasıdır.

## Kaçırılan Koşum

Kilit çakışmayı önler ama bir soruyu açık bırakır: koşum kilit yüzünden ya da servis kapalı
olduğu için hiç yapılamadıysa, sonradan telafi edilecek mi.

İki politika vardır. **Telafisiz** politikada kaçırılan koşum unutulur; bir sonraki
tetiklemede normal koşum yapılır. Şube stok özeti gibi son durumu hesaplayan görevler
buna uygundur, çünkü yeni koşum eskisinin sonucunu zaten kapsar. **Telafili** politikada
kaçırılan her aralık için ayrı koşum üretilir. Aylık ödünç raporu buna uygundur, çünkü
her ayın raporu ayrı bir çıktıdır ve biri atlanırsa yerine geçen olmaz.

Telafili politikanın tehlikesi, uzun bir kesintiden sonra biriken koşumların hepsinin bir
anda tetiklenmesidir. Bu yüzden telafi bir üst sınırla verilir: en fazla son N aralık
telafi edilir, gerisi kütüğe düşer.

## Özet

- Yineleyen görev tanımı "şu andan sonraki koşum anı" işlevine indirgenir; "şimdi" değerini dışarıdan almak, zamanlamayı sınanabilir kılar.
- Ayın olmayan gününe bağlanan tanımlar taşarak sonraki aya kayar; atlama ya da ayın son gününe kelepçeleme politikası açıkça yazılmalıdır.
- Sabit hız kaymayı önler ama çakışmaya açıktır; sabit gecikme çakışmayı önler ama koşum anlarını kaydırır.
- Çakışma uzun koşumdan, çok örnekli dağıtımdan ve yeniden dağıtımdan doğar; üçünün de çözümü koşmadan önce alınan kira kilididir.
- Kilidin doğruluğu almanın atomikliğine, dayanıklılığı ise kira süresinin dolmasıyla kendiliğinden düşmesine bağlıdır; ölçüm eş zamanlı koşum zirvesini 2'den 1'e indirmiştir.

## Sonraki Adım

Bu derste görevler kısaydı: üç yüz milisaniyede biten bir rapor üretimi. Kira süresi de
buna göre seçilebildi. Ama arka plan işlerinin bir bölümü dakikalarca sürer — otuz bin
ödünç kaydını tarayan aylık rapor, yüklenen kapak görselinin farklı boyutlarda yeniden
üretilmesi. Böyle bir iş başladıktan sonra iki soru doğar: işi başlatan taraf ne kadar
ilerlendiğini nasıl öğrenir ve işten vazgeçildiğinde iş nerede durur. Sonraki ders uzun
süren işlerde ilerleme bildirimini ve iptali kurar.
