İçeriğe geç
academia.sh

Ders 12 / 25

Teslim Semantiği

Onay ile etkinin sırasının belirlediği üç kip: aynı iş yükünün en çok bir kez, en az bir kez ve tekilleştirmeli koşumunda kayıp ile yinelenen etki sayısının ölçülmesi, tekillik bekçisinin teslim sayısını değil etki sayısını bire indirmesi ve etki veritabanının dışındayken hiçbir sıranın ikisini birden önleyememesi.

İçindekiler

Önceki ders iki taşıma modelini karşılaştırdı ve ikisinde de aynı soruyu yanıtsız bıraktı: işin yapılması ile “yapıldı” bilgisinin yazılması iki ayrı adımdır. Kuyrukta onay işten önce mi sonra mı gönderilir, akışta konum işten önce mi sonra mı kaydedilir?

İki adımın sırası bir uygulama ayrıntısı değil; sistemin teslim semantiğini (delivery semantics) belirleyen karardır. Bu ders aynı iş yükünü iki sırayla ve bir üçüncü düzenle koşturur, kayıp ile yinelenen etki sayısını sayar.

Üç Adın Anlamı

En çok bir kez (at-most-once): her ileti sıfır ya da bir kez işlenir; kayıp olabilir, yineleme olamaz. Onayın etkiden önce yazılması bu kipi üretir — ileti işlenmiş sayıldıktan sonra iş yapılır, arada bir şey olursa iş hiç yapılmaz.

En az bir kez (at-least-once): her ileti bir ya da daha çok kez işlenir; yineleme olabilir, kayıp olamaz. Onayın etkiden sonra yazılması bu kipi üretir — iş yapıldıktan sonra ileti işlenmiş sayılır, arada bir şey olursa ileti yeniden teslim edilir.

Tam bir kez (exactly-once) üçüncü bir sıra değildir. Onay ile etki iki ayrı adım olduğu sürece aralarında bir aralık kalır ve bu aralıkta süreç ölebilir. Tam bir kez, bir teslim kipi değil, en az bir kezin üstüne konan tekilleştirmenin sonucudur. Ölçüm bunu iki sayıyla gösterecek.

Ölçüm düzeneği Veri Erişim Katmanı ve İş Mantığı kursundaki giden kutusu tablosunu, bir etki tablosunu ve Etkisiz İşlemler dersinde tanıtılan islenen_ileti bekçisini kurar. Etki, gecikme bildiriminin gönderilmesidir; her gönderim bildirim bağıntısına bir satır yazar.

# kur.sh — teslim olcumu icin giden kutusu, etki tablosu ve islenen ileti bekcisi
rm -f teslim.db
sqlite3 teslim.db <<'SQL'
CREATE TABLE giden_kutusu (ileti_id INTEGER PRIMARY KEY, tur TEXT NOT NULL,
                           govde TEXT NOT NULL, durum TEXT NOT NULL DEFAULT 'bekliyor');
CREATE TABLE bildirim (bildirim_id INTEGER PRIMARY KEY, ileti_id INTEGER NOT NULL,
                       uye_id INTEGER NOT NULL);
CREATE TABLE islenen_ileti (ileti_id INTEGER PRIMARY KEY, islenme TEXT NOT NULL);
SQL
echo "tablolar: $(sqlite3 teslim.db "SELECT group_concat(name, ' ') FROM sqlite_master WHERE type='table'")"
tablolar: giden_kutusu bildirim islenen_ileti

Aynı İş Yükü, Üç Kip

On iki gecikme bildirimi işlenecek. Üçünde — 3, 7 ve 11 numaralı iletilerde — ilk denemenin tam ortasında süreç ölecek. Ölüm bir özel durumla temsil edilmiştir; kesinleşmemiş iş geri alınır, süreç yeniden başlar ve hâlâ beklemede görünen iletileri yeniden dener.

// kipler.mjs — ayni is yuku uc kipte: en cok bir kez, en az bir kez, tekillestirme
import { DatabaseSync } from "node:sqlite";

const COKEN = new Set([3, 7, 11]);          // bu iletilerde ilk deneme yarida kesilir

function tazele(db) {
  for (const t of ["giden_kutusu", "bildirim", "islenen_ileti"]) db.exec(`DELETE FROM ${t}`);
  for (let n = 1; n <= 12; n++)
    db.prepare("INSERT INTO giden_kutusu (ileti_id, tur, govde) VALUES (?,?,?)")
      .run(n, "gecikme_bildirimi", JSON.stringify({ uyeId: (n % 3) + 1 }));
}

