Ders 17 / 18
Olay Sıralama ve Yinelenme
Tüketici tarafında etkisizlik: aynı olay akışı yinelenmiş ve sırası bozulmuş olarak teslim edildiğinde düz tüketicinin ürettiği fazladan yan etki ile yanlış biten görünüm, sürüm damgasının eski iletiyi atması ve etkisiz tüketicinin sorgu olarak ölçülen bedeli.
İçindekiler
Önceki ders yayıncı tarafını kapattı: giden kutusu hiçbir iletiyi kaybetmedi, ama 24 iletiyi iki kez teslim etti. Bu artık yayıncının çözebileceği bir sorun değil. Aktarıcının “gönderdim” kaydı ile tüketicinin “işledim” kaydı ayrı depolarda durduğu sürece ikisi aynı işleme giremez. Bedel tüketiciye geçer.
Tüketici tarafında iki ayrı bozulma vardır ve etkileri farklıdır. Birincisi yinelenme:
aynı ileti iki kez gelir. İkincisi sıra bozulması: geç kalan bir ileti, kendinden sonraki
bir iletinin ardından varır. M19/K05’in dağıtık doğruluk konusu tekillik anahtarını ve
anahtar defterini kurmuş ve ölçmüştür; o ölçüm burada tekrarlanmıyor. Anahtar defteri
yinelenmeyi keser ama sıra bozulmasına karşı bir şey yapmaz — bu ders ikisini ayırır ve
ikisini birden karşılayan bir gerçekleştirmenin bedelini ölçer.
Düzenek
DT7. Ağın bozulması tohumlu bir üreteçle modellenir: tohum 20260731, gecikme oranı
0,20, yinelenme oranı 0,15. Aynı oranlarda teslim akışı bütün koşumlarda birebir aynıdır;
dolayısıyla koşumlar arasındaki fark yalnız tüketicinin gerçekleştirmesinden gelir.
DT8. Tüketici süreç içi bir modüldür ve kendi deposuna yazar. Sorgu sayısı depodan değil, kodun kendi sayacından okunur; ölçülen şey tüketicinin yaptığı iş miktarıdır.
# kur.sh — tuketici tarafinin kendi deposu: gorunum tablosu ve yan etki gunlugu rm -f gorunum.db* sqlite3 gorunum.db "PRAGMA journal_mode=WAL; CREATE TABLE kitap_gorunum (kitap_id INTEGER PRIMARY KEY, durumu TEXT, son_surum INTEGER DEFAULT 0); CREATE TABLE bildirim (id INTEGER PRIMARY KEY, kitap_id INTEGER, durumu TEXT);" >/dev/null
Katalog her kitap için artan sürümlü olaylar üretir: tek sürümler ödünç verme, çift sürümler iade. Doğru sıra bu yüzden tek bir sayıdan okunabilir; sıra bozulması da öyle.
// akis.mjs — olay gunlugunu uretir, sonra teslim sirasini bozar ve iletileri yineler export function uretec(tohum) { // tohum gorunur, dizi her kosumda ayni let d = tohum >>> 0; return () => { d = (Math.imul(d, 1103515245) + 12345) >>> 0; return (d >>> 8) / 16777216; }; } export function gunluk(kitap, surumSayisi) { // katalogun urettigi dogru sirali gunluk const g = []; for (let s = 1; s <= surumSayisi; s++) for (let k = 1; k <= kitap; k++) g.push({ ileti_id: g.length + 1, kitap: k, surum: s, durumu: s % 2 ? "odunc" : "rafta" }); return g; } export function teslimSirasi(g, tohum, gecikme, yineleme) { const zar = uretec(tohum), cikti = [], bekleyen = []; for (const o of g) { if (zar() < gecikme) { bekleyen.push(o); continue; } // bu ileti gec kalacak cikti.push(o); if (bekleyen.length && zar() < 0.10) cikti.push(bekleyen.shift()); if (zar() < yineleme) cikti.push(o); // en az bir kez teslim: kopya } cikti.push(...bekleyen); // en gec kalanlar en sonda return cikti; }
Tüketici tek dosyadır ve iki gerçekleştirme içerir. --duz geleni olduğu gibi uygular.
--damgali her kitap için son işlediği sürümü saklar ve o sürümden büyük olmayan iletiyi
atar.
// tuketici.mjs — ayni akis, iki tuketici gerceklestirmesi: --duz ve --damgali import { DatabaseSync } from "node:sqlite"; import { gunluk, teslimSirasi } from "./akis.mjs"; const damgali = process.argv.includes("--damgali"); const KITAP = 10, SURUM = 300, TOHUM = 20260731; const gecikme = Number(process.env.GECIKME ?? 0.20); // sirayi bozan iletilerin orani const yineleme = Number(process.env.YINELEME ?? 0.15); // ikinci kez teslim edilenlerin orani const dogru = gunluk(KITAP, SURUM); const akis = teslimSirasi(dogru, TOHUM, gecikme, yineleme); const db = new DatabaseSync("gorunum.db"); const okuSurum = db.prepare("SELECT son_surum FROM kitap_gorunum WHERE kitap_id = ?"); const yaz = db.prepare( "INSERT INTO kitap_gorunum (kitap_id, durumu, son_surum) VALUES (?,?,?)" + " ON CONFLICT(kitap_id) DO UPDATE SET durumu = excluded.durumu, son_surum = excluded.son_surum"); const bildir = db.prepare("INSERT INTO bildirim (kitap_id, durumu) VALUES (?,?)"); let islenen = 0, atilan = 0, sorgu = 0, eskiVaris = 0, atilanKopya = 0, atilanEski = 0; const gorulen = new Map(), gorulenIleti = new Set(); const t0 = performance.now(); db.exec("BEGIN IMMEDIATE"); for (const o of akis) { if ((gorulen.get(o.kitap) ?? 0) > o.surum) eskiVaris += 1; // sirasi bozulmus varis gorulen.set(o.kitap, Math.max(gorulen.get(o.kitap) ?? 0, o.surum)); if (damgali) { const s = okuSurum.get(o.kitap); sorgu += 1; // ek okuma: surum damgasi if (s && s.son_surum >= o.surum) { atilan += 1; gorulenIleti.has(o.ileti_id) ? (atilanKopya += 1) : (atilanEski += 1); continue; } } gorulenIleti.add(o.ileti_id); yaz.run(o.kitap, o.durumu, o.surum); sorgu += 1; bildir.run(o.kitap, o.durumu); sorgu += 1; islenen += 1; } db.exec("COMMIT"); const sure = performance.now() - t0; const sonDurum = new Map(dogru.map((o) => [o.kitap, o.durumu])); let yanlis = 0; for (const r of db.prepare("SELECT kitap_id, durumu FROM kitap_gorunum").all()) if (sonDurum.get(r.kitap_id) !== r.durumu) yanlis += 1; const yanEtki = db.prepare("SELECT COUNT(*) c FROM bildirim").get().c; console.log(`${damgali ? "damgali" : "duz "} (gecikme=${gecikme} yineleme=${yineleme})` + ` | gunluk=${dogru.length} teslim=${akis.length}` + ` sirasi bozuk varis=${eskiVaris} | islenen=${islenen} atilan=${atilan} (kopya=${atilanKopya} eski=${atilanEski})` + ` yan etki=${yanEtki} yanlis biten kitap=${yanlis}/${KITAP}` + ` | sorgu=${sorgu} sure=${sure.toFixed(1)} ms (bu kosumda)`);
İki Bozulma, İki Farklı Zarar
İlk iki koşum bozulmaları tek tek uygular; üçüncüsü ikisini birden verir; dördüncüsü aynı akışı sürüm damgalı tüketiciye verir.
sh kur.sh; GECIKME=0 node tuketici.mjs --duz sh kur.sh; YINELEME=0 node tuketici.mjs --duz sh kur.sh; node tuketici.mjs --duz sh kur.sh; node tuketici.mjs --damgali
duz (gecikme=0 yineleme=0.15) | gunluk=3000 teslim=3452 sirasi bozuk varis=0 | islenen=3452 atilan=0 (kopya=0 eski=0) yan etki=3452 yanlis biten kitap=0/10 | sorgu=6904 sure=2.8 ms (bu kosumda) duz (gecikme=0.2 yineleme=0) | gunluk=3000 teslim=3000 sirasi bozuk varis=585 | islenen=3000 atilan=0 (kopya=0 eski=0) yan etki=3000 yanlis biten kitap=4/10 | sorgu=6000 sure=2.3 ms (bu kosumda) duz (gecikme=0.2 yineleme=0.15) | gunluk=3000 teslim=3359 sirasi bozuk varis=585 | islenen=3359 atilan=0 (kopya=0 eski=0) yan etki=3359 yanlis biten kitap=4/10 | sorgu=6718 sure=2.5 ms (bu kosumda) damgali (gecikme=0.2 yineleme=0.15) | gunluk=3000 teslim=3359 sirasi bozuk varis=585 | islenen=2415 atilan=944 (kopya=359 eski=585) yan etki=2415 yanlis biten kitap=0/10 | sorgu=8189 sure=3.4 ms (bu kosumda)
Birinci satır yalnız yinelenmedir. Üç bin olay 3452 kez teslim edildi ve tüketici 3452 yan etki üretti: 452 fazladan bildirim. Buna karşılık son durum doğru — çünkü aynı iletinin ikinci kez uygulanması görünümü değiştirmez, yalnız bildirimi tekrarlar. Yinelenmenin zararı yan etkidedir, durumda değil.
İkinci satır yalnız sıra bozulmasıdır. Teslim sayısı günlüğe eşit, yan etki sayısı doğru (3000), ama 585 ileti kendinden sonrakinin ardından vardı ve 10 kitabın 4’ü yanlış durumda bitti. Sıra bozulmasının zararı durumdadır, yan etki sayısında değil.
İki bozulma birlikte verildiğinde ikisi de görünür: 3359 yan etki ve yine 4 yanlış kitap. Anahtar defteriyle yinelenme kesilse bile bu 4 kitap yanlış kalırdı; defter aynı iletiyi ikinci kez tanır, geç kalan başka bir iletiyi tanımaz.
Sürüm Damgası
Dördüncü satır aynı akışı sürüm damgalı tüketiciye verir. Sonuç: yanlış biten kitap 0, yan etki 3359’dan 2415’e iniyor, 944 ileti atılıyor.
Atılanların ayrımı önemlidir: 944 = 359 kopya + 585 eski. Kopyalar aynı iletinin ikinci
gelişidir; atılmaları saf kazançtır. Eskiler ise gerçek olaylardır — sadece geç kalmış
oldukları için üzerlerine daha yeni bir sürüm yazılmıştır. Sürüm damgası onları da atar,
çünkü bir sürümü geriye almanın anlamı yoktur.
Bu yüzden yan etki sayısı 3000 değil 2415’tir. Görünüm doğru bitti, ama 585 gerçek durum değişikliği için bildirim hiç gönderilmedi. Bu, tek bir mekanizmayla iki bozulmayı birden karşılamanın bedelidir: sıra bozulmasını düzelten kural, geç kalan bilgiyi de siler.
Ayrım, tüketicinin ne yaptığına bağlıdır. Görünüm tablosu gibi yalnız son durumu taşıyan bir tüketici için bu doğru davranıştır; atılan ileti hiçbir şey kaybettirmez. Her geçişe tepki vermek zorunda olan bir tüketici — gecikme ücreti hesaplayan ya da her hareketi deftere yazan bir servis — için 585 kayıp gerçek bir kayıptır ve o tüketici sürüm damgasıyla değil, her iletiyi ayrı ayrı tanıyan bir kayıtla çalışmak zorundadır.
Etkisiz Tüketicinin Bedeli
Bedelin koşumdan bağımsız ölçüsü sorgu sayısıdır. Düz tüketici teslim başına iki yazma yapar:
3359 × 2 = 6718. Damgalı tüketici her teslim için önce bir okuma yapar (3359), sonra yalnız
işlediklerine iki yazma (2415 × 2 = 4830); toplam 8189. Fark +1471 sorgu, yani teslim
başına yaklaşık 0,44 ek sorgu. Süre farkı da aynı yöndedir (2,5 ms’e karşı 3,4 ms), ama asıl
sabit maliyet okumadır: her ileti, atılacak olsa bile önce sorgulanır.
Depo tarafındaki maliyet de görünürdür: kitap_gorunum tablosuna son_surum sütunu eklendi ve
her yazmada güncellendi. Sürüm damgası, tüketicinin şemasına giren kalıcı bir alandır; iletiyi
üreten servisin de her olayda artan bir sürüm yayımlamayı sözleşmesinin parçası yapması
gerekir.
Neyi ucuzlattı: sürüm damgası, yinelenmiş ve sırası bozulmuş bir akıştan doğru son durumu çıkardı — 4 yanlış kitap 0’a indi — ve fazladan bildirimleri sıfırladı. Yayıncı tarafında hiçbir garanti yükseltilmeden yapıldı; giden kutusunun “en az bir kez” teslimi olduğu gibi kaldı.
Neyi pahalılaştırdı: tüketici şemasına bir sütun, her iletiye bir okuma (+1471 sorgu)
eklendi; üstelik yayıncı artık her olayda artan bir sürüm yayımlamakla yükümlü.
Hangi yeni arıza kipi doğdu: sessiz atma. Tüketici 585 gerçek olayı hiçbir hata üretmeden yuttu. Görünüm doğru olduğu için bu kayıp durumdan anlaşılmaz; yalnız yan etki sayısı ile günlük uzunluğu karşılaştırılırsa görünür. Sürüm alanı yanlış üretilirse — örneğin iki servis aynı kayda ayrı sayaçlarla yazarsa — atılan iletiler artar ve kimse fark etmez.
Özet
- Yinelenme ile sıra bozulması farklı zararlar verir: yalnız yinelenmede yan etki 3000 yerine 3452 çıktı ama son durum doğruydu; yalnız sıra bozulmasında yan etki sayısı doğruydu ama 10 kitabın 4’ü yanlış durumda bitti.
- Bu yüzden yinelenmeyi kesen bir kayıt tek başına yetmez; geç kalan ileti farklı bir iletidir ve tanınamaz.
- Sürüm damgalı tüketici aynı akıştan doğru son durumu çıkardı (0/10 yanlış) ve 944 iletiyi attı: 359 kopya, 585 eski.
- Atılan eskiler gerçek olaylardır; yan etki 3000 yerine 2415 kaldı. Son durumu taşıyan bir tüketici için bu doğru, her geçişe tepki veren bir tüketici için kayıptır.
- Bedel teslim başına yaklaşık 0,44 ek sorgu (
6718yerine8189), tüketici şemasında bir sütun ve yayıncı sözleşmesinde artan sürüm yükümlülüğüdür.
Sonraki Adım
Bu dersin çözümü sessizce bir söz verdi: yayıncı her olayda artan bir surum alanı
yayımlayacak, tüketici o alanı okuyacak. Aynı söz önceki derslerde de verilmişti — ileti_id
kalıcı olacak, kitap alanı hep aynı anlamı taşıyacak. Bu alanlar artık iki ayrı ekibin
birlikte tuttuğu bir sözleşmedir ve sistemin doğruluğu ona bağlıdır. Servisler ayrı dağıtım
birimleri olduğu için sözleşme tek bir anda değişemez: bir süre iki sürüm birlikte koşar.
Sonraki ders sözleşmenin nasıl değişebileceğini ele alır — alan ekleme ve kaldırma sırası,
iki sürümün birlikte koşması ve tüketicinin korunması.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.