Ders 18 / 25
Uzun Süren İşler
Dakikalarca süren arka plan işinin yönetilebilir hale getirilmesi: işin parçalara bölünmesi, ilerlemenin dayanıklı bir durum kaydına yazılması, işbirlikçi iptalin iptal imiyle kurulması ve iptalin bıraktığı yerden sürdürme.
İçindekiler
Bir önceki ders yineleyen görevleri kurdu ve iki koşumun üst üste binmesini kira kilidiyle önledi. Oradaki görev kısaydı: üç yüz milisaniyede biten bir rapor üretimi. Kira süresi de buna göre seçilebiliyordu.
Arka plan işlerinin bir bölümü ise dakikalarca sürer. Otuz bin ödünç kaydını tarayan aylık rapor, yüklenen kapak görselinin dört ayrı boyutta yeniden üretilmesi, şube stok özetinin baştan hesaplanması. Böyle bir iş başlar başlamaz iki soru doğar. İşi başlatan taraf ne kadar ilerlendiğini nasıl öğrenir. Ve işten vazgeçildiğinde — kullanıcı sekmeyi kapattı, servis kapanıyor, kira doldu — iş nerede durur ve orada ne bırakır.
İşin Parçalara Bölünmesi
Uzun bir işi yönetilebilir kılan ilk karar, onu tek bir bloktan parçalar dizisine çevirmektir. Parçalara bölünmemiş bir iş üç şeyi birden yapamaz: ilerleme bildiremez, iptal edilemez, yarıda kalırsa baştan başlamaktan başka seçenek bırakmaz.
Bölme kuralı basittir: iş, her turda sabit sayıda birim işleyen bir döngüye çevrilir ve her turun sonunda iki şey yapılır. Denetim noktası olarak durum yazılır ve olay döngüsüne geri dönülür. İkincisi tek iş parçacıklı bir çalışma zamanında zorunludur: geri dönülmezse süreç o iş bitene kadar başka hiçbir şey yapamaz, sağlık yoklamasına bile cevap veremez.
İlerleme Bildirimi ve İptal
İlerleme iki yere birden yazılır. Bellekte tutulan bir sayaç yeterli değildir, çünkü
süreç düştüğünde o sayaç da gider. Bu yüzden ilerleme, işin kimliğiyle anahtarlanmış
dayanıklı bir durum kaydına yazılır — burada is_durumu tablosuna.
İptal ise işbirlikçidir (cooperative): dışarıdan bir sürecin döngüsünü zorla kesmek
mümkün değildir. Dışarıdaki taraf yalnız bir im kaldırır; işi yapan taraf o imi kendi
uygun gördüğü noktalarda okur ve durur. JavaScript’te bu im AbortController ile
üretilir; signal.throwIfAborted() çağrısı im kalkmışsa hata atar.
// uzun-is.mjs — aylik odunc raporu: parca parca ilerler, ilerleme bildirir, iptali karsilar import { DatabaseSync } from "node:sqlite"; const db = new DatabaseSync(":memory:"); db.exec(`CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, uye_id INTEGER NOT NULL, gun INTEGER NOT NULL); CREATE TABLE is_durumu (is_id TEXT PRIMARY KEY, durum TEXT NOT NULL, islenen INTEGER NOT NULL, birikim INTEGER NOT NULL)`); const ekle = db.prepare("INSERT INTO odunc (odunc_id, uye_id, gun) VALUES (?, ?, ?)"); for (let i = 1; i <= 30_000; i++) ekle.run(i, 100 + (i % 500), i % 30); const TOPLAM = db.prepare("SELECT count(*) AS n FROM odunc").get().n; const PARCA = 3000; // bir turda islenen kayit sayisi const durumOku = (isId) => db.prepare("SELECT islenen, birikim FROM is_durumu WHERE is_id = ?").get(isId) ?? { islenen: 0, birikim: 0 }; const durumYaz = (isId, durum, islenen, birikim) => db.prepare(`INSERT INTO is_durumu (is_id, durum, islenen, birikim) VALUES (?, ?, ?, ?) ON CONFLICT(is_id) DO UPDATE SET durum = excluded.durum, islenen = excluded.islenen, birikim = excluded.birikim`).run(isId, durum, islenen, birikim); async function raporUret(isId, signal, bildir) { let { islenen, birikim } = durumOku(isId); // sifirdan ya da birakildigi yerden try { while (islenen < TOPLAM) { signal.throwIfAborted(); // iptal yalniz parca sinirinda gorulur for (const s of db.prepare( "SELECT gun FROM odunc ORDER BY odunc_id LIMIT ? OFFSET ?").all(PARCA, islenen)) birikim += s.gun; islenen += PARCA; durumYaz(isId, "calisiyor", islenen, birikim); // once yaz, sonra bildir bildir(Math.round((islenen / TOPLAM) * 100), islenen); await new Promise((c) => setImmediate(c)); // olay dongusune donus noktasi } durumYaz(isId, "bitti", islenen, birikim); return birikim; } catch (h) { durumYaz(isId, "iptal", islenen, birikim); // birakilan yer kaydedilir throw h; } } const IS = "rapor-2026-01"; const yaz = (yuzde, islenen) => console.log(` ilerleme %${yuzde} islenen=${islenen}`); console.log("--- ilk kosum: %40'a gelince iptal ediliyor ---"); const denetim = new AbortController(); try { await raporUret(IS, denetim.signal, (yuzde, islenen) => { yaz(yuzde, islenen); if (yuzde >= 40) denetim.abort(new Error("kullanici iptal etti")); }); } catch (h) { const d = durumOku(IS); console.log(`iptal: ${h.message} islenen=${d.islenen}/${TOPLAM} birikim=${d.birikim}`); } console.log("--- ikinci kosum: birakilan yerden suruyor ---"); const birikim = await raporUret(IS, new AbortController().signal, yaz); console.log(`bitti: birikim=${birikim} ortalama gun=${(birikim / TOPLAM).toFixed(2)}`);
--- ilk kosum: %40'a gelince iptal ediliyor --- ilerleme %10 islenen=3000 ilerleme %20 islenen=6000 ilerleme %30 islenen=9000 ilerleme %40 islenen=12000 iptal: kullanici iptal etti islenen=12000/30000 birikim=174000 --- ikinci kosum: birakilan yerden suruyor --- ilerleme %50 islenen=15000 ilerleme %60 islenen=18000 ilerleme %70 islenen=21000 ilerleme %80 islenen=24000 ilerleme %90 islenen=27000 ilerleme %100 islenen=30000 bitti: birikim=435000 ortalama gun=14.50
İptal, ilerleme yüzde kırka ulaştığında tetikleniyor; ölçümün yinelenebilir olması için iptal anı bir süreye değil ilerleme eşiğine bağlanmış durumda. Çıktı üç şeyi gösteriyor.
İş on iki bin kayıtta durdu, otuz binde değil. Duruş noktası is_durumu tablosuna
yazıldı: durum sütunu iptal, islenen sütunu 12000. Ve ikinci koşum sıfırdan değil,
yüzde ellilik ilerlemeden devam etti; toplam birikim 435000, yani hiçbir kayıt iki kez
sayılmadı.
Üçüncüsü ilk ikisinden daha önemli. İptal edilen iş, sürdürülebilir bir yerde bırakıldı. Bu, uzun süren işlerde iptalin tek anlamlı tanımıdır: iptal, işi geri almak değil, işi tutarlı bir noktada dondurmaktır.
Yazma Sırası
Koddaki durumYaz çağrısının bildir çağrısından önce olması bir karardır. Ters
sırada yazılsaydı, bildirimden sonra süreç düştüğünde ilerleme bildirilmiş ama
kaydedilmemiş olurdu; sürdürme, aslında işlenmiş kayıtları yeniden işlerdi.
Aynı gerekçeyle durumYaz ile parçanın işlenmesi tek bir işlem sınırı içinde olmalıdır.
Rapor birikimi burada bellekte tutulduğu ve durumla birlikte yazıldığı için bu koşul
sağlanıyor. Parça bir tabloya satır yazsaydı, o yazma ile ilerleme güncellemesi
Veri Erişim Katmanı ve İş Mantığı kursunda tanıtılan iş birimi içinde birlikte
işlenmeliydi.
Parça Boyutu
Parça boyutu tek bir sayı gibi görünür ama iki büyüklüğü aynı anda belirler. Küçük parça iptali çabuklaştırır, çünkü im ancak parça sınırında okunur; büyük parça ilerleme yazmalarını azaltır, çünkü her tur bir yazma demektir.
| Parça boyutu | İlerleme yazması | İptal gecikmesi (en çok) |
|---|---|---|
| 100 | 300 | 100 kayıt |
| 500 | 60 | 500 kayıt |
| 3000 | 10 | 3000 kayıt |
| 10000 | 3 | 10000 kayıt |
Otuz bin kayıtlık iş için sayılar bölmeyle çıkar: yazma sayısı 30000 / parça, iptal
gecikmesi ise en kötü durumda bir parçadır. Yüzlük parça, kullanıcının iptal düğmesine
bastıktan sonra beklediği süreyi kısaltır ama durum tablosuna üç yüz yazma yapar; on
binlik parça yazmayı üçe indirir ama iptal on bin kaydın işlenmesi kadar sürer.
Seçim işin doğasına göre yapılır. Kullanıcının başlattığı ve beklediği bir iş için iptal gecikmesi ağır basar. Gece çalışan ve kimsenin izlemediği bir iş için yazma sayısı ağır basar; oradaki ilerleme kaydı iptal için değil, kaldığı yerden sürdürme için tutulur.
İptalin Kaynakları
İptal imi tek bir yerden gelmez. Dört kaynağı vardır ve dördü de aynı ime bağlanabilir.
Kullanıcı iptali en görünür olanıdır: rapor ekranındaki durdurma isteği, işin
kimliğiyle gelen bir çağrıya dönüşür ve o çağrı imi kaldırır. Süre sınırı ikinci
kaynaktır; AbortSignal.timeout(ms) belirtilen süre sonunda kendiliğinden kalkan bir im
üretir ve işin kabul edilemez uzunlukta sürmesini engeller. Kapanma üçüncüsüdür:
önceki derste kurulan zarif kapanma sırasında SIGTERM alan süreç, çalışan işlerin
imini kaldırıp bitmelerini bekler. Kira bitimi dördüncüsüdür; işin kilidi devralınmak
üzereyse eski koşum kendini iptal etmelidir, yoksa iki koşum aynı işi sürdürür.
Dördü de aynı yapıya bağlandığında iş kodunda tek bir denetim noktası kalır. AbortSignal.any([...])
birden çok imi tek bir ime toplar; iş yalnız o tek imi okur ve kimin iptal ettiğini
bilmesi gerekmez.
İşin Sonucu Nereye Gider
İlerleme bildiriminin bir eksiği vardır: burada console.log ile yazıldı. Gerçek bir
serviste ilerlemeyi okuması gereken taraf başka bir süreçtedir — raporu isteyen istemci.
Bu yüzden is_durumu tablosu yalnız sürdürme için değil, sorgulanabilir bir durum
kaydı olarak da işe yarar: istemci işin kimliğiyle bu kaydı okuyabilir.
Bu okumanın nasıl yapılacağı — istemcinin durumu tekrar tekrar sorması mı, yoksa sunucunun değişiklikleri kendiliğinden göndermesi mi — bu kursun son konusunun sorusudur.
Özet
- Uzun bir iş sabit büyüklükte parçalar işleyen bir döngüye çevrilir; her turun sonunda durum yazılır ve olay döngüsüne dönülür.
- İlerleme belleğe değil, işin kimliğiyle anahtarlanmış dayanıklı bir durum kaydına yazılır; kayıt hem sorgulanabilir ilerleme hem de sürdürme noktasıdır.
- İptal işbirlikçidir: dışarıdaki taraf yalnız bir im kaldırır, işi yapan taraf o imi parça sınırlarında okur ve tutarlı bir noktada durur.
- Durum yazması bildirimden önce yapılır; ters sıra, bildirilmiş ama kaydedilmemiş ilerleme yüzünden aynı kayıtların iki kez işlenmesine yol açar.
- Parça boyutu iptal gecikmesi ile ilerleme yazma sayısı arasındaki ödünleşimdir; kullanıcının beklediği işte gecikme, gece koşan işte yazma sayısı ağır basar.
Sonraki Adım
Bu derste tek bir iş vardı ve onu bölmek yeterliydi. Gerçek bir arka planda ise işler sürekli akar: her ödünç işlemi bir bildirim işi, her kapak yüklemesi bir görsel işleme işi doğurur. İşlerin geliş hızı ile işçilerin bitirme hızı birbirinden bağımsız olduğu için ikisi eşit olmak zorunda değildir. Geliş hızı bitirme hızını aştığında kuyruk büyür; büyüme sürerse bellek, disk ya da gecikme bütçesi tükenir. Sonraki ders bu dengesizliğin adını koyar ve sınırlı kuyruğun seçeneklerini sayılarla karşılaştırır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.