const gonder = (db, i) => db.prepare("INSERT INTO bildirim (ileti_id, uye_id) VALUES (?,?)")
  .run(i.ileti_id, JSON.parse(i.govde).uyeId);
const onayla = (db, i) => db.prepare("UPDATE giden_kutusu SET durum='islendi' WHERE ileti_id=?")
  .run(i.ileti_id);

function isle(db, i, kip, cokecek) {
  if (kip === "en cok bir kez") {
    onayla(db, i);                                   // once onay
    if (cokecek) throw new Error("cokme");
    gonder(db, i);                                   // sonra etki
  } else if (kip === "en az bir kez") {
    gonder(db, i);                                   // once etki
    if (cokecek) throw new Error("cokme");
    onayla(db, i);                                   // sonra onay
  } else {
    db.exec("BEGIN IMMEDIATE");                      // etki ve bekci ayni islemde
    if (db.prepare("SELECT 1 AS v FROM islenen_ileti WHERE ileti_id=?").get(i.ileti_id) === undefined) {
      gonder(db, i);
      db.prepare("INSERT INTO islenen_ileti (ileti_id, islenme) VALUES (?, '2025-07-20')").run(i.ileti_id);
    }
    db.exec("COMMIT");
    if (cokecek) throw new Error("cokme");
    onayla(db, i);
  }
}

function kosum(db, kip) {
  tazele(db);
  let teslim = 0;
  for (const tur of [1, 2]) {                        // 1: cokmeli gecis, 2: yeniden baslatma
    for (const i of db.prepare("SELECT ileti_id, govde FROM giden_kutusu WHERE durum='bekliyor' ORDER BY ileti_id").all()) {
      teslim += 1;
      try {
        isle(db, i, kip, tur === 1 && COKEN.has(i.ileti_id));
      } catch {
        if (db.isTransaction) db.exec("ROLLBACK");   // surec oldu: kesinlesmemis is geri alindi
      }
    }
  }
  const say = (s) => db.prepare(s).get().n;
  return {
    teslim,
    etki: say("SELECT count(*) AS n FROM bildirim"),
    kayip: 12 - say("SELECT count(DISTINCT ileti_id) AS n FROM bildirim"),
    yinelenen: say("SELECT count(*) AS n FROM bildirim") - say("SELECT count(DISTINCT ileti_id) AS n FROM bildirim"),
  };
}

const db = new DatabaseSync("teslim.db");
console.log("kip".padEnd(16) + "teslim  etki  kayip  yinelenen");
for (const kip of ["en cok bir kez", "en az bir kez", "tekillestirme"]) {
  const s = kosum(db, kip);
  console.log(kip.padEnd(16) +
    `${String(s.teslim).padStart(6)}${String(s.etki).padStart(6)}${String(s.kayip).padStart(7)}${String(s.yinelenen).padStart(11)}`);
}
node kipler.mjs
kip             teslim  etki  kayip  yinelenen
en cok bir kez      12     9      3          0
en az bir kez       15    15      0          3
tekillestirme       15    12      0          0

İlk satır en çok bir kez kipinin bedelini gösteriyor. On iki teslim yapıldı, dokuz bildirim gönderildi; üç üye bildirim almadı ve kimse bunu fark etmedi, çünkü ileti işlenmiş işaretlenmişti. Kayıp sessizdir: giden kutusunda bekleyen satır kalmadığı için sistem kendini temiz görür.

İkinci satır ters yönü gösteriyor. Hiçbir ileti kaybolmadı, ama teslim sayısı on ikiden on beşe çıktı ve on beş bildirim gönderildi — üç üye aynı bildirimi iki kez aldı. Bu kipte yinelemenin ölçüsü doğrudan kaç kez yeniden teslim yapıldığıdır.

Üçüncü satır asıl noktadır ve dikkatle okunmalıdır. Teslim sayısı hâlâ on beştir. Tekilleştirme teslimi tekilleştirmedi; kuyruk yine üç iletiyi iki kez verdi. Değişen şey etkidir: on iki bildirim gönderildi, ne eksik ne fazla. Bekçi ikinci teslimde iletinin daha önce işlendiğini gördü ve etkiyi atladı.

Tekilleştirmenin Çalışma Koşulu

Üçüncü kipin doğru sonuç vermesi bir yerleşim ayrıntısına bağlıdır: etkinin yazılması ile bekçi satırının yazılması aynı işlem sınırının içindedir. BEGIN IMMEDIATE ile COMMIT arasındaki iki yazma ya birlikte kesinleşir ya birlikte geri alınır. Ayrılsalardı iki yeni kusur doğardı: bekçi önce yazılıp etki yazılmadan süreç ölürse ileti işlenmiş sayılır ve bildirim hiç gitmez — birinci satırdaki kayıp geri gelir. Etki önce yazılıp bekçi yazılmadan ölürse ikinci teslimde etki yeniden uygulanır — ikinci satırdaki yineleme geri gelir.

İşlem Sınırları dersinde kurulan kural burada bir kez daha karşımıza çıkıyor: birlikte doğru olması gereken iki yazma aynı atomik sınırı paylaşmalıdır. Bekçinin işi, “bu iletinin etkisi uygulandı” olgusunu etkinin kendisiyle aynı anda kalıcı kılmaktır.

Bekçinin anahtarı burada iletinin kimliğiydi. İletide böyle bir kimlik yoksa ya da aynı işi farklı iletiler taşıyabiliyorsa, anahtar iş anlamından türetilir; API Tasarımı kursunun Etkisizlik Anahtarları dersinde konan ölçüt aynıdır — anahtar, tekrarın hangi ölçüte göre “aynı iş” sayılacağını tanımlar.

Etki Veritabanının Dışındaysa

Önceki ölçümde etki bir tablo satırıydı, yani bekçiyle aynı işleme sığıyordu. Gerçek gecikme bildirimi bir tabloya değil, dışarıdaki bir servise gider ve o çağrı veritabanı işleminin içine alınamaz. Aşağıdaki koşum bu durumu ölçer: dış etki bir dosyaya yazılır, onay veritabanında tutulur ve süreç tam ikisinin arasında ölür.

// dis-etki.mjs — etki veritabaninin disindaysa hangi sira neyi kaybeder
import { appendFileSync, readFileSync, writeFileSync } from "node:fs";
import { DatabaseSync } from "node:sqlite";

const GIDEN = "gonderilen.log";        // bildirim servisinin gercekten yolladigi bildirimler
const ALICI = "alici-anahtar.log";     // bildirim servisinin gordugu tekillik anahtarlari

function servisGonder(anahtar, bekciVar) {
  if (bekciVar) {
    const gorulen = new Set(readFileSync(ALICI, "utf8").split("\n").filter(Boolean));
    if (gorulen.has(anahtar)) return false;         // ayni anahtar ikinci kez islenmez
    appendFileSync(ALICI, anahtar + "\n");
  }
  appendFileSync(GIDEN, `bildirim ${anahtar}\n`);
  return true;
}

function kosum(db, sira, bekciVar) {
  writeFileSync(GIDEN, ""); writeFileSync(ALICI, "");
  db.exec("DELETE FROM giden_kutusu");
  db.prepare("INSERT INTO giden_kutusu (ileti_id, tur, govde) VALUES (1,'gecikme_bildirimi','{}')").run();
  let cagri = 0;

  for (const tur of [1, 2]) {          // 1: cokmeli gecis, 2: yeniden baslatma
    for (const i of db.prepare("SELECT ileti_id FROM giden_kutusu WHERE durum='bekliyor'").all()) {
      const anahtar = `ileti=${i.ileti_id}`;
      const onayla = () => db.prepare("UPDATE giden_kutusu SET durum='islendi' WHERE ileti_id=?").run(i.ileti_id);
      try {
        if (sira === "etki once") { cagri += 1; servisGonder(anahtar, bekciVar); }
        else onayla();
        if (tur === 1) throw new Error("cokme");    // iki adimin arasinda surec oldu
        if (sira === "etki once") onayla();
        else { cagri += 1; servisGonder(anahtar, bekciVar); }
      } catch { /* surec oldu: sonraki turda yeniden denenir */ }
    }
  }
  const etki = readFileSync(GIDEN, "utf8").split("\n").filter(Boolean).length;
  return { cagri, etki, kayip: etki === 0 ? 1 : 0, yinelenen: Math.max(0, etki - 1) };
}

