İçeriğe geç
academia.sh

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 (6718 yerine 8189), 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat