Ders 15 / 18
Saga Kalıbı
Telafi edici işlemlerle yürüyen iş akışının iki gerçekleştirim kısıtı: adım sırasının telafi edilebilirliğe göre seçilmesi ve her hata noktasında geride kalan geri alınamayan yan etkinin sayılması; koreografi ile yürütücünün kod tarafındaki farkı — akış bilgisini taşıyan dosya, adlandırılan adım, bir adım eklenince dokunulan dosya.
İçindekiler
Önceki ders iki aşamalı işlemenin bedelini gösterdi: kaynak, en yavaş katılımcının hızı kadar tutuldu ve eşgüdümcü düştüğünde askıda kaldı. Saga kalıbı bu pencereyi tamamen kaldırır. Her adım kendi işlemini hemen kesinleştirir; bir adım başarısız olursa tamamlananlar tersten telafi edilir.
Telafi adımının kendisi M16/K04’ün dağıtık işlem sorunu dersinde kurulmuştur, telafi edici
işlemin bedeli M19/K05’in dağıtık doğruluk konusunda ölçülmüştür. İkisi de burada
tekrarlanmıyor. Bu dersin ölçtüğü şey ikisinin de değinmediği bir kısıttır: adımların
sırası. Sıra keyfî değildir, çünkü her adım geri alınabilir değildir; ve sıranın seçimi,
başarısızlık anında sistemden dışarı çıkmış ve geri çağrılamayan etkinin sayısını belirler.
Düzenek
DT3. Bu derste servisler ayrı süreçler değil, aynı süreçte çalışan ve ayrı depolara yazan modüllerdir. Modelin taşıdığı özellik, her adımın kendi işlem sınırında kesinleşmesi ve ortak bir geri alma çağrısının bulunmamasıdır. Ağ gecikmesi ve kısmi hata modelde yoktur.
DT4. Bildirim adımının dış etkisi, bir dosyaya satır yazmakla modellenir. Satırı silmek teslimi geri almadığı için bu adımın telafisi yoktur; yapılabilecek tek şey ikinci bir bildirimle düzeltmedir.
# kur.sh — dort servisin ayri depolari ve dis dunyaya giden bildirim gunlugu rm -f katalog.db uyelik.db ucret.db gonderilen.log sqlite3 katalog.db "CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, durumu TEXT NOT NULL); INSERT INTO kitap VALUES (1,'rafta'),(2,'rafta');" sqlite3 uyelik.db "CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, acik_odunc INTEGER NOT NULL); INSERT INTO uye VALUES (4,0);" sqlite3 ucret.db "CREATE TABLE hareket (id INTEGER PRIMARY KEY, uye_id INTEGER, tutar INTEGER);"
// adimlar.mjs — odunc akisinin dort adimi; her adimin telafisi ya vardir ya yoktur import { DatabaseSync } from "node:sqlite"; import { appendFileSync, readFileSync, writeFileSync } from "node:fs"; const ac = (d) => new DatabaseSync(d); const katalogDb = ac("katalog.db"), uyelikDb = ac("uyelik.db"), ucretDb = ac("ucret.db"); const gonder = (satir) => appendFileSync("gonderilen.log", satir + "\n"); export const adimlar = { "katalog.ayirt": { yap: (i) => katalogDb.prepare("UPDATE kitap SET durumu='odunc' WHERE kitap_id=?").run(i.kitap), telafi: (i) => katalogDb.prepare("UPDATE kitap SET durumu='rafta' WHERE kitap_id=?").run(i.kitap) }, "ucret.tahsil": { yap: (i) => ucretDb.prepare("INSERT INTO hareket (uye_id,tutar) VALUES (?,5)").run(i.uye), telafi: (i) => ucretDb.prepare("INSERT INTO hareket (uye_id,tutar) VALUES (?,-5)").run(i.uye) }, "uyelik.oduncEkle": { yap: (i) => uyelikDb.prepare("UPDATE uye SET acik_odunc=acik_odunc+1 WHERE uye_id=?").run(i.uye), telafi: (i) => uyelikDb.prepare("UPDATE uye SET acik_odunc=acik_odunc-1 WHERE uye_id=?").run(i.uye) }, "bildirim.gonder": { // sistemin disina cikar: geri alinamaz yap: (i) => gonder(`uye ${i.uye}: kitap ${i.kitap} hazir`), telafi: null, duzeltme: (i) => gonder(`uye ${i.uye}: kitap ${i.kitap} icin onceki bildirim gecersiz`) }, }; export function sifirla() { katalogDb.exec("UPDATE kitap SET durumu='rafta'"); uyelikDb.exec("UPDATE uye SET acik_odunc=0"); ucretDb.exec("DELETE FROM hareket"); writeFileSync("gonderilen.log", ""); } export const gonderim = () => readFileSync("gonderilen.log", "utf8").split("\n").filter((s) => s.length > 0);
Dört adımın üçünün telafisi var, birinin yok. Ayırt edici tek fark budur ve bütün sıralama kısıtı bu farktan doğar.
Sıralama Kısıtı
Aynı dört adım iki farklı sıraya dizilir ve her sıra bütün hata noktaları için ayrı ayrı koşturulur. Ölçülen nicelik, telafi bittikten sonra geride kalan geri alınamayan yan etki sayısıdır.
// tarama.mjs — ayni dort adim, iki farkli sira; her hata noktasi ayri ayri taranir import { adimlar, sifirla, gonderim } from "./adimlar.mjs"; const siralar = { "A (bildirim erken)": ["katalog.ayirt", "bildirim.gonder", "ucret.tahsil", "uyelik.oduncEkle"], "B (bildirim sonda)": ["katalog.ayirt", "ucret.tahsil", "uyelik.oduncEkle", "bildirim.gonder"], }; function kos(sira, hataAdimi, istek) { sifirla(); const yapilan = []; let telafi = 0, geriAlinamayan = 0, duzeltme = 0; try { for (const ad of sira) { if (yapilan.length + 1 === hataAdimi) throw new Error(`${ad} basarisiz`); adimlar[ad].yap(istek); yapilan.push(ad); } } catch { for (const ad of [...yapilan].reverse()) { const a = adimlar[ad]; if (a.telafi) { a.telafi(istek); telafi += 1; } else { geriAlinamayan += 1; if (a.duzeltme) { a.duzeltme(istek); duzeltme += 1; } } } } return { tamamlanan: yapilan.length, telafi, geriAlinamayan, duzeltme, gonderim: gonderim().length }; } for (const [ad, sira] of Object.entries(siralar)) { console.log(`\n${ad}: ${sira.join(" > ")}`); console.log("hata_adimi tamamlanan telafi geri_alinamayan duzeltme toplam_gonderim"); let toplam = 0; for (let k = 1; k <= sira.length; k++) { const s = kos(sira, k, { kitap: 1, uye: 4 }); toplam += s.geriAlinamayan; console.log(` ${k} ${s.tamamlanan} ${s.telafi} ` + `${s.geriAlinamayan} ${s.duzeltme} ${s.gonderim}`); } console.log(`toplam geri alinamayan yan etki: ${toplam}`); }
sh kur.sh node tarama.mjs
A (bildirim erken): katalog.ayirt > bildirim.gonder > ucret.tahsil > uyelik.oduncEkle
hata_adimi tamamlanan telafi geri_alinamayan duzeltme toplam_gonderim
1 0 0 0 0 0
2 1 1 0 0 0
3 2 1 1 1 2
4 3 2 1 1 2
toplam geri alinamayan yan etki: 2
B (bildirim sonda): katalog.ayirt > ucret.tahsil > uyelik.oduncEkle > bildirim.gonder
hata_adimi tamamlanan telafi geri_alinamayan duzeltme toplam_gonderim
1 0 0 0 0 0
2 1 1 0 0 0
3 2 2 0 0 0
4 3 3 0 0 0
toplam geri alinamayan yan etki: 0
Aynı adımlar, aynı iş, aynı telafi kodu. Fark yalnız sıradadır ve sonuç dört hata noktasında
2’ye karşı 0’dır. A sırasında bildirim ikinci adım olduğu için, ondan sonraki her
başarısızlıkta üyeye yanlış bir haber gitmiş olur; geri alınamadığı için tek yapılabilen,
üyeye ikinci bir ileti daha göndermektir. Toplam gönderim 0 değil 2’dir: iş
gerçekleşmediği hâlde iki teslim yapılmıştır.
B sırası bunu sıfıra indirir ama bedelsiz değildir. B’nin son satırında telafi sayısı 3; A’nın aynı satırında 2. Telafi edilemeyen adım sona alındığında, o adım başarısız olduğunda geri sarılacak iş en fazladır. Yani sıralama kararı iki nicelik arasında yapılır: geri alınamayan yan etki ile geri sarılan tamamlanmış adım. Birincisi kullanıcıya görünür ve düzeltilemez, ikincisi yalnız pahalıdır. Kural bu asimetriden çıkar: telafi edilemeyen adımlar sıranın sonuna konur.
Aynı Akış, İki Gerçekleştirme
Sıra belirlendikten sonra geriye onu kimin yürüteceği kalır. İki gerçekleştirme vardır ve ikisi de aynı sonucu üretir. Yürütücü kipinde akış tek bir modülde durur; servis modülleri sıradaki yerlerini bilmez.
// yurutucu.mjs — akisin tamami tek modulde; servis modulleri akisi hic bilmez import { adimlar } from "./adimlar.mjs"; export const sira = ["katalog.ayirt", "ucret.tahsil", "uyelik.oduncEkle", "bildirim.gonder"]; export function yurut(istek) { const yapilan = []; for (const ad of sira) { adimlar[ad].yap(istek); yapilan.push(ad); } return yapilan; // telafi sirasi da bu listeden okunur }
Koreografi kipinde merkezî bir yürütücü yoktur; her servis kendinden sonrakini tetikler. Telafi de geriye doğru yürüdüğü için her düğüm önceki komşusunu da bilmek zorundadır.
// d-katalog.mjs — katalog dugumu: kendi adimi ve akistaki iki komsusu export const dugum = { ad: "katalog", adim: "katalog.ayirt", onceki: null, sonraki: "ucret" };
// d-ucret.mjs — ucret dugumu: kendi adimi ve akistaki iki komsusu export const dugum = { ad: "ucret", adim: "ucret.tahsil", onceki: "katalog", sonraki: "uyelik" };
// d-uyelik.mjs — uyelik dugumu: kendi adimi ve akistaki iki komsusu export const dugum = { ad: "uyelik", adim: "uyelik.oduncEkle", onceki: "ucret", sonraki: "bildirim" };
// d-bildirim.mjs — bildirim dugumu: kendi adimi ve akistaki iki komsusu export const dugum = { ad: "bildirim", adim: "bildirim.gonder", onceki: "uyelik", sonraki: null };
// koreografi.mjs — merkezi yurutucu yok; veri yolu sirayi bilmez, her dugum komsusunu bilir import { adimlar } from "./adimlar.mjs"; import { dugum as a } from "./d-katalog.mjs"; import { dugum as b } from "./d-ucret.mjs"; import { dugum as c } from "./d-uyelik.mjs"; import { dugum as d } from "./d-bildirim.mjs"; const yol = Object.fromEntries([a, b, c, d].map((x) => [x.ad, x])); export function baslat(ilkDugum, istek) { const yapilan = []; for (let x = yol[ilkDugum]; x; x = x.sonraki ? yol[x.sonraki] : null) { adimlar[x.adim].yap(istek); yapilan.push(x.adim); // her dugum bir sonrakini tetikler } return yapilan; }
koreografi.mjs dört düğümü de içeri alır, ama sırayı bilmez: hangi düğümün nereye
bağlandığı düğümlerin kendi dosyalarındadır. Modül bir kayıt defteridir, akış tanımı değil.
Kod Tarafındaki Fark
Ölçüm kaynağın kendisinden okunur: her dosyada, kendisi dışındaki kaç servisin adı geçiyor?
// olc-kod.mjs — akis bilgisinin kaynak metinde kac dosyaya dagildigini sayar import { readFileSync } from "node:fs"; import { sifirla, gonderim } from "./adimlar.mjs"; import { yurut, sira } from "./yurutucu.mjs"; import { baslat } from "./koreografi.mjs"; const servisler = ["katalog", "ucret", "uyelik", "bildirim"]; const kenar = ["ucret", "uyelik"]; // yeni adim bu ikisinin arasina girecek function olc(etiket, dosyalar) { let adlandirilan = 0, tasiyan = 0, dokunulan = 0; for (const f of dosyalar) { const metin = readFileSync(f, "utf8"); const kendi = f.replace(/^d-/, "").replace(/\.mjs$/, ""); const n = servisler.filter((s) => s !== kendi && metin.includes(s)).length; adlandirilan += n; if (n > 0) tasiyan += 1; if (kenar.every((s) => metin.includes(s))) dokunulan += 1; } console.log(`${etiket}: akis adi tasiyan dosya=${tasiyan}/${dosyalar.length}` + ` adlandirilan adim=${adlandirilan} | akisi okumak icin acilacak dosya=${tasiyan}` + ` | araya adim eklenince dokunulan mevcut dosya=${dokunulan}`); } sifirla(); const y = yurut({ kitap: 1, uye: 4 }); console.log(`yurutucu sonuc: adim=${y.length} gonderim=${gonderim().length} [${y.join(",")}]`); sifirla(); const k = baslat("katalog", { kitap: 1, uye: 4 }); console.log(`koreografi sonuc: adim=${k.length} gonderim=${gonderim().length} [${k.join(",")}]`); console.log(`ayni sonuc mu: ${JSON.stringify(y) === JSON.stringify(k)}`); olc("yurutucu ", ["yurutucu.mjs"]); olc("koreografi", ["d-katalog.mjs", "d-ucret.mjs", "d-uyelik.mjs", "d-bildirim.mjs"]); console.log(`akis uzunlugu (iki gerceklestirmede de): ${sira.length} adim`);
sh kur.sh
node olc-kod.mjs
yurutucu sonuc: adim=4 gonderim=1 [katalog.ayirt,ucret.tahsil,uyelik.oduncEkle,bildirim.gonder] koreografi sonuc: adim=4 gonderim=1 [katalog.ayirt,ucret.tahsil,uyelik.oduncEkle,bildirim.gonder] ayni sonuc mu: true yurutucu : akis adi tasiyan dosya=1/1 adlandirilan adim=4 | akisi okumak icin acilacak dosya=1 | araya adim eklenince dokunulan mevcut dosya=1 koreografi: akis adi tasiyan dosya=4/4 adlandirilan adim=6 | akisi okumak icin acilacak dosya=4 | araya adim eklenince dokunulan mevcut dosya=2 akis uzunlugu (iki gerceklestirmede de): 4 adim
Koşum tarafı özdeş: dört adım, bir gönderim, aynı adım listesi. Fark tümüyle kod tarafındadır.
Yürütücüde akış bilgisi 1 dosyada ve 4 adlandırma olarak durur. Koreografide 4 dosyaya
dağılır ve 6 adlandırmaya çıkar; fazlalık, telafinin geriye yürüyebilmesi için gereken
onceki kenarlarından gelir. Yürütücünün yapilan listesi tek bir yerde biriktiği için o
kenarlara ihtiyaç duymaz.
Araya yeni bir adım eklenmesi yürütücüde 1, koreografide 2 mevcut dosyaya dokunur: yeni adımın
öncesindeki düğümün sonraki alanı ile sonrasındaki düğümün onceki alanı birlikte
değişmek zorundadır. Buna karşılık koreografide hiçbir modül bütün akışı bilmez; bir servisin
değişmesi yürütücüyü değiştirmez ve akışa yeni bir dinleyici eklemek hiçbir mevcut dosyaya
dokunmadan yapılabilir.
Neyi ucuzlattı: saga, iki aşamalı işlemenin kilit penceresini sıfırladı; her adım kendi işlemini hemen kesinleştirdiği için hiçbir kaynak başka bir servisin yanıtı beklenirken tutulmaz. Yürütücü kipi ayrıca akışın tamamını tek dosyada okunur kıldı.
Neyi pahalılaştırdı: her adım için ikinci bir gerçekleştirme — telafi — yazılmak zorunda. Adım sırası artık serbest değil; telafi edilebilirliğe göre kısıtlı. Koreografi kipinde akış bilgisi 1 dosyadan 4 dosyaya, 4 adlandırmadan 6 adlandırmaya dağıldı.
Hangi yeni arıza kipi doğdu: yarım kalmış akış. Telafi edilemeyen adım tamamlandıktan
sonra çıkan hata, sistemi ne başlangıç ne bitiş durumunda bırakır; A sırasında bu, iş hiç
olmadığı hâlde üyeye giden 2 teslim olarak ölçüldü. Yürütücü kipinde ek olarak yürütücünün
kendisi tek arıza noktasıdır: yapilan listesi onunla birlikte kaybolursa telafi sırası
kimsede kalmaz.
Özet
- Saga, iki aşamalı işlemenin kilit penceresini kaldırır; karşılığında her adımın telafisini yazmayı ve adımları telafi edilebilirliğe göre sıralamayı zorunlu kılar.
- Aynı dört adım iki sıraya dizildiğinde, dört hata noktası boyunca geri alınamayan yan etki A sırasında 2, B sırasında 0 çıktı; A’da iş hiç olmadığı hâlde 2 teslim yapıldı.
- Bedel karşılıklıdır: B sırasında son adımın başarısızlığı 3 telafi çağrısı gerektirdi, A’da 2. Telafi edilemeyen adım sona konur, çünkü geri sarılan iş pahalıdır ama düzeltilir; geri alınamayan etki düzeltilmez.
- Yürütücü ve koreografi aynı sonucu üretti — 4 adım, 1 gönderim, aynı adım listesi — fark yalnız kodda: akış bilgisi 1 dosya ve 4 adlandırmaya karşı 4 dosya ve 6 adlandırma.
- Araya adım eklemek yürütücüde 1, koreografide 2 mevcut dosyaya dokunur; buna karşılık koreografide hiçbir modül akışın tamamını bilmez ve yeni dinleyici eklemek mevcut dosyalara dokunmaz.
Sonraki Adım
Bu derste her adım “yap” çağrısını doğrudan yaptı; adımın kesinleşmesiyle bir sonrakine haber verilmesi arasında hiçbir boşluk yokmuş gibi davranıldı. Gerçekte bu iki iş ayrıdır: servis kendi deposuna yazar, sonra iletiyi yayımlar — ve ikisinin arasında düşebilir. Sonraki ders bu boşluğu ölçer: yayıncı çöktüğünde kaç ileti kaybolur, giden kutusuyla aynı yük altında kaç ileti ikinci kez teslim edilir, aktarıcının gecikmesi ne kadardır ve kutuda kaç satır birikir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.