const db = new DatabaseSync("teslim.db");
console.log("sira".padEnd(32) + "cagri  dis etki  kayip  yinelenen");
for (const [ad, sira, bekci] of [["etki once, onay sonra", "etki once", false],
                                 ["onay once, etki sonra", "onay once", false],
                                 ["etki once + alicida tekillik", "etki once", true]]) {
  const s = kosum(db, sira, bekci);
  console.log(ad.padEnd(32) +
    `${String(s.cagri).padStart(5)}${String(s.etki).padStart(10)}${String(s.kayip).padStart(7)}${String(s.yinelenen).padStart(11)}`);
}
node dis-etki.mjs
sira                            cagri  dis etki  kayip  yinelenen
etki once, onay sonra               2         2      0          1
onay once, etki sonra               0         0      1          0
etki once + alicida tekillik        2         1      0          0

İlk iki satır kapalı bir ikilemdir. Gönderim önce yapılırsa bildirim iki kez gider; onay önce yazılırsa bildirim hiç gitmez. Arada bir üçüncü sıra yoktur, çünkü iki farklı sistemin — veritabanı ile bildirim servisi — ortak bir işlem sınırı yoktur. Dağıtık İşlem Sorunu dersinde kurulan güçlük tam olarak budur.

Üçüncü satır çözümün nerede olduğunu gösteriyor: çağrı iki kez yapıldı, etki bir kez oldu. Fark, bekçinin gönderen tarafta değil, alan tarafta durmasıdır. Bildirim servisi tekillik anahtarını görüp ikinci çağrıyı yok saydığında, gönderen tarafın en az bir kez kipinde kalması bir kusur olmaktan çıkar.

Buradan dersin sonucu çıkar: uçtan uca tam bir kez teslim kurulamaz; kurulabilen şey tam bir kez etkidir ve bunun koşulu, tekrarın etkisiz olmasını sağlayan bir bekçinin etkiyle aynı sınırda durmasıdır. Etki dışarıdaysa o sınır alıcının içindedir.

Hangi Kip Nerede

En çok bir kez kipi bir kusur değil, bilinçli bir seçimdir; koşulu, kaybın maliyetinin işlemenin maliyetinden küçük olmasıdır. Saniyede binlerce satır üreten bir kullanım ölçütü akışında tek satırın kaybı sonucu değiştirmez ve her satır için onay yazmak taşıma maliyetinin katlanmasına yol açar.

Ödünç sisteminde ölçüt tersine döner. Gecikme bildiriminin gitmemesi üyenin ceza ödemesine yol açar, aylık rapordan düşen satır sayıyı yanlış gösterir, şube stok özetinin kaçırdığı iade kitabı raftaymış gibi gösterir. Üçü de en az bir kez ister. Yinelemenin maliyeti ise işten işe değişir: aynı stok özeti iki kez hesaplanırsa sonuç aynıdır, ama aynı bildirim iki kez gönderilirse üye iki ileti alır. Bekçi, ikinci gruptaki işler için zorunludur.

Özet

  • Onayın etkiden önce yazılması en çok bir kez, sonra yazılması en az bir kez kipini üretir; tam bir kez ayrı bir sıra değil, en az bir kezin üstüne konan tekilleştirmenin sonucudur.
  • Aynı on iki iletilik iş yükünde en çok bir kez kipi 12 teslimle 9 etki ve 3 kayıp üretti; en az bir kez kipi 15 teslimle 15 etki ve 3 yinelenen bildirim üretti.
  • Tekilleştirmeli koşumda teslim sayısı yine 15 kaldı ama etki 12’ye indi: bekçi teslimi değil, etkiyi tekilleştirir.
  • Bekçinin doğru çalışması, etkinin ve bekçi satırının aynı işlem sınırında yazılmasına bağlıdır; ayrıldıklarında kayıp ya da yineleme geri gelir.
  • Etki veritabanının dışındayken hiçbir sıra ikisini birden önlemez: gönderim önce yapılınca bildirim iki kez gitti, onay önce yazılınca hiç gitmedi; alıcı tarafta tekillik anahtarı olduğunda iki çağrı tek etkiye indi.

Sonraki Adım

Teslim sayısı ile etki sayısı ayrıldı, ama iki ölçüm de teslimin sırasına hiç bakmadı. Ödünç sisteminde sıranın önemli olduğu yerler var: aynı kitabın “verildi” ve “iade alındı” olayları ters sırayla işlenirse stok özeti kitabı hem rafta hem üyede gösterir. Önceki derslerde işi hızlandırmanın yolu tüketici eklemekti; tüketici eklemek ise sırayı bozar. Sonraki ders bu gerilimi ölçer: anahtara göre bölümleme ile sıranın nerede korunup nerede kaybolduğunu, bölüm sayısı arttıkça koşutluğun ve sıra ihlallerinin nasıl değiştiğini sayar.

İ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