---
title: 'Önbellekleme, Kuyruklar ve Eşzamansız İşleme'
source: 'https://academia.sh/tr/kurslar/eszamansiz-isleme'
language: tr
updated: '2026-08-17T04:38:45+00:00'
license: 'CC BY-SA 4.0'
lessons: 25
outcomes:
    - 'Önbellek katmanlarını ve geçersizleştirme stratejilerini seçer'
    - 'Mesaj kuyruğu ile akış arasındaki farkı ve kullanım alanlarını açıklar'
    - 'Arka plan işlerini yeniden deneme ve ölü mektup kutusuyla dayanıklı kurar'
---

# Önbellekleme, Kuyruklar ve Eşzamansız İşleme

İstek yolunun kısaltılması: önbellek katmanları ve geçersizleştirme stratejileri, mesaj kuyruklarıyla akışların teslim ve sıralama garantileri, arka plan işçileri ile geri basınç ve yük boşaltma, sunucudan istemciye gerçek zamanlı veri taşıma yolları.

## Önbellekleme

Aynı yanıtın yeniden üretilmesini önlemek: istemciden veritabanına uzanan önbellek katmanları, yanında okuma ile yazma stratejileri, süre, olay ve sürüm tabanlı geçersizleştirme, ad alanı ve kiracı ayrımı taşıyan anahtar tasarımı, sürü etkisinin tek uçuşla önlenmesi, HTTP doğrulayıcıları ve isabet oranının ölçümü.

### Önbellek Katmanları

Source: https://academia.sh/tr/kurslar/eszamansiz-isleme/onbellek-katmanlari

Aynı yanıtı defalarca üreten bir okuma yolunun ölçülmesi ve istemci, kenar, sunucu ile veritabanı önbelleklerinin zincir olarak kurulması: her katmanın kaç isteği durdurduğu, paylaşılan ile özel önbellek ayrımı ve kaynağa ulaşan istek oranının düşürülmesi.

Veri Erişim Katmanı ve İş Mantığı kursu, bir isteğin veriye inen yolunu baştan sona kurdu:
depo kalıbı, işlem sınırı, sorgu bütçesi, alan kuralları. Kursun sonunda bir varsayım
kalmıştı: her istek verinin kaynağına gidiyor. Bu varsayım her zaman tutmaz. Kütüphane
sisteminde kitap ayrıntı sayfası günde binlerce kez açılır ve kitabın başlığı ile yazarı
aylardır değişmemiştir. Aynı sorgu aynı yanıtı üretiyorsa, yanıtın saklanması sorgunun
tekrarından ucuzdur.

Bu ders önce tekrarın ölçüsünü koyar, sonra yanıtın saklanabileceği yerleri sıralar. Saklama
yerlerinin her biri bir **önbellek** (cache) katmanıdır ve katmanlar bir zincir oluşturur:
istek zincirin ne kadar erken bir halkasında durursa o kadar ucuza karşılanır.

#### Ölçülen Tekrar

Ölçümler kurs boyunca kullanılan kütüphane şeması üzerinde yapılır: şube, kitap, üye ve
ödünç bağıntıları. Aşağıdaki blok veritabanını sıfırdan kurar.

```sh
# kur.sh — kutuphane veritabanini kurar (M16/K04 semasinin ayni sutunlariyla)
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE sube (sube_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sehir TEXT NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, yazar TEXT NOT NULL,
                    basim_yili INTEGER, sube_id INTEGER REFERENCES sube(sube_id));
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, soyad TEXT NOT NULL,
                  eposta TEXT, kayit_tarihi TEXT NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY,
                    kitap_id INTEGER NOT NULL REFERENCES kitap(kitap_id),
                    uye_id INTEGER NOT NULL REFERENCES uye(uye_id),
                    alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);
INSERT INTO sube VALUES (1,'Merkez','Ankara'),(2,'Bahçelievler','Ankara'),
  (3,'Kadıköy','İstanbul'),(4,'Konak','İzmir');
INSERT INTO kitap SELECT n, 'Kitap ' || n, 'Yazar ' || (n % 40 + 1), 1950 + n % 70, n % 4 + 1
  FROM (WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n < 300) SELECT n FROM s);
INSERT INTO uye SELECT n, 'Uye' || n, 'Soyad' || n, 'uye' || n || '@ornek.test', '2024-01-01'
  FROM (WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n < 200) SELECT n FROM s);
INSERT INTO odunc SELECT n, n % 300 + 1, n % 200 + 1,
  '2025-' || substr('0' || (n % 12 + 1), -2) || '-' || substr('0' || (n % 28 + 1), -2),
  CASE WHEN n % 5 = 0 THEN NULL ELSE '2025-12-31' END
  FROM (WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n < 4000) SELECT n FROM s);
CREATE INDEX odunc_uye ON odunc(uye_id);
CREATE INDEX kitap_sube ON kitap(sube_id);
SQL
sqlite3 kutuphane.db "SELECT 'sube=' || (SELECT count(*) FROM sube) || ' kitap=' || (SELECT count(*) FROM kitap) || ' uye=' || (SELECT count(*) FROM uye) || ' odunc=' || (SELECT count(*) FROM odunc);"
```

```
sube=4 kitap=300 uye=200 odunc=4000
```

Kaynak, üç okumayı sunar: kitap ayrıntısı, şube raf sayısı ve üyenin ödünç geçmişi. Her
okuma sayılır — Başarım Sorunları konusundaki sorgu sayacının aynı yaklaşımı.

```js
// kaynak.mjs — kutuphane verisinin kaynagi; her okuma sayilir
import { DatabaseSync } from "node:sqlite";

export function kaynakAc(dosya = "kutuphane.db") {
  const db = new DatabaseSync(dosya);
  const sayac = { sorgu: 0 };
  const tek = (sql, ...d) => { sayac.sorgu += 1; return db.prepare(sql).get(...d); };
  return {
    sayac,
    kitapAyrinti: (id) => tek(
      "SELECT kitap_id, baslik, yazar, basim_yili, sube_id FROM kitap WHERE kitap_id = ?", id),
    subeStok: (subeId) => tek(
      `SELECT ? AS sube_id, count(*) AS raftaki FROM kitap k
       WHERE k.sube_id = ? AND NOT EXISTS
         (SELECT 1 FROM odunc o WHERE o.kitap_id = k.kitap_id AND o.iade_tarihi IS NULL)`,
      subeId, subeId),
    uyeGecmisi: (uyeId) => tek(
      "SELECT ? AS uye_id, count(*) AS toplam FROM odunc WHERE uye_id = ?", uyeId, uyeId),
    kapat: () => db.close(),
  };
}

// Istek akisi: kitap ayrinti istekleri birkac baslikta yogunlasir (belirlenimci uretec).
export function istekAkisi(adet) {
  let tohum = 20250729;
  const rastgele = () => ((tohum = (tohum * 1103515245 + 12345) % 2147483648) / 2147483648);
  const istekler = [];
  for (let i = 0; i < adet; i++) {
    const r = rastgele();
    if (r < 0.7) istekler.push(["kitap", Math.floor(rastgele() * 20) + 1]);
    else if (r < 0.9) istekler.push(["sube", Math.floor(rastgele() * 4) + 1]);
    else istekler.push(["uye", Math.floor(rastgele() * 50) + 1]);
  }
  return istekler;
}
```

Altı yüz isteklik akış doğrudan kaynağa uygulandığında ortaya çıkan sayı, tekrarın ölçüsüdür.

```js
// kaynaksiz.mjs — her istek kaynaga gidiyor: sorgu sayisi istek sayisina esit
import { kaynakAc, istekAkisi } from "./kaynak.mjs";

const kaynak = kaynakAc();
const istekler = istekAkisi(600);
const okuyucu = { kitap: kaynak.kitapAyrinti, sube: kaynak.subeStok, uye: kaynak.uyeGecmisi };

const t = performance.now();
for (const [tur, id] of istekler) okuyucu[tur](id);
const sure = performance.now() - t;

const tekil = new Set(istekler.map(([tur, id]) => `${tur}:${id}`)).size;
console.log(`istek=${istekler.length}  tekil_anahtar=${tekil}  kaynak_sorgusu=${kaynak.sayac.sorgu}`);
console.log(`ayni yaniti ureten tekrar sorgu = ${kaynak.sayac.sorgu - tekil}`);
console.log(`sure=${sure.toFixed(1)} ms (ortama bagli)`);
kaynak.kapat();
```

{/* mask: (?<=sure=)[0-9.]+(?= ms) */}
```
istek=600  tekil_anahtar=53  kaynak_sorgusu=600
ayni yaniti ureten tekrar sorgu = 547
sure=545.3 ms (ortama bagli)
```

Altı yüz istek yalnızca 53 farklı yanıt üretti. Kalan 547 sorgu, veritabanının daha önce
ürettiği bir sonucu yeniden üretmek için çalıştı. Süre alanı makineye bağlıdır ve her koşuda
değişir; 600 ile 53 sayıları değişmez, çünkü bunlar iş yükünün özellikleridir.

Buradaki fırsat sorgunun hızlandırılması değildir. Sorgu zaten dizinli ve planı düzgün.
Fırsat, sorgunun **çalıştırılmamasıdır**.

#### Zincirdeki Dört Katman

Yanıt, kaynak ile kullanıcı arasındaki dört noktada saklanabilir.

**İstemci önbelleği** kullanıcının kendi aygıtındadır: tarayıcının HTTP önbelleği ya da
uygulamanın yerel deposu. Ağa hiç çıkmadığı için en ucuz katmandır. Karşılığında yalnız tek
kullanıcıya hizmet eder ve içeriği sunucunun denetiminden çıkmıştır — konan kopya, konarken
verilen süre dolana kadar orada kalır.

**Kenar önbelleği** (edge cache) kullanıcıya coğrafi olarak yakın, paylaşılan bir ara
noktadır. Frontend Kalitesi kursundaki Statik Barındırma dersinde statik dosyalar için
tanıtılmıştı; aynı katman API yanıtları için de çalışır. Paylaşılan olduğu için bir
kullanıcının ısıttığı kopyayı diğerleri kullanır. Aynı nedenle kişiye özel veriyi taşıyamaz:
oraya konan bir üye geçmişi başka bir üyeye sunulabilir.

**Sunucu önbelleği** uygulamanın kendi denetimindedir. İki biçimi vardır: süreç içi bellek
ve bütün örneklerin paylaştığı ayrı bir depo. Süreç içi olan en hızlısıdır ama her örnek
kendi kopyasını tutar; paylaşılan olan bir ağ turu ister ama tek bir doğruluk noktası verir.
Bu katman uygulamanın kodundadır, dolayısıyla geçersizleştirme kararını anında uygulayabilir.

**Veritabanı önbelleği** kaynağın içindedir: sayfa arabelleği ve sorgu planı önbelleği.
Yukarıdaki ölçümde bu katman zaten çalışıyordu — 600 sorgunun tamamı sıcak sayfalar üzerinde
koştu. Sonuç yine 600 sorgudur. Veritabanı önbelleği bir sorgunun **maliyetini** düşürür,
sorgu **sayısını** düşürmez. Bağlantı, ayrıştırma, plan seçimi ve gidiş-dönüş her çağrıda
yeniden ödenir.

#### Zincirin Ölçülmesi

Dört katmanı bir zincir olarak kurup her katmanın kaç isteği durdurduğunu saymak, katman
tartışmasını sayıya çevirir. Aşağıdaki kurulum üç önbellek katmanını gerçekler; dördüncüsü
kaynağın içindedir. Her katmanın bir kapasitesi vardır: dolduğunda en eski girdi atılır.

```js
// katmanlar.mjs — dort katmanli zincir: her istek nerede duruyor?
import { kaynakAc } from "./kaynak.mjs";

// Kapasitesi dolunca en eski girdiyi atan kucuk bir onbellek.
function kucukOnbellek(kapasite) {
  const kutu = new Map();
  return {
    al: (a) => kutu.get(a),
    var: (a) => kutu.has(a),
    koy(a, d) {
      kutu.delete(a); kutu.set(a, d);
      if (kutu.size > kapasite) kutu.delete(kutu.keys().next().value);
    },
  };
}

const kaynak = kaynakAc();
const okuyucu = { kitap: kaynak.kitapAyrinti, sube: kaynak.subeStok, uye: kaynak.uyeGecmisi };
const GENEL = new Set(["kitap", "sube"]);          // her uyeye ayni donen kaynaklar

const istemciler = new Map();                       // kullanici basina ayri onbellek, 4 girdi
const kenar = kucukOnbellek(10);                    // paylasilan, yalniz genel kaynaklar
const sunucu = kucukOnbellek(200);                  // paylasilan, her sey
const sayac = { istemci: 0, kenar: 0, sunucu: 0, kaynak: 0 };

function oku(kullanici, tur, id) {
  const anahtar = `${tur}:${id}`;
  if (!istemciler.has(kullanici)) istemciler.set(kullanici, kucukOnbellek(4));
  const istemci = istemciler.get(kullanici);
  const yerlestir = (d) => {
    if (GENEL.has(tur)) kenar.koy(anahtar, d);
    istemci.koy(anahtar, d);
    return d;
  };
  if (istemci.var(anahtar)) { sayac.istemci += 1; return istemci.al(anahtar); }
  if (GENEL.has(tur) && kenar.var(anahtar)) { sayac.kenar += 1; return yerlestir(kenar.al(anahtar)); }
  const sunucuAnahtari = GENEL.has(tur) ? anahtar : `${anahtar}@${kullanici}`;
  if (sunucu.var(sunucuAnahtari)) { sayac.sunucu += 1; return yerlestir(sunucu.al(sunucuAnahtari)); }
  sayac.kaynak += 1;
  const deger = okuyucu[tur](id);
  sunucu.koy(sunucuAnahtari, deger);
  return yerlestir(deger);
}

let tohum = 20250729;
const rastgele = () => ((tohum = (tohum * 1103515245 + 12345) % 2147483648) / 2147483648);
const ISTEK = 600;
for (let i = 0; i < ISTEK; i++) {
  const kullanici = Math.floor(rastgele() * 40) + 1;
  const r = rastgele();
  const tur = r < 0.7 ? "kitap" : r < 0.9 ? "sube" : "uye";
  const id = tur === "kitap" ? Math.floor(rastgele() * 20) + 1
    : tur === "sube" ? Math.floor(rastgele() * 4) + 1 : kullanici;
  oku(kullanici, tur, id);
}

console.log(`istek=${ISTEK}`);
for (const [ad, n] of Object.entries(sayac)) {
  console.log(`${ad.padEnd(8)} ${String(n).padStart(4)}  (%${((n / ISTEK) * 100).toFixed(1)})`);
}
console.log(`kaynak sorgusu = ${kaynak.sayac.sorgu}`);
kaynak.kapat();
```

```
istek=600
istemci    98  (%16.3)
kenar     189  (%31.5)
sunucu    260  (%43.3)
kaynak     53  (%8.8)
kaynak sorgusu = 53
```

Kaynağa ulaşan istek sayısı 600'den 53'e düştü; iş yükündeki her farklı anahtar kaynağı bir
kez gördü. Zincirin geri kalanı ilginç olan kısmıdır. En yakın katman isteklerin yalnız
%16'sını durdurdu, çünkü kapasitesi dört girdiyle sınırlı ve her kullanıcının kopyası
ayrıdır. Paylaşılan kenar katmanı %31'ini durdurdu. En çok işi %43 ile sunucu katmanı yaptı:
kapasitesi en büyük, kapsamı en geniş katman odur.

Sayıların kapasiteye bağlı olması bu ölçümün asıl dersidir. Kenar katmanının kapasitesi
büyütülürse payı artar, sunucununki azalır. Katmanlar birbirinin yerine geçmez; birbirinin
artığını toplar.

#### Bir Veri Hangi Katmana Konur

Katman seçimi iki ölçütle yapılır.

Birincisi **paylaşım**tır. Bir yanıt bütün kullanıcılara aynı dönüyorsa paylaşılan katmanlara
konabilir. Kitap ayrıntısı ve şube raf sayısı böyledir. Üyenin ödünç geçmişi değildir; onu
kenar önbelleğine koymak, Kimlik Doğrulama ve Yetkilendirme kursunda üzerinde durulan
yetkilendirme sınırını delmek demektir. Ölçüm kodunda bu ayrım `GENEL` kümesiyle yapılmış ve
kişiye özel kayıtlar sunucu katmanında kullanıcı kimliğiyle anahtarlanmıştır.

İkincisi **bayatlık toleransıdır** (staleness). Önbellekteki kopya, kaynak değiştiğinde bir
süre eski kalır. Kitabın yazarı için bu süre saatler olabilir; şubedeki raf sayısı için
saniyeler. Ödünç verme işleminin kendisi için sıfırdır — bir yazma işleminin sonucu
önbellekten okunamaz.

Katmanın kullanıcıya yakınlığı ile denetlenebilirliği ters orantılıdır. Sunucu katmanındaki
bir girdi kod tarafından anında silinebilir. Kenar katmanındakini silmek ayrı bir işlem
ister. İstemcideki kopya için hiçbir yol yoktur: konarken verilen süre dolana kadar
kullanılır. Bu yüzden en yakın katmana en uzun ömür verilen veriler konur ve bu kararın
geri alınamayacağı bilinerek verilir.

#### Özet

- Altı yüz isteklik iş yükü yalnız 53 farklı yanıt üretti; 547 sorgu daha önce üretilmiş bir
  sonucu yeniden üretti. Önbelleklemenin fırsatı sorguyu hızlandırmak değil, çalıştırmamaktır.
- Yanıt dört noktada saklanabilir: istemci, kenar, sunucu ve veritabanı önbelleği. İlk üçü
  sorgu sayısını düşürür; veritabanı önbelleği yalnız her sorgunun maliyetini düşürür.
- Zincir ölçüldüğünde kaynağa ulaşan istek 600'den 53'e indi; katmanların payı kapasitelerine
  göre dağıldı. Katmanlar birbirinin yerine geçmez, artığını toplar.
- Paylaşılan katmanlara yalnız bütün kullanıcılara aynı dönen veri konur; kişiye özel veri
  paylaşılan bir önbellekte yetkilendirme sınırını deler.
- Katman kullanıcıya yaklaştıkça ucuzlar ve denetlenemez hâle gelir; istemcideki kopya,
  konarken verilen süre dolana kadar geri alınamaz.

#### Sonraki Adım

Bu derste önbellek, okuma yolunun içine elle yerleştirildi: bak, yoksa kaynaktan getir, koy.
Bu yazımın adı vardır ve tek seçenek değildir. Okuma yolunun yanında duran bir önbellek ile
bütün okumaların üzerinden geçtiği bir önbellek farklı sorumluluk dağılımları kurar; yazma
yolunda ise önbelleğin ne zaman güncelleneceği sorusu ortaya çıkar — yazma anında mı, sonra
mı. Sonraki ders bu üç stratejiyi aynı iş yükü üzerinde koşturur ve kaynağa giden okuma ve
yazma sayılarıyla tutarsızlık penceresini karşılaştırır.

### Önbellek Stratejileri

Source: https://academia.sh/tr/kurslar/eszamansiz-isleme/onbellek-stratejileri

Yanında okuma, içinden yazma ve arkadan yazma stratejilerinin aynı iş yükü üzerinde karşılaştırılması: kaynağa giden okuma ve yazma sayıları, isabet oranı, arkadan yazmanın dayanıklılık ödünü ve yanında okumanın ıska ile yazma arasındaki yarışı.

Önceki ders önbelleği okuma yolunun içine elle yerleştirdi: bak, yoksa kaynaktan getir, koy.
Bu yazımın bir adı vardır ve tek seçenek değildir. Önbellek okuma yolunun yanında durabilir
ya da bütün okumaların üzerinden geçtiği bir katman olabilir. Yazma yolunda ise ayrı bir
soru vardır: kaynak güncellendiğinde önbellekteki kopyaya ne olur — aynı anda mı güncellenir,
silinir mi, yoksa yazma önce önbelleğe mi gider?

Bu ders üç stratejiyi aynı iş yükü üzerinde koşturur: **yanında okuma** (cache-aside),
**içinden yazma** (write-through) ve **arkadan yazma** (write-behind). Ölçülen şey kaynağa
giden okuma ve yazma sayıları ile kaynağın gerçekten geride kaldığı kayıt sayısıdır.

#### Ölçüm Düzeneği

Ölçüm, raftaki kopya sayısı üzerinden yapılır: ödünç verme bu sayıyı düşürür, iade artırır.
Bu, kütüphane verisinin en sık yazılan alanıdır ve yazma stratejilerini ayırt etmeye
elverişlidir. Önceki dersteki `kitap` tablosuna bu sayıyı tutan bir sütun eklenir.

```sh
# kur.sh — dersin veritabani: kitap tablosuna raftaki kopya sayisi eklenir
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE sube (sube_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sehir TEXT NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, yazar TEXT NOT NULL,
                    sube_id INTEGER REFERENCES sube(sube_id), rafta INTEGER NOT NULL);
INSERT INTO sube VALUES (1,'Merkez','Ankara'),(2,'Bahçelievler','Ankara'),(3,'Kadıköy','İstanbul');
INSERT INTO kitap SELECT n, 'Kitap ' || n, 'Yazar ' || (n % 10 + 1), n % 3 + 1, 3
  FROM (WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n < 60) SELECT n FROM s);
SQL
sqlite3 kutuphane.db "SELECT 'kitap=' || count(*) || ' toplam_rafta=' || sum(rafta) FROM kitap;"
```

```
kitap=60 toplam_rafta=180
```

İş yükü 1200 işlemdir: %85 okuma, %15 yazma, on iki popüler kitap üzerinde yoğunlaşmış.
Her strateji kendi veritabanı kopyasında koşar, böylece karşılaştırma aynı başlangıç
durumundan yapılır.

#### Üç Strateji

Üç kurulum aynı arayüzü verir: `oku` ve `yaz`. Aralarındaki fark yalnız bu iki işlevin
gövdesindedir.

```js
// stratejiler.mjs — ayni is yuku uc stratejiyle: kaynak erisimi ve tutarsizlik olculur
import { DatabaseSync } from "node:sqlite";
import { copyFileSync, rmSync } from "node:fs";

// Ayni is yuku: %85 okuma, %15 yazma; istekler ilk 12 kitapta yogunlasir.
function isYuku(adet) {
  let tohum = 20250729;
  const rast = () => ((tohum = (tohum * 1103515245 + 12345) % 2147483648) / 2147483648);
  return Array.from({ length: adet }, () => {
    const yaz = rast() < 0.15;
    return [yaz ? "yaz" : "oku", Math.floor(rast() * 12) + 1, yaz ? (rast() < 0.5 ? -1 : 1) : 0];
  });
}

function kaynakAc(dosya) {
  rmSync(dosya, { force: true });
  copyFileSync("kutuphane.db", dosya);
  const db = new DatabaseSync(dosya);
  const s = { okuma: 0, yazma: 0 };
  return {
    s,
    oku: (id) => { s.okuma += 1; return db.prepare("SELECT rafta FROM kitap WHERE kitap_id = ?").get(id).rafta; },
    yaz: (id, d) => { s.yazma += 1; db.prepare("UPDATE kitap SET rafta = ? WHERE kitap_id = ?").run(d, id); },
    denetim: (id) => db.prepare("SELECT rafta FROM kitap WHERE kitap_id = ?").get(id).rafta,
    kapat: () => db.close(),
  };
}

function kosu(ad, kur) {
  const kaynak = kaynakAc(`${ad}.db`);
  const onbellek = new Map();
  const gercek = new Map();                       // is yukunun mantiksal dogrusu
  const o = { isabet: 0, iska: 0, bayat: 0 };
  const { oku, yaz, bitir } = kur(kaynak, onbellek, o);
  for (const [tur, id, delta] of isYuku(1200)) {
    if (tur === "oku") {
      const d = oku(id);
      if (!gercek.has(id)) gercek.set(id, kaynak.denetim(id));
      if (d !== gercek.get(id)) o.bayat += 1;
    } else {
      if (!gercek.has(id)) gercek.set(id, kaynak.denetim(id));
      const yeni = Math.max(0, gercek.get(id) + delta);
      gercek.set(id, yeni);
      yaz(id, yeni);
    }
  }
  const bekleyen = bitir ? bitir() : 0;
  const sapan = [...gercek.keys()].filter((id) => kaynak.denetim(id) !== gercek.get(id)).length;
  kaynak.kapat();
  rmSync(`${ad}.db`, { force: true });
  return { ad, ...kaynak.s, ...o, bekleyen, sapan };
}

const yanindaOkuma = (k, c, o) => ({
  oku(id) {
    if (c.has(id)) { o.isabet += 1; return c.get(id); }
    o.iska += 1; const d = k.oku(id); c.set(id, d); return d;
  },
  yaz(id, d) { k.yaz(id, d); c.delete(id); },     // yazma sonrasi girdi silinir
});

const icindenYazma = (k, c, o) => ({
  oku(id) {
    if (c.has(id)) { o.isabet += 1; return c.get(id); }
    o.iska += 1; const d = k.oku(id); c.set(id, d); return d;
  },
  yaz(id, d) { k.yaz(id, d); c.set(id, d); },     // kaynak ve onbellek birlikte guncellenir
});

const arkadanYazma = (k, c, o) => {
  const bekleyen = new Map();
  let sayim = 0;
  return {
    oku(id) {
      if (c.has(id)) { o.isabet += 1; return c.get(id); }
      o.iska += 1; const d = k.oku(id); c.set(id, d); return d;
    },
    yaz(id, d) {                                  // yazma once onbellege, kaynak sonra
      c.set(id, d); bekleyen.set(id, d);
      if ((sayim += 1) % 20 === 0) { for (const [i, v] of bekleyen) k.yaz(i, v); bekleyen.clear(); }
    },
    bitir: () => bekleyen.size,                   // surec burada dursaydi kaybolacak yazma sayisi
  };
};

const basliklar = ["strateji", "kay.okuma", "kay.yazma", "isabet", "iska", "bayat", "bekleyen", "sapan"];
console.log(basliklar.map((b, i) => (i === 0 ? b.padEnd(16) : b.padStart(10))).join(""));
for (const [ad, kur] of [["yaninda okuma", yanindaOkuma], ["icinden yazma", icindenYazma], ["arkadan yazma", arkadanYazma]]) {
  const r = kosu(ad.replace(/ /g, "-"), kur);
  console.log(ad.padEnd(16) + [r.okuma, r.yazma, r.isabet, r.iska, r.bayat, r.bekleyen, r.sapan]
    .map((n) => String(n).padStart(10)).join(""));
}
```

```
strateji         kay.okuma kay.yazma    isabet      iska     bayat  bekleyen     sapan
yaninda okuma          156       175       869       156         0         0         0
icinden yazma            7       175      1018         7         0         0         0
arkadan yazma            7        74      1018         7         0        10         5
```

#### Sayıların Söyledikleri

**Yanında okuma** 156 kaynak okuması yaptı. On iki farklı anahtar var; kalan 144 ıska, her
yazmadan sonra girdinin silinmesinden doğdu. Silinen her girdi bir sonraki okumada yeniden
yüklenir. Bu, stratejinin bedelidir: yazma sıklığı arttıkça ıska sayısı da artar.

**İçinden yazma** 7 kaynak okuması yaptı — yalnız ilk kez görülen her anahtar için bir tane.
Yazma önbellekteki girdiyi silmek yerine yeni değerle güncellediği için girdi hiç düşmedi.
İsabet sayısı 869'dan 1018'e çıktı. Karşılığında önbellek, hiç okunmayacak değerleri de
taşımaya başlar: nadiren okunan bir kitabın raf sayısı her yazmada önbelleğe girer ve yer
kaplar.

**Arkadan yazma** kaynak yazma sayısını 175'ten 74'e düşürdü. Yazmalar önce önbelleğe gitti,
her yirmi yazmada bir toplu olarak kaynağa aktarıldı ve aynı kitaba yapılan ardışık yazmalar
tek bir güncellemede birleşti. Bu, kaynak üzerindeki yazma yükünü yarıdan aşağı indiren tek
stratejidir.

Son iki sütun bu kazancın bedelini gösteriyor. İş yükü bittiğinde 10 yazma hâlâ önbellekteydi
ve kaynaktaki 5 kayıt gerçek değerden sapmış durumdaydı. Süreç o anda dursaydı bu yazmalar
kaybolurdu — önbellek belleğe dayanır, kaynak diske. Arkadan yazma, yazma yükünü düşürmek
için **dayanıklılığı** öder. Kütüphane örneğinde raf sayısının bir kopyası kaybolabilir; aynı
strateji ödünç kaydının kendisi için kullanılamaz.

`bayat` sütununun her satırda sıfır olması dikkat çekicidir. Üç strateji de okuyucuya yanlış
değer vermedi, çünkü hepsi yazmayı önbelleğe yansıttı. Tutarsızlık okuyucuda değil, kaynak
ile önbellek arasında oluştu ve `sapan` sütununda göründü.

#### Yanında Okumanın Yarışı

Yukarıdaki koşuda işlemler sırayla çalıştı. Gerçek bir sunucuda iki istek aynı anda ilerler
ve yanında okumanın bilinen bir açığı ortaya çıkar: bir ıska yüklemesi ile bir yazma
çakışabilir. Aşağıdaki blok bu sıralamayı adım adım kurar.

```js
// yaninda-yarisma.mjs — yaninda okumanin klasik yarisi: iska yuklemesi ile yazma cakisir
import { DatabaseSync } from "node:sqlite";
import { copyFileSync, rmSync } from "node:fs";

rmSync("yaris.db", { force: true });
copyFileSync("kutuphane.db", "yaris.db");
const db = new DatabaseSync("yaris.db");
const kaynakOku = (id) => db.prepare("SELECT rafta FROM kitap WHERE kitap_id = ?").get(id).rafta;
const kaynakYaz = (id, d) => db.prepare("UPDATE kitap SET rafta = ? WHERE kitap_id = ?").run(d, id);
const onbellek = new Map();
const yaz = (adim, ne) => console.log(`${String(adim).padStart(2)}. ${ne.padEnd(46)} ` +
  `onbellek=${onbellek.has(7) ? onbellek.get(7) : "-"} kaynak=${kaynakOku(7)}`);

// A istegi: okuma yapiyor, iska aldi ve kaynagi okudu; henuz onbellege koymadi.
const aOkudu = kaynakOku(7);
yaz(1, "A: iska, kaynaktan okudu (deger elinde)");

// B istegi arada tamamlaniyor: bir kopya odunc verildi.
kaynakYaz(7, aOkudu - 1);
onbellek.delete(7);
yaz(2, "B: kaynagi guncelledi, onbellek girdisini sildi");

// A istegi simdi elindeki eski degeri onbellege koyuyor.
onbellek.set(7, aOkudu);
yaz(3, "A: elindeki eski degeri onbellege koydu");

let bayat = 0;
for (let i = 0; i < 50; i++) {
  const d = onbellek.has(7) ? onbellek.get(7) : kaynakOku(7);
  if (d !== kaynakOku(7)) bayat += 1;
}
yaz(4, `sonraki 50 okumadan bayat olan: ${bayat}`);
db.close();
rmSync("yaris.db", { force: true });
```

```
 1. A: iska, kaynaktan okudu (deger elinde)        onbellek=- kaynak=3
 2. B: kaynagi guncelledi, onbellek girdisini sildi onbellek=- kaynak=2
 3. A: elindeki eski degeri onbellege koydu        onbellek=3 kaynak=2
 4. sonraki 50 okumadan bayat olan: 50             onbellek=3 kaynak=2
```

Yazma kuralına harfiyen uyuldu — kaynak güncellendi, girdi silindi — ve önbellek yine de
bayat kaldı. Kusur sıradadır: silme, konmamış bir girdiyi silemez. Bayatlık kendiliğinden
düzelmez; girdi bir daha yazılana ya da süresi dolana kadar yanlış değer sunulur.

Bu, yanında okumanın hiçbir zaman tek başına yeterli olmadığının kanıtıdır. Yanına en az bir
zaman sınırı konur; sıralamayı kesin olarak çözmek gerektiğinde girdiye bir sürüm damgası
eklenir ya da aynı anahtarın yüklenmesi tek bir çağrıya kısılır. Üçü de sonraki derslerin
konusudur.

#### Seçim Ölçütü

Üç strateji birbirinin yerine geçen seçenekler değildir; farklı okuma–yazma oranlarına
uyarlar.

Okuma ağırlıklı ve seyrek değişen veride **yanında okuma** yeterlidir; kodun yalnız okuma
yolunu bilmesi gerekir ve önbellek düştüğünde uygulama çalışmayı sürdürür. Aynı anahtarın
hem sık okunduğu hem sık yazıldığı yerde **içinden yazma** ıskaları önler, karşılığında
önbelleği okunmayan değerlerle doldurur. Yazma hızının kaynağın dayanabileceğinin üstüne
çıktığı ve tek tek yazmaların kaybının katlanılabilir olduğu yerde **arkadan yazma** yükü
düşürür; kayıp katlanılamazsa bu strateji elenir.

Yazmanın önbelleğe hiç uğramadığı bir dördüncü seçenek de vardır: **yanından yazma**
(write-around). Kaynak güncellenir, önbellek girdisi ne silinir ne yazılır; girdi süresi
dolana kadar bayat kalır. Bir kerelik yazılıp uzun süre okunmayan verilerde önbelleği
kirletmemek için kullanılır, ama bayatlık penceresi açıkça kabul edilmiş olur.

#### Özet

- Yanında okuma yazmada girdiyi siler; 1200 işlemlik iş yükünde 156 kaynak okuması üretti,
  bunun 144'ü silinen girdilerin yeniden yüklenmesidir.
- İçinden yazma girdiyi silmek yerine güncellediği için kaynak okumasını 7'ye indirdi ve
  isabeti 1018'e çıkardı; karşılığında okunmayan değerleri de önbellekte tutar.
- Arkadan yazma kaynak yazmasını 175'ten 74'e düşürdü, çünkü aynı anahtara ardışık yazmalar
  toplu aktarımda birleşti; bedeli, süreç durduğunda kaybolacak 10 bekleyen yazmadır.
- Üç stratejide de okuyucu bayat değer görmedi; tutarsızlık kaynak ile önbellek arasında
  oluştu. Dayanıklılık gerektiren veride arkadan yazma kullanılamaz.
- Yanında okumada ıska yüklemesi ile yazma çakıştığında silme işe yaramaz ve bayat girdi
  kendiliğinden düzelmez; strateji tek başına yeterli değildir.

#### Sonraki Adım

Son ölçüm bir soruyu açıkta bıraktı: önbellekteki bayat girdi ne zaman gider? Bu dersteki
stratejilerin hepsi yazma anını yakaladıklarını varsaydı. Yazma başka bir süreçten geldiğinde,
veri doğrudan veritabanında değiştirildiğinde ya da yukarıdaki gibi bir sıralama kusuru
oluştuğunda o varsayım düşer. Girdinin ne kadar yaşayacağı, hangi olayın onu düşüreceği ve
anahtarın kendisinin değişmesiyle eski girdinin nasıl erişilemez kılınacağı ayrı bir karar
kümesidir. Sonraki ders süre, olay ve sürüm tabanlı geçersizleştirmeyi aynı iş yükünde
karşılaştırır ve her birinin ürettiği bayat okuma sayısını ölçer.

### Geçersizleştirme

Source: https://academia.sh/tr/kurslar/eszamansiz-isleme/gecersizlestirme

Süre, olay ve sürüm tabanlı geçersizleştirmenin aynı iş yükünde ölçülmesi: yaşam süresinin kaynak okuması ile bayat okuma arasında kurduğu ödünleşim, giden kutusundan beslenen olay tüketicisinin gecikmesi ve sürümü anahtara sokmanın erişilemez girdi bedeli.

Önceki dersin son ölçümü bayat bir girdi bıraktı: yazma kuralına uyulduğu hâlde önbellekte
eski değer kaldı ve elli okuma boyunca öyle kaldı. O girdiyi düşürecek bir mekanizma
olmadıkça sorun kendiliğinden çözülmez.

Önbellekteki bir girdinin düşürülmesine **geçersizleştirme** (invalidation) denir ve üç ayrı
tetikleyicisi vardır. Girdi belirli bir süre sonra kendiliğinden düşebilir, bir olay onu
düşürebilir ya da anahtarın kendisi değişerek girdiyi erişilemez kılabilir. Bu ders üçünü
aynı iş yükünde koşturur ve her birinin ürettiği bayat okuma sayısını ölçer.

#### Ölçüm Düzeneği

Şema önceki dersinkidir; üzerine İş Mantığı Yerleşimi konusunda kurulan `giden_kutusu`
tablosu eklenir. Yazma işlemi hem raf sayısını günceller hem de aynı işlemde olay kaydını
bu tabloya düşürür.

```sh
# kur.sh — onceki dersin semasi, uzerine M16/K04'teki giden kutusu tablosu
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL,
                    sube_id INTEGER NOT NULL, rafta INTEGER NOT NULL);
CREATE TABLE giden_kutusu (ileti_id INTEGER PRIMARY KEY, govde TEXT NOT NULL,
                           durum TEXT NOT NULL DEFAULT 'bekliyor');
INSERT INTO kitap SELECT n, 'Kitap ' || n, n % 3 + 1, 3
  FROM (WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n < 60) SELECT n FROM s);
SQL
sqlite3 kutuphane.db "SELECT 'kitap=' || count(*) || ' toplam_rafta=' || sum(rafta) FROM kitap;"
```

```
kitap=60 toplam_rafta=180
```

İş yükü yine 1200 işlemdir. Saat olarak gerçek zaman değil **iş yükü adımı** kullanılır:
her işlem saati bir artırır. Böylece yaşam süresi karşılaştırması makineden bağımsız aynı
sayıları üretir; süre birimi milisaniye değil adımdır.

#### Üç Yol

Üç strateji aynı üç soruya farklı yanıt verir: anahtar nedir, girdi ne kadar yaşar, yazma
olduğunda ne yapılır.

```js
// gecersizlestirme.mjs — sure, olay ve surum tabanli gecersizlestirme ayni is yukunde
import { DatabaseSync } from "node:sqlite";
import { copyFileSync, rmSync } from "node:fs";

function isYuku(adet) {                            // %85 okuma, %15 yazma, 12 populer kitap
  let tohum = 20250729;
  const rast = () => ((tohum = (tohum * 1103515245 + 12345) % 2147483648) / 2147483648);
  return Array.from({ length: adet }, () => {
    const yaz = rast() < 0.15;
    return [yaz ? "yaz" : "oku", Math.floor(rast() * 12) + 1, yaz ? (rast() < 0.5 ? -1 : 1) : 0];
  });
}

function kaynakAc(dosya) {
  rmSync(dosya, { force: true });
  copyFileSync("kutuphane.db", dosya);
  const db = new DatabaseSync(dosya);
  const s = { okuma: 0 };
  return {
    s, db,
    oku: (id) => { s.okuma += 1; return db.prepare("SELECT rafta FROM kitap WHERE kitap_id = ?").get(id).rafta; },
    dogru: (id) => db.prepare("SELECT rafta FROM kitap WHERE kitap_id = ?").get(id).rafta,
    yaz(id, delta) {                               // yazma ve olay ayni islemde (giden kutusu)
      db.exec("BEGIN");
      const yeni = Math.max(0, this.dogru(id) + delta);
      db.prepare("UPDATE kitap SET rafta = ? WHERE kitap_id = ?").run(yeni, id);
      db.prepare("INSERT INTO giden_kutusu (govde) VALUES (?)").run(JSON.stringify({ kitapId: id }));
      db.exec("COMMIT");
    },
    kapat: () => db.close(),
  };
}

function kosu(etiket, kurulum) {
  const kaynak = kaynakAc("gecersiz.db");
  const kutu = new Map();                          // anahtar -> { deger, sonGecerlilik }
  const o = { isabet: 0, iska: 0, bayat: 0 };
  let adim = 0;                                    // saat: gercek zaman degil, is yuku adimi
  const strateji = kurulum(kaynak, kutu, () => adim);
  for (const [tur, id, delta] of isYuku(1200)) {
    adim += 1;
    if (tur === "oku") {
      const anahtar = strateji.anahtar(id);
      const girdi = kutu.get(anahtar);
      let deger;
      if (girdi && girdi.sonGecerlilik > adim) { o.isabet += 1; deger = girdi.deger; }
      else { o.iska += 1; deger = kaynak.oku(id); kutu.set(anahtar, { deger, sonGecerlilik: strateji.omur(adim) }); }
      if (deger !== kaynak.dogru(id)) o.bayat += 1;
    } else {
      kaynak.yaz(id, delta);
      strateji.yazildi(id);
    }
    strateji.dongu?.();
  }
  kaynak.kapat();
  rmSync("gecersiz.db", { force: true });
  return `${etiket.padEnd(22)}${[kaynak.s.okuma, o.isabet, o.iska, o.bayat].map((n) => String(n).padStart(10)).join("")}`;
}

// 1) Sure tabanli: yazma onbellege dokunmaz, girdi omru dolunca duser.
const sureTabanli = (omur) => () => ({
  anahtar: (id) => `kitap:${id}`,
  omur: (adim) => adim + omur,
  yazildi: () => {},
});

// 2) Olay tabanli: yazma giden kutusuna dusen olayi uretir, tuketici anahtari siler.
const olayTabanli = (gecikme) => (kaynak, kutu) => {
  let sayim = 0;
  return {
    anahtar: (id) => `kitap:${id}`,
    omur: () => Infinity,
    yazildi: () => {},
    dongu() {                                      // tuketici her `gecikme` adimda bir calisir
      if ((sayim += 1) % gecikme !== 0) return;
      const iletiler = kaynak.db.prepare(
        "SELECT ileti_id, govde FROM giden_kutusu WHERE durum = 'bekliyor'").all();
      for (const i of iletiler) {
        kutu.delete(`kitap:${JSON.parse(i.govde).kitapId}`);
        kaynak.db.prepare("UPDATE giden_kutusu SET durum = 'islendi' WHERE ileti_id = ?").run(i.ileti_id);
      }
    },
  };
};

// 3) Surum tabanli: anahtarin icinde surum var; yazma surumu artirir, eski anahtar erisilemez olur.
const surumTabanli = () => {
  const surum = new Map();
  return {
    anahtar: (id) => `kitap:s${surum.get(id) ?? 1}:${id}`,
    omur: () => Infinity,
    yazildi: (id) => surum.set(id, (surum.get(id) ?? 1) + 1),
  };
};

console.log(["gecersizlestirme", "kay.okuma", "isabet", "iska", "bayat"]
  .map((b, i) => (i === 0 ? b.padEnd(22) : b.padStart(10))).join(""));
console.log(kosu("sure (omur=10)", sureTabanli(10)));
console.log(kosu("sure (omur=50)", sureTabanli(50)));
console.log(kosu("sure (omur=200)", sureTabanli(200)));
console.log(kosu("olay (gecikme=5)", olayTabanli(5)));
console.log(kosu("olay (gecikme=25)", olayTabanli(25)));
console.log(kosu("surum", surumTabanli));
```

```
gecersizlestirme       kay.okuma    isabet      iska     bayat
sure (omur=10)               626       399       626        18
sure (omur=50)               232       793       232       147
sure (omur=200)               72       953        72       445
olay (gecikme=5)             155       870       155        14
olay (gecikme=25)            153       872       153        97
surum                        156       869       156         0
```

#### Sayıların Söyledikleri

**Süre tabanlı** satırlar tek bir eğri çiziyor. Yaşam süresi ondan iki yüze çıktığında kaynak
okuması 626'dan 72'ye düşüyor, bayat okuma 18'den 445'e çıkıyor. Bu iki sayı aynı düğmenin
iki ucudur: yaşam süresi, kaynağa binen yük ile kabul edilen bayatlık arasındaki dönüşüm
oranıdır. Sıfıra yaklaştıkça önbellek anlamsızlaşır, büyüdükçe önbellek kendi gerçekliğini
kurar.

**Olay tabanlı** satırlar bu eğriden çıkıyor. Beş adımlık tüketici gecikmesiyle kaynak okuması
155, bayat okuma 14. Aynı bayatlık düzeyi için süre tabanlı yol dört kat fazla kaynak okuması
istiyordu. Fark, geçersizleştirmenin **nedene** bağlanmasından gelir: girdi zaman geçtiği için
değil, veri değiştiği için düşer. Değişmeyen anahtar sonsuza kadar önbellekte kalır.

Olay tabanlı yolun zayıflığı ikinci satırda görünüyor. Tüketici gecikmesi beşten yirmi beşe
çıktığında bayat okuma 14'ten 97'ye yükseldi. Bayatlık penceresi artık olayın üretilmesiyle
işlenmesi arasındaki süredir. Bu, bir teslim sorunudur: olay üretildi ve kaydedildi, ama
henüz kimse okumadı.

**Sürüm tabanlı** satırda bayat okuma sıfırdır. Yazma anahtarı değiştirdiği için eski girdi
aranmaz bile; yeni anahtar hiçbir zaman önbellekte bulunmaz ve ilk okuma kaynağa gider.
Kaynak okuması 156 ile olay tabanlı yolla hemen hemen aynıdır, bayatlık ise tümüyle
kalkmıştır.

#### Sürüm Anahtara Girdiğinde

Sürüm tabanlı geçersizleştirmenin iki sonucu vardır: silme işlemi hiç yapılmaz ve eski
girdiler önbellekte kalmayı sürdürür. Aşağıdaki blok her ikisini de gösterir. Ayrıca sürümün
tek bir kayıt yerine bir **ad alanının** tamamına konabileceğini gösterir; bu biçimine
**nesil** (generation) denir.

```js
// surum-anahtari.mjs — surum anahtara girince eski girdiler erisilemez olur, ama silinmez
const kutu = new Map();
const surum = new Map();                          // kitap basina surum
let subeSurumu = 1;                               // sube duzeyinde ortak surum (nesil)

const kitapAnahtari = (id) => `kitap:s${surum.get(id) ?? 1}:${id}`;
const listeAnahtari = (subeId, sayfa) => `sube:${subeId}:n${subeSurumu}:liste:${sayfa}`;

kutu.set(kitapAnahtari(7), { rafta: 3 });
kutu.set(listeAnahtari(2, 1), ["Kitap 5", "Kitap 8"]);
kutu.set(listeAnahtari(2, 2), ["Kitap 11"]);
console.log("baslangic anahtarlari:", [...kutu.keys()].join("  "));

surum.set(7, 2);                                  // kitap 7 guncellendi
subeSurumu += 1;                                  // sube listeleri toptan gecersiz
console.log("kitap 7 icin aranan anahtar:", kitapAnahtari(7),
  "-> onbellekte:", kutu.has(kitapAnahtari(7)));
console.log("sube 2 sayfa 1 icin aranan  :", listeAnahtari(2, 1),
  "-> onbellekte:", kutu.has(listeAnahtari(2, 1)));

kutu.set(kitapAnahtari(7), { rafta: 2 });
const canli = new Set([kitapAnahtari(7), listeAnahtari(2, 1), listeAnahtari(2, 2)]);
const olu = [...kutu.keys()].filter((a) => !canli.has(a));
console.log(`girdi=${kutu.size}  erisilemez girdi=${olu.length}  ->`, olu.join("  "));
```

```
baslangic anahtarlari: kitap:s1:7  sube:2:n1:liste:1  sube:2:n1:liste:2
kitap 7 icin aranan anahtar: kitap:s2:7 -> onbellekte: false
sube 2 sayfa 1 icin aranan  : sube:2:n2:liste:1 -> onbellekte: false
girdi=4  erisilemez girdi=3  -> kitap:s1:7  sube:2:n1:liste:1  sube:2:n1:liste:2
```

Nesil sayacının değeri şudur: bir şubenin liste sayfaları kaç tane olursa olsun, tek bir
sayının artırılmasıyla hepsi geçersizleşir. Anahtarların tek tek bulunup silinmesi gerekmez —
paylaşılan bir önbellekte anahtar deseniyle arama pahalı, çoğu zaman da tehlikelidir.

Bedeli son satırdadır. Dört girdinin üçü artık hiçbir okumanın aramayacağı ölü girdidir. Bu
girdiler yer kaplar ve ancak yaşam süresiyle ya da tahliye politikasıyla temizlenir. Sürüm
tabanlı geçersizleştirme bu yüzden her zaman bir üst sınır süresiyle birlikte kurulur.

İkinci koşul, sürüm sayacının **paylaşılan** olmasıdır. Yukarıdaki örnekte sayaç süreç
belleğindedir; iki uygulama örneği çalıştığında biri sürümü artırdığında diğeri bunu görmez
ve eski anahtarı okumayı sürdürür. Sayacın kendisi paylaşılan depoda tutulmalı, dolayısıyla
her okumada bir ek erişim ödenmelidir.

#### Hangi Yol Nerede

Üç yol birbirini dışlamaz; katman katman kurulur.

**Süre tabanlı** yol her zaman vardır, çünkü diğerlerinin başarısız olduğu durumda tek
güvencedir. Bir olay kaybolduğunda ya da bir uygulama örneği sürüm artışını kaçırdığında,
bayatlığı sınırlayan tek şey yaşam süresidir. İstemci ve kenar önbelleklerinde ise tek
seçenektir: o kopyalara ulaşıp silmenin yolu yoktur.

**Olay tabanlı** yol veri değiştiğinde tepki verir ve bayatlığı yaşam süresinden bağımsız
kılar. Doğruluğu, olayın gerçekten teslim edilmesine bağlıdır. Ölçümdeki tüketici gecikmesi
bunun en iyimser hâliydi: olay kaydedilmişti ve yalnız geç okunuyordu. Kaybolan bir olay
sonsuza kadar bayat bir girdi bırakır.

**Sürüm tabanlı** yol geçersizleştirmeyi okuma yoluna taşır: yanlış girdiyi silmek yerine
doğru girdiyi başka bir adla arar. Yarış koşullarına en dayanıklı yoldur, çünkü silme
işleminin sırasına bağlı değildir. Bedeli, ölü girdilerin biriktirdiği yerdir.

#### Özet

- Yaşam süresi, kaynak yükü ile kabul edilen bayatlık arasındaki dönüşüm oranıdır: ömür
  10'dan 200'e çıktığında kaynak okuması 626'dan 72'ye düştü, bayat okuma 18'den 445'e çıktı.
- Olay tabanlı geçersizleştirme bu eğriden çıkar: beş adımlık tüketici gecikmesiyle 155
  kaynak okuması ve 14 bayat okuma, yani aynı bayatlık için dörtte bir kaynak yükü.
- Olay tabanlı yolun bayatlık penceresi, olayın üretilmesi ile işlenmesi arasındaki süredir;
  gecikme beşten yirmi beşe çıktığında bayat okuma 14'ten 97'ye yükseldi.
- Sürüm anahtara girdiğinde bayat okuma sıfıra iner ve silme işlemi hiç yapılmaz; nesil
  sayacıyla bir ad alanının tamamı tek artışla geçersizleşir.
- Sürüm tabanlı yolun bedeli erişilemez girdilerdir; bu yüzden her zaman bir üst sınır
  süresiyle kurulur ve sayacın paylaşılan olması gerekir.

#### Sonraki Adım

Bu derste anahtar üç kez değişti: önce `kitap:7`, sonra sürümü taşıyan bir biçim, sonra bir
şubenin nesil sayacını taşıyan bir liste anahtarı. Her seferinde anahtarın içine bir bilgi
daha kondu ve her seferinde bu iş elle yapıldı. Anahtar yapısı elle kurulduğunda iki hata
kaçınılmazdır: iki farklı sorgunun aynı anahtara düşmesi ve bir kiracının verisinin başka bir
kiracıya sunulması. Sonraki ders anahtarı bir tasarım nesnesi olarak ele alır; ad alanı,
kiracı ayrımı ve ölçütlerin kurallı sıralanması üzerine kurulu bir üreteç yazar ve iki hatayı
da koşturarak gösterir.

### Önbellek Anahtarı Tasarımı

Source: https://academia.sh/tr/kurslar/eszamansiz-isleme/onbellek-anahtari-tasarimi

Elle birleştirilmiş anahtarın iki kusurunun koşturularak gösterilmesi — çakışma ve kiracı sızıntısı — ve ad alanı, biçim sürümü, nesil, kiracı ile kurallı ölçütlerden anahtar üreten bir işlevin yazılması; anahtar kardinalitesinin isabet oranına ve doğruluğa etkisinin ölçümü.

Önceki derste anahtar üç ayrı biçim aldı: önce `kitap:7`, sonra sürüm taşıyan bir biçim,
sonra şubenin nesil sayacını taşıyan bir liste anahtarı. Her seferinde anahtar dizgi
birleştirmesiyle elle kuruldu.

Anahtar, önbelleğin tek adresleme mekanizmasıdır. Yanlış kurulduğunda önbellek sessizce
yanlış yanıt döndürür — hata vermez, yavaşlamaz, yalnız başka birinin verisini sunar. Bu ders
elle birleştirmenin iki kusurunu koşturarak gösterir, ardından anahtarı bir tasarım nesnesi
olarak ele alan bir üreteç yazar.

#### İki Kusur

Ölçümler yedi kitaplık bir katalog üzerinde yapılır. Kitaplardan biri, başlığında iki nokta
üst üste taşır; bu, birinci kusuru ortaya çıkarmak için gerekli.

```sh
# kur.sh — arama ve liste sorgulari icin kitap verisi (M16/K04 tohumu)
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE sube (sube_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sehir TEXT NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, yazar TEXT NOT NULL,
                    basim_yili INTEGER, sube_id INTEGER REFERENCES sube(sube_id));
INSERT INTO sube VALUES (1,'Merkez','Ankara'),(2,'Bahçelievler','Ankara'),(3,'Kadıköy','İstanbul');
INSERT INTO kitap VALUES
  (1,'Körlük','José Saramago',1995,1),(2,'Tutunamayanlar','Oğuz Atay',1972,1),
  (3,'Kum Kitabı','Jorge Luis Borges',1975,2),(4,'Yaban','Yakup Kadri',1932,2),
  (5,'Sessiz Ev','Orhan Pamuk',1983,3),(6,'Anayurt Oteli','Yusuf Atılgan',1973,3),
  (7,'Kum: Borges Seçkisi','Derleme',2010,1);
SQL
sqlite3 kutuphane.db "SELECT sube_id, count(*) FROM kitap GROUP BY sube_id;"
```

```
1|3
2|2
3|2
```

Aşağıdaki blok iki senaryo koşturur. Birincisinde arama ölçütleri iki nokta üst üste ile
birleştirilir; ikincisinde şube listesi anahtarı yalnız sayfa numarasını taşır.

```js
// carpisma.mjs — elle birlestirilmis anahtarin iki kusuru: cakisma ve kiraci sizintisi
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync("kutuphane.db");
const ara = (baslik, yazar) => db.prepare(
  "SELECT baslik FROM kitap WHERE baslik LIKE ? AND yazar LIKE ? ORDER BY kitap_id")
  .all(`%${baslik}%`, `%${yazar}%`).map((k) => k.baslik);
const subeListesi = (subeId, sayfa) => db.prepare(
  "SELECT baslik FROM kitap WHERE sube_id = ? ORDER BY kitap_id LIMIT 2 OFFSET ?")
  .all(subeId, (sayfa - 1) * 2).map((k) => k.baslik);

const kutu = new Map();
const onbellekli = (anahtar, uret) => {
  if (kutu.has(anahtar)) return { anahtar, sonuc: kutu.get(anahtar), kaynak: "onbellek" };
  const sonuc = uret();
  kutu.set(anahtar, sonuc);
  return { anahtar, sonuc, kaynak: "kaynak" };
};

console.log("--- cakisma: iki farkli sorgu ayni anahtara dusuyor");
const a = onbellekli(`arama:Kum:Borges`, () => ara("Kum", "Borges"));
const b = onbellekli(`arama:Kum:Borges`, () => ara("Kum: Borges", ""));
console.log(`A baslik="Kum" yazar="Borges"   anahtar=${a.anahtar} ${a.kaynak}: ${a.sonuc.join(", ")}`);
console.log(`B baslik="Kum: Borges" yazar=""  anahtar=${b.anahtar} ${b.kaynak}: ${b.sonuc.join(", ")}`);
console.log(`B'nin kaynaktan alacagi dogru sonuc: ${ara("Kum: Borges", "").join(", ")}`);

console.log("--- kiraci sizintisi: anahtar sube bilgisini tasimiyor");
const s2 = onbellekli("liste:sayfa:1", () => subeListesi(2, 1));
const s3 = onbellekli("liste:sayfa:1", () => subeListesi(3, 1));
console.log(`sube 2 sayfa 1 -> ${s2.kaynak}: ${s2.sonuc.join(", ")}`);
console.log(`sube 3 sayfa 1 -> ${s3.kaynak}: ${s3.sonuc.join(", ")}`);
const dogru = subeListesi(3, 1);
const sizan = s3.sonuc.filter((x) => !dogru.includes(x));
console.log(`sube 3'e sunulan ama sube 3'e ait olmayan kayit: ${sizan.length} -> ${sizan.join(", ")}`);
db.close();
```

```
--- cakisma: iki farkli sorgu ayni anahtara dusuyor
A baslik="Kum" yazar="Borges"   anahtar=arama:Kum:Borges kaynak: Kum Kitabı
B baslik="Kum: Borges" yazar=""  anahtar=arama:Kum:Borges onbellek: Kum Kitabı
B'nin kaynaktan alacagi dogru sonuc: Kum: Borges Seçkisi
--- kiraci sizintisi: anahtar sube bilgisini tasimiyor
sube 2 sayfa 1 -> kaynak: Kum Kitabı, Yaban
sube 3 sayfa 1 -> onbellek: Kum Kitabı, Yaban
sube 3'e sunulan ama sube 3'e ait olmayan kayit: 2 -> Kum Kitabı, Yaban
```

Birinci kusur bir **çakışmadır**: ayırıcı olarak kullanılan karakter değerin kendi içinde
geçtiği anda iki farklı ölçüt kümesi aynı dizgiyi üretir. İkinci arama önbellekten ilkinin
yanıtını aldı ve kimse bunu fark etmedi.

İkinci kusur bir **kiracı sızıntısıdır**. Kütüphane sisteminde şube bir **kiracıdır**
(tenant): her şube kendi verisini görür. Anahtar şubeyi taşımadığı için Kadıköy şubesinin
listesi Bahçelievler'in kitaplarını gösterdi. Kimlik Doğrulama ve Yetkilendirme kursunda
sunucu tarafında kurulan yetkilendirme denetimi burada devrede değildir; sorgu hiç
çalışmadı, yanıt önbellekten geldi. **Önbellek, yetkilendirme denetiminin arkasından
dolaşabilen tek katmandır.**

#### Anahtarın Parçaları

İki kusur da anahtarın elle birleştirilmesinden doğdu. Anahtarı üreten tek bir işlev,
kuralları tek yerde toplar.

```js
// anahtar.mjs — ad alani, kiraci ve kurallı olcutlerden onbellek anahtari ureten islev
import { createHash } from "node:crypto";

const BICIM = 1;                                   // anahtar bicimi surumu
const kacir = (m) => encodeURIComponent(String(m));

export function anahtarUret({ adAlani, islem, kiraci, nesil = 1, olcutler = {} }) {
  const olcut = Object.entries(olcutler)
    .filter(([, d]) => d !== undefined && d !== null && d !== "")
    .sort(([a], [b]) => (a < b ? -1 : 1))          // sira bagimsiz: ayni olcut kumesi ayni anahtar
    .map(([ad, d]) => `${kacir(ad)}=${kacir(d)}`)
    .join("&");
  const govde = `${kacir(adAlani)}:b${BICIM}:n${nesil}:k${kacir(kiraci)}:${kacir(islem)}`;
  const kuyruk = olcut.length <= 48 ? olcut : `o_${createHash("sha256").update(olcut).digest("hex").slice(0, 16)}`;
  return kuyruk ? `${govde}:${kuyruk}` : govde;
}

if (import.meta.url === `file://${process.argv[1]}`) {
  const A = (o) => anahtarUret({ adAlani: "kutuphane", nesil: 4, ...o });

  console.log("--- cakisan iki sorgu artik ayri anahtarda");
  console.log(A({ islem: "arama", kiraci: "sube-2", olcutler: { baslik: "Kum", yazar: "Borges" } }));
  console.log(A({ islem: "arama", kiraci: "sube-2", olcutler: { baslik: "Kum: Borges", yazar: "" } }));

  console.log("--- kiraci anahtarda: ayni sorgu, ayri sube");
  console.log(A({ islem: "liste", kiraci: "sube-2", olcutler: { sayfa: 1 } }));
  console.log(A({ islem: "liste", kiraci: "sube-3", olcutler: { sayfa: 1 } }));

  console.log("--- olcut sirasi anahtari degistirmiyor");
  const s1 = A({ islem: "arama", kiraci: "sube-1", olcutler: { yazar: "Atay", baslik: "Tutunamayanlar" } });
  const s2 = A({ islem: "arama", kiraci: "sube-1", olcutler: { baslik: "Tutunamayanlar", yazar: "Atay" } });
  console.log(`${s1}\nesit mi: ${s1 === s2}`);

  console.log("--- uzun olcut kumesi ozetleniyor");
  const uzun = A({ islem: "arama", kiraci: "sube-1", olcutler: {
    baslik: "Tutunamayanlar", yazar: "Oğuz Atay", yilBas: 1970, yilSon: 1980,
    dil: "tr", durum: "rafta", siralama: "baslik", sayfa: 3 } });
  console.log(`${uzun}\nuzunluk=${uzun.length}`);
}
```

```
--- cakisan iki sorgu artik ayri anahtarda
kutuphane:b1:n4:ksube-2:arama:baslik=Kum&yazar=Borges
kutuphane:b1:n4:ksube-2:arama:baslik=Kum%3A%20Borges
--- kiraci anahtarda: ayni sorgu, ayri sube
kutuphane:b1:n4:ksube-2:liste:sayfa=1
kutuphane:b1:n4:ksube-3:liste:sayfa=1
--- olcut sirasi anahtari degistirmiyor
kutuphane:b1:n4:ksube-1:arama:baslik=Tutunamayanlar&yazar=Atay
esit mi: true
--- uzun olcut kumesi ozetleniyor
kutuphane:b1:n4:ksube-1:arama:o_f350218d0caedc9e
uzunluk=48
```

Anahtar beş bölümden oluşuyor. **Ad alanı** aynı önbellek deposunu paylaşan farklı
uygulamaları ayırır. **Biçim sürümü** anahtar yapısının kendisini sürümler: değer biçimi
değiştiğinde bu sayı artırılır ve eski biçimdeki bütün girdiler tek hamlede erişilemez olur.
**Nesil** önceki dersteki toplu geçersizleştirmeyi taşır. **Kiracı** yetki sınırını anahtarın
içine yerleştirir. **İşlem** ve **kurallı ölçütler** ise sorgunun kimliğidir.

Kurallılık üç kuralla sağlanıyor: boş ölçütler atılır, ölçütler ada göre sıralanır, ad ve
değer kaçırılır. Sıralama, aynı sorgunun farklı sırayla gelen ölçütlerle iki ayrı girdi
üretmesini engeller. Kaçırma, birinci kusurdaki çakışmayı yapısal olarak imkânsız kılar:
değerin içindeki iki nokta üst üste `%3A` olur ve ayırıcı olmaktan çıkar.

Son bölüm uzunluk sınırıdır. Ölçüt kümesi uzadığında özetlenir, ama anahtarın okunabilir
gövdesi korunur. Bu ayrım işletim sırasında değerlidir: önbellekteki bir girdinin hangi
uygulamaya, hangi şubeye ve hangi işleme ait olduğu anahtara bakılarak görülür; yalnız hangi
ölçütlerle üretildiği görünmez.

#### Fazla Alan ve Eksik Alan

Anahtarın hangi alanları taşıyacağı bir doğruluk kararıdır ve iki yönde de bozulur.

```js
// kardinalite.mjs — anahtara fazla alan koymak isabeti, eksik alan koymak dogrulugu bozar
import { DatabaseSync } from "node:sqlite";
import { anahtarUret } from "./anahtar.mjs";

const db = new DatabaseSync("kutuphane.db");
const liste = (subeId, sayfa) => db.prepare(
  "SELECT baslik FROM kitap WHERE sube_id = ? ORDER BY kitap_id LIMIT 2 OFFSET ?")
  .all(subeId, (sayfa - 1) * 2).map((k) => k.baslik).join(", ");

const istekler = [];                               // 400 istek, 3 sube x 2 sayfa
for (let i = 0; i < 400; i++) istekler.push([(i % 3) + 1, (i % 2) + 1, `i-${i}`]);

function kosu(ad, anahtarla) {
  const kutu = new Map();
  const o = { isabet: 0, iska: 0, yanlis: 0 };
  for (const [subeId, sayfa, istekKimligi] of istekler) {
    const anahtar = anahtarla(subeId, sayfa, istekKimligi);
    let deger;
    if (kutu.has(anahtar)) { o.isabet += 1; deger = kutu.get(anahtar); }
    else { o.iska += 1; deger = liste(subeId, sayfa); kutu.set(anahtar, deger); }
    if (deger !== liste(subeId, sayfa)) o.yanlis += 1;
  }
  const oran = ((o.isabet / istekler.length) * 100).toFixed(1);
  return `${ad.padEnd(26)}${String(o.isabet).padStart(8)}${String(o.iska).padStart(8)}` +
    `${(oran + "%").padStart(10)}${String(o.yanlis).padStart(9)}${String(kutu.size).padStart(8)}`;
}

const temel = { adAlani: "kutuphane", islem: "liste", nesil: 1 };
console.log(["anahtar bicimi", "isabet", "iska", "oran", "yanlis", "girdi"]
  .map((b, i) => (i === 0 ? b.padEnd(26) : b.padStart(i === 3 ? 10 : i === 4 ? 9 : 8))).join(""));
console.log(kosu("dogru (kiraci + olcut)", (s, y) =>
  anahtarUret({ ...temel, kiraci: `sube-${s}`, olcutler: { sayfa: y } })));
console.log(kosu("fazla alan (istek kimligi)", (s, y, k) =>
  anahtarUret({ ...temel, kiraci: `sube-${s}`, olcutler: { sayfa: y, istek: k } })));
console.log(kosu("eksik alan (kiraci yok)", (s, y) =>
  anahtarUret({ ...temel, kiraci: "ortak", olcutler: { sayfa: y } })));
db.close();
```

```
anahtar bicimi              isabet    iska      oran   yanlis   girdi
dogru (kiraci + olcut)         394       6     98.5%        0       6
fazla alan (istek kimligi)       0     400      0.0%        0     400
eksik alan (kiraci yok)        398       2     99.5%      200       2
```

Fazla alan isabeti sıfırlar. İstek kimliği her istekte farklı olduğu için hiçbir anahtar
ikinci kez aranmaz; önbellek dört yüz girdiyle dolar ve tek bir isabet üretmez. Aynı sonucu
zaman damgası, oturum kimliği ya da yanıtı etkilemeyen bir başlık da doğurur.

Eksik alanın satırı daha tehlikelidir. İsabet oranı en yüksek olan satır odur — %99,5 — ve
dört yüz isteğin ikisi hariç hepsi iki girdiden karşılanmıştır. Aynı satırda iki yüz yanlış
yanıt vardır. **En yüksek isabet oranı, yanlış gerçekleştirime aittir.** İsabet oranı tek
başına bir kalite ölçütü değildir; doğruluk ayrıca kanıtlanmalıdır.

Bundan çıkan kural şudur: anahtar, yanıtı değiştiren her girdiyi taşımalı ve değiştirmeyen
hiçbir girdiyi taşımamalıdır. Yanıtı değiştirenler arasında yetki bağlamı — kiracı, rol,
görünürlük düzeyi — her zaman bulunur. Yanıtı değiştirmeyenler arasında istek kimliği, izleme
başlıkları ve oturum belirteci bulunur. Belirtecin anahtara girmesi ayrıca bir güvenlik
kusurudur: gizli bir değer, önbellek anahtarlarını listeleyen herkesin görebileceği bir yere
yazılmış olur.

#### Özet

- Ayırıcı karakterle elle birleştirilen anahtar, değerin kendisi o karakteri taşıdığında
  çakışır; iki farklı arama aynı girdiden karşılandı.
- Anahtar kiracıyı taşımadığında önbellek yetkilendirme denetiminin arkasından dolaşır:
  bir şubenin listesi başka bir şubeye sunuldu.
- Anahtar üreteci beş bölüm taşır: ad alanı, biçim sürümü, nesil, kiracı ve kurallı
  ölçütler. Kaçırma çakışmayı, sıralama ise aynı sorgunun iki girdiye bölünmesini önler.
- Uzun ölçüt kümeleri özetlenir, ama okunabilir gövde korunur; girdinin hangi uygulamaya ve
  hangi kiracıya ait olduğu anahtardan okunabilir kalır.
- Anahtara fazla alan koymak isabeti sıfırladı, eksik alan koymak %99,5 isabetle 200 yanlış
  yanıt üretti. İsabet oranı tek başına doğruluk kanıtı değildir.

#### Sonraki Adım

Bu derse kadar bütün ölçümler tek bir işlem sırasında yapıldı: bir istek ıska alır, kaynağa
gider, girdiyi koyar, sıradaki istek isabet alır. Gerçek bir sunucuda aynı anahtarı aynı anda
yüzlerce istek arar. O anahtarın girdisi düştüğü anda hepsi birden ıska alır ve hepsi birden
kaynağa gider; önbelleğin koruduğu kaynak, tam da önbelleğin boşaldığı anda en yüksek yükü
görür. Sonraki ders bu davranışı eşzamanlı isteklerle ölçer ve aynı anahtar için kaynağa
giden çağrıyı tek bir üretime indiren kilidi kurar.

### Sıcak Anahtar ve Sürü Etkisi

Source: https://academia.sh/tr/kurslar/eszamansiz-isleme/sicak-anahtar-ve-suru-etkisi

Sıcak bir anahtarın girdisi düştüğünde eşzamanlı yüz isteğin kaynağa yığılmasının ölçülmesi ve tek uçuş kilidiyle bunun tek üretime indirilmesi; yeniden doğrulayarak sunmanın bekleme süresini sıfırlaması, yaşam süresinin dağıtılmasıyla eşzamanlı süre sonunun önlenmesi ve sıcak anahtarın bölümlere yayılması.

Önceki derslerin bütün ölçümleri tek bir sırada yapıldı: bir istek ıska alır, kaynağa gider,
girdiyi koyar, sıradaki istek isabet alır. Bu sıra, isteklerin birbirini beklediğini
varsayar. Gerçek bir sunucuda varsaymaz.

Kütüphane sisteminin en çok okunan anahtarı Merkez şubesinin stok sayısıdır. O girdi
düştüğünde, düşme anıyla yeni girdinin konması arasında geçen sürede gelen **her** istek ıska
alır ve **her biri** kaynağa gider. Bu davranışa **sürü etkisi** (cache stampede) denir ve
sonucu şudur: kaynak, en çok korunduğu anahtarda en yüksek yükü, tam da önbelleğin boşaldığı
anda görür. Bu ders davranışı ölçer ve üç önlemi karşılaştırır.

#### Yüz İsteğin Aynı Anda Gelmesi

Ölçüm düzeneğinde kaynak erişimi eşzamansız bir çağrıdır: sorgu ile gidiş-dönüş birlikte 30
milisaniye sürer. Sayılan şey süre değil, **kaynağa giden çağrı sayısıdır**.

```js
// suru.mjs — sicak anahtarin girdisi dustugu anda 100 istek birden geliyor
import { setTimeout as bekle } from "node:timers/promises";

const GECIKME = 30;                                // kaynak erisimi: sorgu + gidis-donus
const sayac = { kaynak: 0 };
async function kaynaktanUret(anahtar) {            // sube stok sayisi: pahali toplama sorgusu
  sayac.kaynak += 1;
  await bekle(GECIKME);
  return { anahtar, raftaki: 137 };
}

const ANAHTAR = "kutuphane:b1:n1:ksube-1:stok";
const ISTEK = 100;

async function kosu(ad, kur) {
  sayac.kaynak = 0;
  const olcu = { bekleyen: 0, bayat: 0 };
  const oku = kur(olcu);
  const t = performance.now();
  await Promise.all(Array.from({ length: ISTEK }, () => oku(ANAHTAR)));
  const sure = performance.now() - t;
  return `${ad.padEnd(28)}${String(sayac.kaynak).padStart(14)}${String(olcu.bekleyen).padStart(16)}` +
    `${String(olcu.bayat).padStart(12)}${(String(Math.round(sure / 10) * 10) + " ms").padStart(12)}`;
}

// 1) Duz yaninda okuma: girdi yok, herkes kaynaga gider.
const duz = (olcu) => {
  const kutu = new Map();
  return async (a) => {
    if (kutu.has(a)) return kutu.get(a);
    olcu.bekleyen += 1;
    const d = await kaynaktanUret(a);
    kutu.set(a, d);
    return d;
  };
};

// 2) Tek ucus: ayni anahtar icin tek uretim; digerleri ayni sozu bekler.
const tekUcus = (olcu) => {
  const kutu = new Map();
  const ucus = new Map();
  return async (a) => {
    if (kutu.has(a)) return kutu.get(a);
    olcu.bekleyen += 1;
    if (!ucus.has(a)) {
      ucus.set(a, kaynaktanUret(a).then((d) => { kutu.set(a, d); ucus.delete(a); return d; }));
    }
    return ucus.get(a);
  };
};

// 3) Yeniden dogrulayarak sunma: suresi dolmus girdi hemen donulur, yenileme arkada baslar.
const yenidenDogrulayarakSun = (olcu) => {
  const kutu = new Map([[ANAHTAR, { deger: { anahtar: ANAHTAR, raftaki: 131 }, taze: false }]]);
  const ucus = new Map();
  return async (a) => {
    const girdi = kutu.get(a);
    if (girdi && !girdi.taze && !ucus.has(a)) {     // yenilemeyi baslat, bekleme
      ucus.set(a, kaynaktanUret(a).then((d) => { kutu.set(a, { deger: d, taze: true }); ucus.delete(a); }));
    }
    if (girdi) { if (!girdi.taze) olcu.bayat += 1; return girdi.deger; }
    olcu.bekleyen += 1;
    return kaynaktanUret(a);
  };
};

console.log(["strateji", "kaynak cagrisi", "bekleyen istek", "bayat yanit", "sure~"]
  .map((b, i) => (i === 0 ? b.padEnd(28) : b.padStart([0, 14, 16, 12, 12][i]))).join(""));
console.log(await kosu("duz", duz));
console.log(await kosu("tek ucus", tekUcus));
console.log(await kosu("yeniden dogrulayarak sunma", yenidenDogrulayarakSun));
```

{/* mask: (?<=duz[ 0-9]{74})[34]0(?= ms)|(?<=tek ucus[ 0-9]{69})[34]0(?= ms) */}
```
strateji                    kaynak cagrisi  bekleyen istek bayat yanit       sure~
duz                                    100             100           0       30 ms
tek ucus                                 1             100           0       30 ms
yeniden dogrulayarak sunma               1               0         100        0 ms
```

Süre sütunu on milisaniyeye yuvarlanmıştır ve makineye bağlıdır; ilk üç sütun değildir.

**Düz** kurulumda yüz istek yüz kaynak çağrısı üretti. Kod hatalı değil: her istek önbelleğe
baktı, bulamadı, kaynağa gitti. Kusur, ıska ile girdinin konması arasındaki boşlukta yatar.
Bu boşluk kaynak erişimi kadar sürer ve o süre boyunca gelen her istek aynı işi yeniden
başlatır. Yükün büyüklüğü isteklerin gelme hızıyla belirlenir: saniyede bin istek alan bir
uç noktada 30 milisaniyelik boşluk otuz eşzamanlı sorgu demektir.

**Tek uçuş** (single-flight) kilidi kaynak çağrısını bire indirdi. Kurulum basittir: üretim
başlatılmadan önce anahtar bir uçuş tablosuna yazılır ve sonraki istekler aynı sözü
bekler. Yüz isteğin hepsi yine bekledi — sonuçta veri henüz yoktu — ama kaynak tek sorgu
gördü. Bu, sürü etkisinin doğrudan çözümüdür.

**Yeniden doğrulayarak sunma** (stale-while-revalidate) farklı bir ödün verir. Süresi dolmuş
girdi silinmez; istek gelince eski değer hemen döndürülür ve yenileme arka planda başlatılır.
Bekleyen istek sayısı sıfıra indi, karşılığında yüz yanıt bayat veri taşıdı. Frontend
Kalitesi kursundaki Önbellek Stratejileri dersinde tarayıcı tarafında tanıtılan aynı kalıp,
sunucu tarafında da aynı ödünü verir: bekleme yerine bayatlık.

Üç satır arasındaki seçim veriye bağlıdır. Şube stok sayısı birkaç saniye eski olabilir;
orada yeniden doğrulayarak sunma en iyi seçimdir. Ödünç verme sırasında yapılan uygunluk
denetimi bayat veriyle çalışamaz; orada tek uçuş kullanılır.

Tek uçuş tablosunun kapsamı da ayrı bir karardır. Süreç içindeki tablo yalnız o örneği
korur; dört uygulama örneği çalışıyorsa kaynağa bir yerine dört çağrı gider. Kaynağı kesin
olarak tek çağrıya indirmek için kilidin paylaşılan depoda tutulması gerekir; bu, doğruluğu
artırırken her ıskaya bir ağ turu ekler.

#### Süre Sonlarının Aynı Ana Düşmesi

Sürü etkisinin ikinci biçimi tek bir anahtarda değil, aynı anda yüklenmiş bir anahtar
kümesinde ortaya çıkar. Bir uygulama örneği yeniden başlatıldığında ya da bir nesil sayacı
artırıldığında yüzlerce anahtar aynı adımda önbelleğe girer. Hepsine aynı yaşam süresi
verilirse hepsi aynı adımda düşer.

```js
// sicak-anahtar.mjs — es zamanli sure sonu ve tek dugume yigilan sicak anahtar
import { createHash } from "node:crypto";

const ozet = (m) => parseInt(createHash("sha256").update(m).digest("hex").slice(0, 8), 16);

console.log("--- 200 anahtar ayni adimda yuklendi, omur=100");
for (const [ad, omur] of [["sabit omur", () => 100], ["dagitilmis omur", (a) => 100 + (ozet(a) % 40)]]) {
  const adimBasi = new Map();
  for (let i = 1; i <= 200; i++) {
    const anahtar = `kutuphane:b1:n1:ksube-1:kitap:${i}`;
    const adim = omur(anahtar);
    adimBasi.set(adim, (adimBasi.get(adim) ?? 0) + 1);
  }
  const enYuksek = Math.max(...adimBasi.values());
  console.log(`${ad.padEnd(18)} sure sonu adimi=${adimBasi.size}  tek adimdaki en yuksek iska=${enYuksek}`);
}

console.log("--- 4 bolumlu paylasilan onbellek, isteklerin %60'i tek anahtarda");
const SICAK = "kutuphane:b1:n1:ksube-1:stok";
const istekler = Array.from({ length: 1000 }, (_, i) =>
  i % 10 < 6 ? SICAK : `kutuphane:b1:n1:ksube-${(i % 3) + 2}:kitap:${i % 40}`);

function dagilim(donustur) {
  const bolum = [0, 0, 0, 0];
  for (const [i, a] of istekler.entries()) bolum[ozet(donustur(a, i)) % 4] += 1;
  return bolum;
}
const yaz = (ad, b) => console.log(`${ad.padEnd(22)}${b.map((n) => String(n).padStart(8)).join("")}` +
  `   en yuklu bolum=%${((Math.max(...b) / 1000) * 100).toFixed(0)}`);

yaz("tek anahtar", dagilim((a) => a));
yaz("4 kopyaya bolunmus", dagilim((a, i) => (a === SICAK ? `${a}#${i % 4}` : a)));
yaz("8 kopyaya bolunmus", dagilim((a, i) => (a === SICAK ? `${a}#${i % 8}` : a)));
```

```
--- 200 anahtar ayni adimda yuklendi, omur=100
sabit omur         sure sonu adimi=1  tek adimdaki en yuksek iska=200
dagitilmis omur    sure sonu adimi=40  tek adimdaki en yuksek iska=11
--- 4 bolumlu paylasilan onbellek, isteklerin %60'i tek anahtarda
tek anahtar                150     116      83     651   en yuklu bolum=%65
4 kopyaya bolunmus         300     416      83     201   en yuklu bolum=%42
8 kopyaya bolunmus         300     341     158     201   en yuklu bolum=%34
```

Sabit ömürde iki yüz anahtarın tamamı tek bir adımda düştü. Yaşam süresine anahtardan
türetilen küçük bir sapma eklendiğinde süre sonları kırk adıma yayıldı ve tek adımdaki en
yüksek ıska sayısı 11'e indi. Sapmanın anahtardan türetilmesi önemlidir: rastgele bir sayı
kullanılırsa aynı anahtar farklı örneklerde farklı sürelerde düşer ve davranış
yinelenemez hâle gelir.

#### Sıcak Anahtarın Kendisi

Çıktının ikinci yarısı ayrı bir sorunu ölçüyor. Paylaşılan bir önbellek birden çok düğüme
bölündüğünde anahtar, özetine göre bir bölüme düşer. İsteklerin %60'ı tek bir anahtardaysa o
anahtarın bölümü isteklerin %65'ini görür; kalan üç bölüm boşta kalır. Bölüm sayısını
artırmak bu dağılımı düzeltmez, çünkü sorun anahtar sayısının azlığında değil, **tek bir
anahtarın ağırlığındadır**.

Çözüm anahtarı çoğaltmaktır: sıcak anahtar sabit sayıda kopyaya bölünür ve istek bunlardan
birine yönlendirilir. Dört kopyada en yüklü bölüm %65'ten %42'ye, sekiz kopyada %34'e indi.
Dağılım eşit değildir — özet işlevi kopyaları bölümlere eşit dağıtmaz — ama yığılma
kırılmıştır.

Çoğaltmanın bedeli iki tanedir. Aynı değer birden çok kez saklanır ve geçersizleştirme artık
tek anahtarı değil, bütün kopyaları düşürmelidir. İkincisi, kopya sayısı sabit olduğu için
önceki dersteki nesil sayacı burada da işe yarar: nesil artırıldığında bütün kopyalar tek
hamlede erişilemez olur.

Sıcak anahtarın üçüncü ve en ucuz önlemi, uygulama örneğinin içinde kısa ömürlü bir yerel
kopya tutmaktır. İlk derste ölçülen katman zincirinin sunucu halkası budur: paylaşılan
önbelleğin önünde duran birkaç saniyelik yerel bir girdi, sıcak anahtara giden ağ turlarının
çoğunu keser.

#### Özet

- Sıcak bir anahtarın girdisi düştüğünde ıska ile girdinin konması arasındaki boşlukta gelen
  her istek kaynağa gider: yüz eşzamanlı istek yüz kaynak çağrısı üretti.
- Tek uçuş kilidi aynı anahtar için üretimi bire indirir; yüz istek yine bekledi ama kaynak
  tek sorgu gördü. Kilit süreç içindeyse koruma örnek sayısı kadar zayıflar.
- Yeniden doğrulayarak sunma bekleyen istek sayısını sıfıra indirir ve karşılığında bayat yanıt döndürür;
  seçim, verinin bayatlık toleransına bağlıdır.
- Aynı anda yüklenen iki yüz anahtar sabit ömürle tek adımda düştü; ömre anahtardan türetilen
  bir sapma eklendiğinde tek adımdaki en yüksek ıska 200'den 11'e indi.
- Bölümlere ayrılmış paylaşılan bir önbellekte tek sıcak anahtar bir bölüme %65 yük bindirdi;
  anahtar sekiz kopyaya bölündüğünde en yüklü bölüm %34'e indi.

#### Sonraki Adım

Bu derse kadar bütün önbellekler uygulamanın içinde kuruldu ve bütün kararlar kodda verildi.
Yanıt istemciye gönderildiğinde ise önbellekleme kararı artık kodun elinde değildir: tarayıcı,
aradaki paylaşılan önbellekler ve kenar katmanı, yalnız yanıtın başlıklarında yazana bakar.
Bu, ilk derste "denetlenemez" diye anılan katmanla konuşmanın tek yoludur ve kendi
sözlüğü vardır — hangi yanıt saklanabilir, ne kadar taze sayılır, saklanan kopyanın hâlâ
geçerli olup olmadığı kaynağa sorulmadan nasıl anlaşılır. Sonraki ders bu sözlüğü bir HTTP
sunucusu ile gerçek istekler üzerinde kurar.

### HTTP Önbellekleme

Source: https://academia.sh/tr/kurslar/eszamansiz-isleme/http-onbellekleme

Yanıtın kendi önbellek kurallarını taşıması: ETag ve Last-Modified doğrulayıcılarının koşullu isteklerle 304 üretmesi, gövde aktarımının sıfıra inmesi, kaynak değiştiğinde doğrulayıcının değişmesi ve public, private, max-age, no-store ile yeniden doğrulayarak sunma yönergelerinin kaynak türüne göre seçilmesi.

Önceki derslerin bütün önbellekleri uygulamanın içindeydi: anahtar kodda üretiliyor, girdi
kodda konuyor, geçersizleştirme kodda tetikleniyordu. Yanıt istemciye gönderildiği anda bu
denetim biter. İlk derste "denetlenemez" diye anılan katmanlar — tarayıcı önbelleği,
aradaki paylaşılan önbellekler, kenar katmanı — kodun ne düşündüğünü bilmez; yalnız yanıtın
başlıklarında yazana bakar.

Bu yüzden HTTP'nin kendi önbellek sözlüğü vardır ve iki soruyu ayrı ayrı yanıtlar: bir yanıt
ne kadar süre **taze** sayılır, tazeliği bittiğinde saklanan kopyanın hâlâ geçerli olup
olmadığı kaynağa gövde indirmeden nasıl sorulur. Bu ders iki soruyu da gerçek istekler
üzerinde yanıtlar.

#### Doğrulayıcı Üreten Bir Sunucu

Aşağıdaki sunucu kütüphane API'sinin dört uç noktasını sunar. Her yanıt gövdesinin özetinden
bir **doğrulayıcı** (validator) üretilir ve koşullu istekler bu değere göre yanıtlanır.
Yanıtlardaki tarih başlığı sabit tutulmuştur; böylece ölçümün çıktısı her koşuda aynıdır.

```js
// http-onbellek.mjs — kutuphane API'si: dogrulayicilar ve onbellek denetim yonergeleri
import { createServer } from "node:http";
import { createHash } from "node:crypto";

const kitaplar = new Map([[7, { kitapId: 7, baslik: "Kum: Borges Seçkisi", yazar: "Derleme", rafta: 3 }]]);
const stok = { subeId: 1, raftaki: 137 };
const gecmis = { uyeId: 4, toplam: 12 };
const DEGISIM = new Map([[7, new Date("2025-07-01T09:00:00Z")]]);   // son degisiklik anlari

const etiket = (govde) => `"${createHash("sha256").update(govde).digest("hex").slice(0, 16)}"`;

function yanitla(istek, yanit, { govde, denetim, sonDegisim = null, vary = null }) {
  const metin = JSON.stringify(govde);
  const etag = etiket(metin);
  const basliklar = { "cache-control": denetim, etag };
  if (sonDegisim) basliklar["last-modified"] = sonDegisim.toUTCString();
  if (vary) basliklar.vary = vary;

  const gelenEtag = istek.headers["if-none-match"];
  const gelenTarih = istek.headers["if-modified-since"];
  const etagUyuyor = gelenEtag !== undefined && gelenEtag.split(",").map((e) => e.trim()).includes(etag);
  const tarihUyuyor = !etagUyuyor && gelenTarih !== undefined && sonDegisim !== null &&
    Math.floor(sonDegisim.getTime() / 1000) <= Math.floor(Date.parse(gelenTarih) / 1000);

  if (etagUyuyor || tarihUyuyor) { yanit.writeHead(304, basliklar); return yanit.end(); }
  yanit.writeHead(200, { ...basliklar, "content-type": "application/json; charset=utf-8",
    "content-length": Buffer.byteLength(metin) });
  yanit.end(metin);
}

const sunucu = createServer((istek, yanit) => {
  yanit.sendDate = false;                                          // ciktinin belirlenimci kalmasi icin
  const yol = new URL(istek.url, "http://yerel").pathname;

  if (istek.method === "POST" && yol === "/kitap/7") {              // kitap guncellenir: dogrulayici degisir
    const k = kitaplar.get(7);
    k.rafta -= 1;
    DEGISIM.set(7, new Date("2025-07-02T14:30:00Z"));
    yanit.writeHead(204, { "cache-control": "no-store" });
    return yanit.end();
  }
  if (yol === "/kitap/7") {                                         // seyrek degisen genel kaynak
    return yanitla(istek, yanit, { govde: kitaplar.get(7), denetim: "public, max-age=60",
      sonDegisim: DEGISIM.get(7), vary: "Accept-Language" });
  }
  if (yol === "/sube/1/stok") {                                     // sik degisen genel kaynak
    return yanitla(istek, yanit, { govde: stok, denetim: "public, max-age=5, stale-while-revalidate=30" });
  }
  if (yol === "/uye/4/gecmis") {                                    // kisiye ozel kaynak
    return yanitla(istek, yanit, { govde: gecmis, denetim: "private, max-age=30" });
  }
  if (yol === "/odunc/onay") {                                      // hic saklanmamasi gereken yanit
    return yanitla(istek, yanit, { govde: { onay: "U-4711", kalan: 2 }, denetim: "no-store" });
  }
  yanit.writeHead(404, { "cache-control": "no-store" });
  yanit.end();
});

sunucu.listen(8547, "127.0.0.1", () => console.log("kutuphane API 127.0.0.1:8547"));
```

Sunucuyu başlatan ve ölçen blok ayrıdır. Doğrulayıcı ile son değişiklik tarihi yanıttan
okunur, sonraki isteklere koşul başlığı olarak konur.

```bash
# Yanitlarin onbellek basliklari ve kosullu isteklerin sonucu
node http-onbellek.mjs & sunucu=$!
sleep 0.5
K=http://127.0.0.1:8547

basliklar() { curl -sS -D - -o /dev/null "$@" | tr -d '\r' | grep -iE '^(HTTP/|etag|last-modified|cache-control|vary)'; }
olc() { curl -sS -o /dev/null -w "durum=%{http_code} indirilen=%{size_download} bayt\n" "$@"; }

echo "== 1. ilk istek: dogrulayicilar ve yonergeler"
basliklar "$K/kitap/7"

ETAG=$(curl -sS -D - -o /dev/null "$K/kitap/7" | tr -d '\r' | awk '/^[Ee]tag:/ {print $2}')
TARIH=$(curl -sS -D - -o /dev/null "$K/kitap/7" | tr -d '\r' | sed -n 's/^[Ll]ast-[Mm]odified: //p')
echo "== 2. ayni kaynak kosullu istenirse"
olc "$K/kitap/7"
olc -H "If-None-Match: $ETAG" "$K/kitap/7"
olc -H "If-Modified-Since: $TARIH" "$K/kitap/7"

echo "== 3. kaynak degisince eski dogrulayici tutmuyor"
curl -sS -o /dev/null -X POST "$K/kitap/7"
olc -H "If-None-Match: $ETAG" "$K/kitap/7"
basliklar "$K/kitap/7" | grep -iE '^(etag|last-modified)'

echo "== 4. kaynak turune gore yonergeler"
for yol in /kitap/7 /sube/1/stok /uye/4/gecmis /odunc/onay; do
  printf '%-16s %s\n' "$yol" "$(curl -sS -D - -o /dev/null "$K$yol" | tr -d '\r' | sed -n 's/^[Cc]ache-[Cc]ontrol: //p')"
done

kill $sunucu
```

```
kutuphane API 127.0.0.1:8547
== 1. ilk istek: dogrulayicilar ve yonergeler
HTTP/1.1 200 OK
cache-control: public, max-age=60
etag: "c5397b0c31b92e65"
last-modified: Tue, 01 Jul 2025 09:00:00 GMT
vary: Accept-Language
== 2. ayni kaynak kosullu istenirse
durum=200 indirilen=73 bayt
durum=304 indirilen=0 bayt
durum=304 indirilen=0 bayt
== 3. kaynak degisince eski dogrulayici tutmuyor
durum=200 indirilen=73 bayt
etag: "211739eb1d385801"
last-modified: Wed, 02 Jul 2025 14:30:00 GMT
== 4. kaynak turune gore yonergeler
/kitap/7         public, max-age=60
/sube/1/stok     public, max-age=5, stale-while-revalidate=30
/uye/4/gecmis    private, max-age=30
/odunc/onay      no-store
```

#### Tazelik ve Doğrulama

Çıktının ikinci bölümü iki farklı işi ayırıyor. Koşulsuz istek 200 döndürdü ve 73 bayt
indirildi. Aynı kaynak `If-None-Match` başlığıyla istendiğinde yanıt 304 oldu ve indirilen
gövde sıfır bayta indi. Üçüncü satırda aynı sonuç `If-Modified-Since` ile alındı.

**304 Not Modified** yanıtının kazandırdığı şey gövdedir, gidiş-dönüş değil. İstek yine
sunucuya gitti, sunucu yine doğrulayıcıyı hesapladı. Kazanç, gövde büyüdükçe büyür: yetmiş
üç baytlık bir kitap kaydında ihmal edilebilir, elli kilobayttan bir liste yanıtında
belirleyicidir.

Tazelik ise doğrulamadan önce gelir. `max-age=60` yazan bir yanıt altmış saniye boyunca
**taze** sayılır ve o süre içinde önbellek sunucuya hiç sormaz. Süre dolduğunda kopya
atılmaz, **bayat** duruma geçer ve bir sonraki istekte doğrulanır. Doğrulama 304 dönerse
kopya yeniden taze sayılır. İki mekanizma birlikte çalışır: `max-age` gidiş-dönüşü, doğrulayıcı
gövdeyi ortadan kaldırır.

Üçüncü bölüm doğrulayıcının anlamını kanıtlıyor. Kitap POST isteğiyle güncellendikten sonra
elde tutulan eski doğrulayıcıyla yapılan koşullu istek 304 değil 200 döndü ve gövde yeniden
indirildi. Yeni yanıtın hem etiketi hem son değişiklik tarihi değişmiştir. Doğrulayıcı,
kaynağın içeriğine bağlı olduğu için bu davranış kendiliğinden doğrudur.

İki doğrulayıcı arasında bir ayrım vardır. Son değişiklik tarihinin çözünürlüğü bir
saniyedir: aynı saniye içinde iki kez değişen bir kayıt için eski kopya geçerli sanılabilir.
Gövde özetinden üretilen etiketin böyle bir sınırı yoktur. Buna karşılık etiketin
hesaplanması gövdenin üretilmesini gerektirir; tarih ise çoğu zaman kaydın kendi
sütunundan okunur ve sorgu çalıştırmadan yanıtlanabilir.

#### Yönergelerin Anlamı

Son bölümdeki dört satır, dört farklı kaynak türü için verilmiş dört karardır.

`public, max-age=60` — kitap ayrıntısı her kullanıcıya aynı döner ve seyrek değişir.
`public`, yanıtın **paylaşılan** önbelleklerde saklanabileceğini söyler.

`public, max-age=5, stale-while-revalidate=30` — şube stok sayısı sık değişir. Beş saniye
taze sayılır; sonraki otuz saniye boyunca bayat kopya sunulmayı sürdürür ve önbellek arka
planda yenilemeyi yapar. Bu, önceki derste süreç içinde kurulan davranışın HTTP'deki
karşılığıdır; aradaki fark, kararı artık uygulamanın değil aradaki önbelleğin uygulamasıdır.

`private, max-age=30` — üyenin ödünç geçmişi kişiye özeldir. `private`, yanıtın yalnız son
kullanıcının kendi önbelleğinde saklanabileceğini, paylaşılan bir önbelleğe konamayacağını
söyler. Anahtar Tasarımı dersindeki kiracı sızıntısının HTTP karşılığı budur ve tek satırlık
bir başlıkla önlenir.

`no-store` — ödünç onayı hiçbir yerde saklanmamalıdır. Bu yönerge sıkça `no-cache` ile
karıştırılır: `no-cache` saklamayı yasaklamaz, yalnız kullanmadan önce doğrulamayı zorunlu
kılar. Saklanmaması gereken yanıt için doğru yönerge `no-store`'dur.

Paylaşılan önbellekler için ayrı bir süre verilebilir: `s-maxage`, yalnız paylaşılan
önbellekleri bağlar ve `max-age`'i geçersiz kılar. Kenar katmanında uzun, tarayıcıda kısa bir
süre istendiğinde kullanılan yol budur.

#### Vary ve Paylaşılan Anahtar

Kitap yanıtındaki `Vary: Accept-Language` başlığı, paylaşılan önbelleğin anahtarını
değiştirir. Anahtar Tasarımı dersinde önbellek anahtarının yanıtı değiştiren her girdiyi
taşıması gerektiği ölçülmüştü; HTTP'de bu girdiler istek başlıklarıdır ve `Vary` onları adıyla
bildirir. Başlık verilmezse önbellek anahtar olarak yalnız adresi kullanır ve Türkçe isteyen
bir kullanıcıya İngilizce yanıt sunulabilir.

`Vary` başlığının tehlikeli iki değeri vardır. `Vary: Cookie` ya da `Vary: Authorization`
yazan bir yanıt teknik olarak doğrudur ama pratikte önbelleklenemez: her kullanıcının
çerezi farklı olduğu için her istek ayrı bir anahtara düşer ve isabet oranı sıfıra iner. Bu,
Anahtar Tasarımı dersinde ölçülen "fazla alan" satırının aynısıdır. Kişiye özel yanıtlarda
doğru çözüm `Vary` değil `private`'tır.

#### Özet

- HTTP önbelleklemesi iki ayrı mekanizmadır: `max-age` yanıtın ne kadar süre taze sayılacağını,
  doğrulayıcı ise bayat kopyanın hâlâ geçerli olup olmadığını belirler.
- Koşullu istek 304 döndürdüğünde indirilen gövde 73 bayttan 0 bayta indi; gidiş-dönüş yine
  yapıldı. Kazanç gövdededir ve gövde büyüdükçe artar.
- Kaynak güncellendiğinde gövde özetinden üretilen etiket ve son değişiklik tarihi değişti;
  eski doğrulayıcıyla yapılan istek 304 yerine 200 aldı.
- Son değişiklik tarihinin çözünürlüğü bir saniyedir; gövde özetinden üretilen etiketin böyle
  bir sınırı yoktur ama üretilmesi gövdenin hesaplanmasını gerektirir.
- `public` paylaşılan önbelleğe izin verir, `private` yalnız son kullanıcının önbelleğine,
  `no-store` hiçbir yere. `no-cache` saklamayı değil, kullanmadan önce doğrulamamayı yasaklar.

#### Sonraki Adım

Konu boyunca her ders bir sayı üretti: kaynağa giden sorgu sayısı, bayat okuma sayısı, isabet
ve ıska, indirilen bayt. Bu sayılar tek tek anlamlıydı ama hiçbiri "önbellek işe yarıyor mu"
sorusunu tek başına yanıtlamaz. Anahtar Tasarımı dersinde en yüksek isabet oranı yanlış
gerçekleştirime aitti; bir başka kurulumda yüksek isabet oranı, hiç okunmayan ucuz anahtarlardan
gelebilir. Sonraki ders bu sayıları bir ölçüt kümesine bağlar: isabet oranı nasıl hesaplanır,
neyi gizler, ıska cezası ile birleştiğinde ortalama yanıt süresine ne yapar ve önbellek
kapasitesi değiştikçe isabet oranı nasıl davranır.

### Önbellek Ölçütleri

Source: https://academia.sh/tr/kurslar/eszamansiz-isleme/onbellek-olcutleri

İsabet oranının hesaplanması ve ortalama yanıt süresine etkisi: kapasite arttıkça isabet oranının çizdiği eğri, tahliye sayısının anlamı, tek bir toplam oranın gizlediği ad alanı ayrışımı, bayt isabet oranı ile istek isabet oranının farkı ve düşük tekrarlı verinin önbellekten çıkarılmasının ölçülen kazancı.

Konu boyunca her ders bir sayı üretti: kaynağa giden sorgu, bayat okuma, isabet ve ıska,
indirilen bayt. Bu sayılar tek tek anlamlıydı ama hiçbiri "önbellek işe yarıyor mu" sorusunu
tek başına yanıtlamaz. Anahtar Tasarımı dersinde en yüksek isabet oranı yanlış gerçekleştirime
aitti; bu ders, doğru gerçekleştirimlerde bile tek bir oranın neyi gizlediğini gösterir.

Ölçütün tanımı basittir. **İsabet oranı**, isteklerin önbellekten karşılanan payıdır:

$$h = \frac{\text{isabet}}{\text{isabet} + \text{ıska}}$$

Ortalama yanıt süresi ise iki bileşenden oluşur: her istek önbelleğe bakar, ıska alanlar
ayrıca kaynağa gider. Önbellek erişimi $t_o$, kaynak erişimi $t_k$ ise:

$$T = t_o + (1 - h)\,t_k$$

Bu biçim, isabet oranındaki iyileşmenin neden orantısız kazanç verdiğini açıklar. Oran
%90'dan %95'e çıktığında iyileşme beş puandır, ama $1-h$ çarpanı 0,10'dan 0,05'e indiği için
kaynağa binen yük **yarıya** iner.

#### Ölçüm Düzeneği

```sh
# kur.sh — olcum icin kitap ve odunc verisi
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, yazar TEXT NOT NULL,
                    basim_yili INTEGER, sube_id INTEGER NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                    uye_id INTEGER NOT NULL, iade_tarihi TEXT);
INSERT INTO kitap SELECT n, 'Kitap ' || n, 'Yazar ' || (n % 40 + 1), 1950 + n % 70, n % 4 + 1
  FROM (WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n < 300) SELECT n FROM s);
INSERT INTO odunc SELECT n, n % 300 + 1, n % 200 + 1, CASE WHEN n % 5 = 0 THEN NULL ELSE '2025-12-31' END
  FROM (WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n < 4000) SELECT n FROM s);
CREATE INDEX odunc_uye ON odunc(uye_id);
SQL
sqlite3 kutuphane.db "SELECT 'kitap=' || (SELECT count(*) FROM kitap) || ' odunc=' || (SELECT count(*) FROM odunc);"
```

```
kitap=300 odunc=4000
```

Ölçüm için konu boyunca parça parça kurulan davranışları tek yerde toplayan bir önbellek
gerekir: yaşam süresi, kapasite dolduğunda **en son kullanılana göre tahliye** ve sayaçlar.

```js
// onbellek.mjs — sure sonu, en son kullanilana gore tahliye ve sayaclarla onbellek
export function onbellekAc({ kapasite = 128, omur = Infinity, saat = () => Date.now() } = {}) {
  const kutu = new Map();
  const sayac = { isabet: 0, iska: 0, tahliye: 0, sureSonu: 0, isabetBayt: 0, iskaBayt: 0 };
  const boyutu = (d) => Buffer.byteLength(JSON.stringify(d));
  return {
    sayac,
    al(anahtar) {
      const girdi = kutu.get(anahtar);
      if (girdi === undefined) { sayac.iska += 1; return undefined; }
      if (girdi.sonGecerlilik <= saat()) {
        kutu.delete(anahtar); sayac.sureSonu += 1; sayac.iska += 1; return undefined;
      }
      kutu.delete(anahtar); kutu.set(anahtar, girdi);   // en son kullanilan sona tasinir
      sayac.isabet += 1; sayac.isabetBayt += girdi.boyut;
      return girdi.deger;
    },
    koy(anahtar, deger) {
      const boyut = boyutu(deger);
      sayac.iskaBayt += boyut;
      kutu.delete(anahtar);
      kutu.set(anahtar, { deger, boyut, sonGecerlilik: saat() + omur });
      while (kutu.size > kapasite) { kutu.delete(kutu.keys().next().value); sayac.tahliye += 1; }
    },
    get girdiSayisi() { return kutu.size; },
    isabetOrani: () => sayac.isabet / (sayac.isabet + sayac.iska),
    baytIsabetOrani: () => sayac.isabetBayt / (sayac.isabetBayt + sayac.iskaBayt),
  };
}
```

Tahliye sırası `Map`'in ekleme sırasından gelir: okunan girdi silinip yeniden konarak sona
taşınır, kapasite aşıldığında baştaki — yani en uzun süredir kullanılmayan — girdi düşer.

#### Kapasite ile İsabet Oranı

İlk ölçüm tek bir düğmeyi çevirir: önbellek kapasitesi. İş yükü 2000 istektir ve üç işlem
türünü kapsar; farklı anahtar sayıları farklı tekrar oranları üretir.

```js
// kapasite.mjs — kapasite arttikca isabet orani ve kaynaga giden sorgu nasil degisiyor
import { DatabaseSync } from "node:sqlite";
import { onbellekAc } from "./onbellek.mjs";

const db = new DatabaseSync("kutuphane.db");
const sorgu = { sayi: 0 };
const kaynak = {
  kitap: (id) => { sorgu.sayi += 1; return db.prepare(
    "SELECT kitap_id, baslik, yazar, basim_yili FROM kitap WHERE kitap_id = ?").get(id); },
  stok: (id) => { sorgu.sayi += 1; return db.prepare(
    `SELECT ? AS sube_id, count(*) AS raftaki FROM kitap k WHERE k.sube_id = ? AND NOT EXISTS
     (SELECT 1 FROM odunc o WHERE o.kitap_id = k.kitap_id AND o.iade_tarihi IS NULL)`).get(id, id); },
  liste: (id) => { sorgu.sayi += 1; return db.prepare(
    "SELECT kitap_id, baslik FROM kitap WHERE sube_id = ? ORDER BY kitap_id").all(id); },
};

function isYuku(adet) {                             // kitap istekleri 60 baslikta yogunlasir
  let tohum = 20250729;
  const rast = () => ((tohum = (tohum * 1103515245 + 12345) % 2147483648) / 2147483648);
  return Array.from({ length: adet }, () => {
    const r = rast();
    if (r < 0.75) return ["kitap", Math.floor(rast() * 60) + 1];
    if (r < 0.9) return ["stok", Math.floor(rast() * 4) + 1];
    return ["liste", Math.floor(rast() * 4) + 1];
  });
}

const istekler = isYuku(2000);
console.log(["kapasite", "isabet", "iska", "isabet orani", "tahliye", "kaynak sorgusu", "sure~"]
  .map((b, i) => (i === 0 ? b.padEnd(10) : b.padStart([0, 8, 8, 14, 9, 16, 9][i]))).join(""));

for (const kapasite of [4, 8, 16, 32, 64, 128]) {
  sorgu.sayi = 0;
  const onbellek = onbellekAc({ kapasite });
  const t = performance.now();
  for (const [tur, id] of istekler) {
    const anahtar = `kutuphane:b1:n1:k${tur === "kitap" ? "genel" : `sube-${id}`}:${tur}:${id}`;
    let deger = onbellek.al(anahtar);
    if (deger === undefined) { deger = kaynak[tur](id); onbellek.koy(anahtar, deger); }
  }
  const sure = Math.round((performance.now() - t) / 10) * 10;
  const s = onbellek.sayac;
  console.log(String(kapasite).padEnd(10) + [String(s.isabet).padStart(8), String(s.iska).padStart(8),
    (`%` + (onbellek.isabetOrani() * 100).toFixed(1)).padStart(14), String(s.tahliye).padStart(9),
    String(sorgu.sayi).padStart(16), (sure + " ms").padStart(9)].join(""));
}
db.close();
```

{/* mask: (?<=[0-9])[ ]+[0-9]{2,4}(?= ms$) */}
```
kapasite    isabet    iska  isabet orani  tahliye  kaynak sorgusu    sure~
4              133    1867          %6.7     1863            1867   910 ms
8              289    1711         %14.4     1703            1711   760 ms
16             588    1412         %29.4     1396            1412   460 ms
32            1053     947         %52.6      915             947   200 ms
64            1851     149         %92.5       85             149    10 ms
128           1932      68         %96.6        0              68    10 ms
```

Süre sütunu on milisaniyeye yuvarlanmıştır ve makineye bağlıdır; ilk beş sütun değildir.

Eğrinin biçimi anlatıcıdır. Kapasite 32'den 64'e çıktığında isabet oranı %52,6'dan %92,5'e
sıçrıyor, 64'ten 128'e çıktığında yalnız dört puan artıyor. Sıçramanın yeri iş yükünün
**çalışma kümesini** verir: 60 kitap, 4 stok ve 4 liste anahtarı, toplam 68 farklı anahtar.
Kapasite bu sayının altındayken her yeni anahtar bir başkasını atar ve tahliye sayısı
ıskaların neredeyse tamamına eşit olur — kapasite 4'te 1867 ıskaya karşılık 1863 tahliye.
Kapasite çalışma kümesini geçtiğinde tahliye sıfırlanır ve kalan 68 ıska yalnız ilk
yüklemelerdir.

Bu, kapasite kararının doğru sorusunu verir: "ne kadar bellek ayıralım" değil, "çalışma kümesi
kaç anahtar". Tahliye sayacı bu soruyu doğrudan yanıtlar. Tahliye ıska sayısına yakınsa
önbellek kendi girdilerini birbirine yedirmektedir; sıfıra yakınsa kapasite yeterlidir.

Süre sütunu formülün doğruladığı şeyi gösteriyor. Kapasite 4'te isabet oranı %6,7 ve iş yükü
910 milisaniye sürdü; %92,5'te 10 milisaniyeye indi. Aradaki fark, ıska cezasının bu iş
yükünde büyük olmasından gelir — stok sorgusu diğerlerinden pahalıdır.

#### Toplam Oranın Gizlediği

Yukarıdaki tabloda tek bir oran vardı çünkü iş yükünün her parçası önbelleğe uygundu.
Gerçek uygulamalarda böyle olmaz. Aşağıdaki iş yüküne, iki yüz farklı üyenin ödünç geçmişi
eklenmiştir: çok anahtarlı, az tekrarlı bir okuma.

```js
// ayrisik.mjs — tek bir isabet orani neyi gizler: ad alanina ve bayta gore ayrisim
import { DatabaseSync } from "node:sqlite";
import { onbellekAc } from "./onbellek.mjs";

const db = new DatabaseSync("kutuphane.db");
const kaynak = {
  kitap: (id) => db.prepare("SELECT kitap_id, baslik, yazar FROM kitap WHERE kitap_id = ?").get(id),
  stok: (id) => db.prepare(
    `SELECT ? AS sube_id, count(*) AS raftaki FROM kitap k WHERE k.sube_id = ? AND NOT EXISTS
     (SELECT 1 FROM odunc o WHERE o.kitap_id = k.kitap_id AND o.iade_tarihi IS NULL)`).get(id, id),
  liste: (id) => db.prepare("SELECT kitap_id, baslik FROM kitap WHERE sube_id = ? ORDER BY kitap_id").all(id),
  gecmis: (id) => db.prepare(
    "SELECT odunc_id, kitap_id FROM odunc WHERE uye_id = ? ORDER BY odunc_id").all(id),
};

let tohum = 20250729;
const rast = () => ((tohum = (tohum * 1103515245 + 12345) % 2147483648) / 2147483648);
const istekler = Array.from({ length: 2000 }, () => {
  const r = rast();
  if (r < 0.70) return ["kitap", Math.floor(rast() * 60) + 1];
  if (r < 0.80) return ["stok", Math.floor(rast() * 4) + 1];
  if (r < 0.85) return ["liste", Math.floor(rast() * 4) + 1];
  return ["gecmis", Math.floor(rast() * 200) + 1];    // 200 uye, tekrar orani dusuk
});

function kosu(onbelleklenir) {
  const onbellek = onbellekAc({ kapasite: 64 });
  const tur = { kitap: [0, 0], stok: [0, 0], liste: [0, 0], gecmis: [0, 0] };  // [isabet, iska]
  let kaynakSorgusu = 0;
  for (const [t, id] of istekler) {
    if (!onbelleklenir.has(t)) { kaynak[t](id); kaynakSorgusu += 1; tur[t][1] += 1; continue; }
    const anahtar = `kutuphane:b1:n1:k${t === "kitap" ? "genel" : `sube-${(id % 4) + 1}`}:${t}:${id}`;
    const oncekiIsabet = onbellek.sayac.isabet;
    let deger = onbellek.al(anahtar);
    if (deger === undefined) { deger = kaynak[t](id); kaynakSorgusu += 1; onbellek.koy(anahtar, deger); }
    tur[t][onbellek.sayac.isabet > oncekiIsabet ? 0 : 1] += 1;
  }
  return { onbellek, tur, kaynakSorgusu };
}

const { onbellek, tur, kaynakSorgusu } = kosu(new Set(["kitap", "stok", "liste", "gecmis"]));

console.log(["ad alani", "istek", "isabet", "iska", "isabet orani"]
  .map((b, i) => (i === 0 ? b.padEnd(10) : b.padStart([0, 8, 8, 8, 14][i]))).join(""));
for (const [ad, [i, k]] of Object.entries(tur)) {
  console.log(ad.padEnd(10) + [String(i + k).padStart(8), String(i).padStart(8), String(k).padStart(8),
    ("%" + ((i / (i + k)) * 100).toFixed(1)).padStart(14)].join(""));
}
const s = onbellek.sayac;
console.log(`\ntoplam istek isabet orani : %${(onbellek.isabetOrani() * 100).toFixed(1)}`);
console.log(`bayt isabet orani         : %${(onbellek.baytIsabetOrani() * 100).toFixed(1)}`);
console.log(`isabetten donen bayt      : ${s.isabetBayt}   kaynaktan gelen bayt: ${s.iskaBayt}`);
console.log(`kaynak sorgusu            : ${kaynakSorgusu}`);

const ikinci = kosu(new Set(["kitap", "stok", "liste"]));    // gecmis hic onbelleklenmiyor
console.log(`\ngecmis onbelleklenmediginde:`);
console.log(`  kitap isabet orani      : %${((ikinci.tur.kitap[0] / (ikinci.tur.kitap[0] + ikinci.tur.kitap[1])) * 100).toFixed(1)}`);
console.log(`  tahliye                 : ${ikinci.onbellek.sayac.tahliye} (onceki: ${s.tahliye})`);
console.log(`  kaynak sorgusu          : ${ikinci.kaynakSorgusu}`);
db.close();
```

```
ad alani     istek  isabet    iska  isabet orani
kitap         1380     965     415         %69.9
stok           202     189      13         %93.6
liste          110      84      26         %76.4
gecmis         308      19     289          %6.2

toplam istek isabet orani : %62.8
bayt isabet orani         : %51.8
isabetten donen bayt      : 303731   kaynaktan gelen bayt: 282500
kaynak sorgusu            : 743

gecmis onbelleklenmediginde:
  kitap isabet orani      : %90.7
  tahliye                 : 77 (onceki: 679)
  kaynak sorgusu          : 449
```

Toplam oran %62,8'dir ve tek başına raporlandığında "orta düzeyde bir önbellek" izlenimi
verir. Ayrışım başka bir şey söylüyor: ödünç geçmişi ad alanının isabet oranı %6,2. İki yüz
farklı anahtar ve düşük tekrar, bu veriyi önbelleğe uygunsuz kılıyor. Dahası bu ad alanı
zararlıdır — koyduğu her girdi kapasiteden yer alır ve tekrar okunmadan tahliye edilir. Kitap
ad alanının isabet oranını %69,9'a düşüren şey budur.

Son bölüm bunu kanıtlıyor. Ödünç geçmişi hiç önbelleklenmediğinde kitap ad alanının isabet
oranı %69,9'dan %90,7'ye çıktı, tahliye sayısı 679'dan 77'ye indi ve kaynağa giden toplam
sorgu 743'ten 449'a düştü. Bir ad alanının önbellekten **çıkarılması** kaynak yükünü
düşürdü. Bu sonuç yalnız ayrışık ölçümle bulunabilirdi; toplam oran her iki koşuda da benzer
görünürdü.

Bundan çıkan işletme kuralı şudur: isabet oranı ad alanı başına ölçülür. Anahtar Tasarımı
dersinde anahtara konan ad alanı bölümünün ikinci işlevi budur — ölçütü ayrıştırılabilir
kılmak.

#### Bayt İsabet Oranı

Çıktının ikinci satır çifti ayrı bir ayrımı gösteriyor. **İstek isabet oranı** %62,8,
**bayt isabet oranı** %51,8. Fark, değerlerin boyutlarının eşit olmamasından gelir: liste ve
geçmiş yanıtları kitap kaydından çok daha büyüktür.

İki oran farklı soruları yanıtlar. İstek isabet oranı kaynağa kaç çağrı gitmediğini söyler;
gidiş-dönüş ve sorgu maliyetini bu sayı belirler. Bayt isabet oranı ağdan geçmeyen veri
miktarını söyler; bant genişliği ve aktarım süresini bu sayı belirler. Frontend Kalitesi
kursundaki Statik Barındırma dersinde kenar önbelleği için tanımlanan bu iki ölçüt, sunucu
önbelleğinde de aynı ayrımı taşır.

Hangisinin izleneceği darboğaza bağlıdır. Kaynak sorgu sayısıyla zorlanıyorsa istek isabet
oranı, ağ ile zorlanıyorsa bayt isabet oranı belirleyicidir.

#### Özet

- İsabet oranı isteklerin önbellekten karşılanan payıdır; ortalama süre $T = t_o + (1-h)t_k$
  biçiminde davranır, bu yüzden %90'dan %95'e çıkmak kaynak yükünü yarıya indirir.
- Kapasite çalışma kümesinin altındayken tahliye sayısı ıska sayısına yaklaşır; 68 anahtarlık
  kümede kapasite 32'den 64'e çıkınca isabet oranı %52,6'dan %92,5'e sıçradı.
- Tahliye sayacı kapasite kararının doğrudan ölçütüdür: ıskaya yakınsa önbellek kendi
  girdilerini yiyordur, sıfıra yakınsa kapasite yeterlidir.
- Tek bir toplam oran ad alanı farklarını gizler: %62,8'lik toplamın içinde %6,2'lik bir ad
  alanı vardı ve o ad alanı önbellekten çıkarıldığında kaynak sorgusu 743'ten 449'a indi.
- İstek isabet oranı kaynağa giden çağrı sayısını, bayt isabet oranı ağdan geçmeyen veri
  miktarını ölçer; aynı koşuda %62,8 ve %51,8 çıktılar.

#### Sonraki Adım

Bu konu boyunca yapılan iş tek bir cümleyle özetlenebilir: aynı yanıtın yeniden üretilmesi
önlendi. Kapasite ölçümündeki 1867 kaynak sorgusu 68'e indi; okuma yolu gerçekten kısaldı.

Kısalmayan bir yol kaldı. Önbellek yalnız **okumaya** yarar; yazma her zaman kaynağa gider ve
isteğin süresine eklenir. Yazmanın yanına iliştirilmiş işler de öyledir: ödünç verildiğinde
gönderilen bildirim, gün sonunda üretilen gecikme raporu, yüklenen kapak görselinin
işlenmesi. Bunların hiçbiri kullanıcının yanıtı için gerekli değildir, ama hepsi yanıtın
gelmesini geciktirir. İstek yolunu kısaltmanın ikinci yolu işi hızlandırmak değil, işi
istekten **çıkarmaktır**. Sonraki ders bu yolu açar: bir işin eşzamansız yürütülmesinin ne
zaman doğru olduğunu, isteğin süresinden neyin düştüğünü ve karşılığında hangi yeni
sorumlulukların doğduğunu ele alır.

## Mesajlaşma

İşin istekten çıkarılması ve iki uç arasında mesajla taşınması: kuyruk, yayımla–abone ol ve akış modellerinin ayrımı, teslim semantiğinin ölçümü, sıralama ile bölümleme arasındaki ödünleşim, ölü mektup kuyruğu ve yeniden deneme stratejileri.

### Eşzamansız İşlemenin Gerekçesi

Source: https://academia.sh/tr/kurslar/eszamansiz-isleme/eszamansiz-islemenin-gerekcesi

İstek yolunun işi çıkararak kısaltılması: adım maliyetlerinin ve başarı olasılığının ölçülmesi, isteğin içinde kalması zorunlu iş ile ertelenebilir işin ayrılması, yanıtın anlamının değişmesi ve devrin bellekte yapılmasıyla giden kutusuna yazılması arasındaki dayanıklılık farkı.

Önceki ders önbellek isabet oranını ölçtü ve okuma yolunun ne kadar kısaldığını sayıyla
gösterdi. Aynı ders bir sınırı da işaretledi: önbellek yalnız okumaya yarar. Ödünç verme
isteği bir yazmadır, üstelik yazmanın yanında sürüklediği başka işler vardır — üyeye
gecikme bildirimi, şube stok özetinin yeniden hesaplanması, aylık rapora bir satır. Bunların
hiçbiri önbelleklenemez ve hepsi isteğin süresine eklenir.

İstek yolunu kısaltmanın ikinci yolu işi hızlandırmak değil, işi istekten **çıkarmaktır**.
Bu ders üç soruyu sırayla yanıtlar: isteğin içinde ne kalmak zorundadır, ne çıkarılabilir,
çıkarmanın bedeli nedir.

#### İsteğin İçindeki İş

Ödünç verme isteğinin beş adımı var. Her adımın maliyeti aşağıdaki modelde bildirilmiştir;
bu değerler ölçülmüş süreler değil, adımların birbirine göre büyüklüğünü temsil eden model
değerleridir. Asıl ilgilendiğimiz, hangi adımın toplam süreye ne kattığıdır.

```js
// istek-yolu.mjs — odunc verme isteginin adimlari ve her adimin modellenmis maliyeti
const ADIMLAR = [
  { ad: "yetki denetimi",           ms: 3,   yaniti_belirler: true  },
  { ad: "odunc kaydinin yazilmasi", ms: 9,   yaniti_belirler: true  },
  { ad: "gecikme bildirimi",        ms: 140, yaniti_belirler: false },
  { ad: "sube stok ozetinin yeniden hesabi", ms: 260, yaniti_belirler: false },
  { ad: "aylik rapor satiri",       ms: 55,  yaniti_belirler: false },
];

let birikim = 0;
for (const a of ADIMLAR) {
  birikim += a.ms;
  const isaret = a.yaniti_belirler ? "zorunlu " : "ertelenebilir";
  console.log(`${a.ad.padEnd(40)} ${String(a.ms).padStart(4)} ms  ${isaret}  birikim=${birikim} ms`);
}

const toplam = ADIMLAR.reduce((t, a) => t + a.ms, 0);
const zorunlu = ADIMLAR.filter((a) => a.yaniti_belirler).reduce((t, a) => t + a.ms, 0);
console.log(`\nistek suresi (hepsi istekte)      = ${toplam} ms`);
console.log(`istek suresi (yalniz zorunlu is)  = ${zorunlu} ms`);
console.log(`kisalma orani                     = ${(1 - zorunlu / toplam).toFixed(3)}`);

// Tek is parcasi saniyede kac istek bitirebilir?
console.log(`tek is parcasinin verimi: ${(1000 / toplam).toFixed(1)} -> ${(1000 / zorunlu).toFixed(1)} istek/s`);

// Her adim bir dis bagimliliktir; istegin basarisi hepsinin ayni anda calismasina baglidir.
const p = 0.999;
const hepsi = p ** ADIMLAR.length, azi = p ** ADIMLAR.filter((a) => a.yaniti_belirler).length;
console.log(`istek basari olasiligi: ${hepsi.toFixed(5)} -> ${azi.toFixed(5)}`);
```

```sh
node istek-yolu.mjs
```

```
yetki denetimi                              3 ms  zorunlu   birikim=3 ms
odunc kaydinin yazilmasi                    9 ms  zorunlu   birikim=12 ms
gecikme bildirimi                         140 ms  ertelenebilir  birikim=152 ms
sube stok ozetinin yeniden hesabi         260 ms  ertelenebilir  birikim=412 ms
aylik rapor satiri                         55 ms  ertelenebilir  birikim=467 ms

istek suresi (hepsi istekte)      = 467 ms
istek suresi (yalniz zorunlu is)  = 12 ms
kisalma orani                     = 0.974
tek is parcasinin verimi: 2.1 -> 83.3 istek/s
istek basari olasiligi: 0.99501 -> 0.99800
```

Üç sayı okunmaya değer. Birincisi süre: isteğin işini yapan iki adım 12 milisaniye tutuyor,
geri kalan 455 milisaniye üyenin yanıtı beklediği ölü zaman. İkincisi verim: aynı iş
parçası bu ölü zamanı taşımadığında birim zamanda kırk kat fazla istek bitiriyor. Üçüncüsü
başarı olasılığı: her adım ayrı bir dış bağımlılıktır ve isteğin başarılı olması hepsinin
aynı anda çalışmasına bağlıdır. Beş bağımlılık iki bağımlılığa indiğinde başarısızlık oranı
binde beşten binde ikiye düşüyor. Rapor sunucusunun erişilemez olması artık ödünç vermeyi
engellemiyor.

#### Zorunlu İş ile Ertelenebilir İş

Ayrımı yapan ölçüt hız değildir. Gecikme bildirimi hızlı olsaydı da isteğin içinde kalması
gerekmezdi; asıl soru şudur: **bu adım yapılmazsa yanıt yanlış olur mu?**

Üç durumda iş isteğin içinde kalır. Adım yanıtın içeriğini belirliyorsa — ödünç kimliği
yanıtta dönüyorsa kaydın yazılması ertelenemez. Adım isteğin reddine yol açabiliyorsa —
yetki denetimi ya da stok kısıtı sonradan çalıştırılamaz, çünkü üyeye "verildi" denmiş
olurdu. Adım bir değişmezi koruyorsa — kitabın aynı anda iki üyeye verilmemesi kuralı ödünç
kaydıyla aynı işlem sınırının içinde kalmak zorundadır; İşlem Sınırları dersinde kurulan
kural budur.

Kalan iş ertelenebilir. Ertelenebilir işin ortak özelliği, **sonunda** yapılmasının
yetmesidir: bildirim iki saniye sonra gitse de doğrudur, stok özeti bir dakika geriden
gelse de rapor doğruyu gösterir. İş Mantığı Yerleşimi konusunda alan olaylarının dinleyicisi
için konan ölçüt aynıdır: dinleyiciye verilen iş, yapılmasa da ödünç kaydının geçerli kaldığı
iştir.

#### Devrin Bedeli: Yanıtın Anlamı Değişir

İş çıkarıldığında yanıt artık "yapıldı" demez, "kabul edildi" der. API Tasarımı kursunun
Eşzamanlı ve Eşzamansız API'ler dersinde kurulan kalıp burada karşılığını bulur: sunucu
`202` döndürür ve işin izleneceği kaynağı bildirir. Bu değişimin üç sonucu vardır.

İstemci işin bittiğini yanıttan öğrenemez; sormak ya da bildirilmek zorundadır. Hata artık
istemciye dönmez; ertelenmiş iş başarısız olduğunda ortada yanıtını bekleyen kimse yoktur,
dolayısıyla hatanın görülmesi ayrı bir düzenek ister. Ve iki uç arasında bir **mesaj** doğar:
bir tarafın yazdığı, başka bir tarafın sonra okuyacağı kayıt.

Mesajın nereye yazıldığı, bu dersin son ölçümünün konusudur.

#### Devir Dayanıklı Olmalı

İşi istekten çıkarmanın en kısa yolu, onu aynı süreçte sonraya bırakmaktır: bir diziye koy,
yanıtı döndür, sonra işle. Ölçüm bunun neyi kaybettiğini gösterir. Aşağıdaki kurulum Veri Erişim Katmanı ve İş Mantığı
kursunda kullanılan tabloların aynısını kurar.

```sh
# kur.sh — kutuphane veritabanini sifirdan kurar
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE sube (sube_id INTEGER PRIMARY KEY, ad TEXT NOT NULL);
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, sube_id INTEGER NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                    uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);
CREATE TABLE giden_kutusu (ileti_id INTEGER PRIMARY KEY, tur TEXT NOT NULL,
                           govde TEXT NOT NULL, durum TEXT NOT NULL DEFAULT 'bekliyor');
INSERT INTO sube VALUES (1,'Merkez'),(2,'Bahcelievler');
INSERT INTO uye VALUES (1,'Ayse Demir'),(2,'Bora Kaya'),(3,'Ceren Ak');
INSERT INTO kitap VALUES (1,'Korluk',1),(2,'Kum Kitabi',1),(3,'Puslu Kitalar',2);
SQL
```

İlk sürüm yan işleri bellekteki bir diziye bırakır. Sürecin dizi boşaltılmadan sonlanması
`process.exit` ile temsil edilmiştir; gerçek karşılığı dağıtımın yeni sürümü yerleştirmesi
ya da sürecin çökmesidir.

```js
// bellekte.mjs — yan isler bellekteki bir diziye birakilir; surec bosaltmadan sonlanir
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");
const yanIsler = [];

function oduncVer(uyeId, kitapId) {
  db.exec("BEGIN IMMEDIATE");
  const oduncId = Number(db.prepare(
    "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi) VALUES (?,?,'2025-07-20')")
    .run(kitapId, uyeId).lastInsertRowid);
  db.exec("COMMIT");
  for (const tur of ["gecikme_bildirimi", "stok_ozeti", "rapor_satiri"])
    yanIsler.push({ tur, oduncId });
  return oduncId;
}

for (const [uyeId, kitapId] of [[1, 1], [2, 2], [3, 3]]) oduncVer(uyeId, kitapId);
console.log(`istek bitti; bellekte bekleyen yan is = ${yanIsler.length}`);
process.exit(0);   // surec yeniden baslatildi: dizi bosaldi
```

İkinci sürüm aynı yan işleri ödünç kaydıyla **aynı işlemde** giden kutusuna yazar. Giden
kutusu düzeni Alan Olayları dersinde tanıtılmıştı; buradaki kullanımı aynıdır, yalnız
yazılan şey bir alan olayı değil, yapılacak bir iştir.

```js
// giden-kutusuyla.mjs — yan isler odunc kaydiyla ayni islemde giden kutusuna yazilir
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");

function oduncVer(uyeId, kitapId) {
  db.exec("BEGIN IMMEDIATE");
  const oduncId = Number(db.prepare(
    "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi) VALUES (?,?,'2025-07-20')")
    .run(kitapId, uyeId).lastInsertRowid);
  for (const tur of ["gecikme_bildirimi", "stok_ozeti", "rapor_satiri"])
    db.prepare("INSERT INTO giden_kutusu (tur, govde) VALUES (?,?)")
      .run(tur, JSON.stringify({ oduncId, uyeId, kitapId }));
  db.exec("COMMIT");
  return oduncId;
}

for (const [uyeId, kitapId] of [[1, 1], [2, 2], [3, 3]]) oduncVer(uyeId, kitapId);
console.log("istek bitti; yan isler giden kutusunda");
process.exit(0);   // surec yeniden baslatildi
```

Yeniden başlatılmış süreç geriye ne kaldığını sayar.

```js
// sayim.mjs — yeniden baslatilmis surec: geriye ne kaldi
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");
const say = (s) => db.prepare(s).get().n;
console.log(`odunc satiri = ${say("SELECT count(*) AS n FROM odunc")}, ` +
            `kurtarilabilir yan is = ${say("SELECT count(*) AS n FROM giden_kutusu WHERE durum = 'bekliyor'")}`);
```

```sh
sh kur.sh
node bellekte.mjs
node sayim.mjs
sh kur.sh
node giden-kutusuyla.mjs
node sayim.mjs
```

```
istek bitti; bellekte bekleyen yan is = 9
odunc satiri = 3, kurtarilabilir yan is = 0
istek bitti; yan isler giden kutusunda
odunc satiri = 3, kurtarilabilir yan is = 9
```

Her iki sürümde de üç ödünç kaydı yazıldı ve üç üye yanıtını hızlı aldı. Fark kurtarılabilir
yan iş sayısındadır: bellekte biriken dokuz iş süreçle birlikte yok oldu, giden kutusuna
yazılan dokuz iş yeniden başlatılan süreç tarafından bulundu. Üç üye bildirim almadı ve
kimse bunu fark etmedi — çünkü isteğe dönen yanıt zaten başarılıydı.

Buradan çıkan kural, eşzamansız işlemenin temel koşuludur: **iş istekten çıkarılıyorsa,
çıkarıldığı yerin kalıcı olması gerekir.** İşin devri, yapıldığı işlemle aynı atomikliği
paylaşmalıdır; yoksa kayıp sessiz olur.

#### Ne Zaman Çıkarılmaz

Karşı yönde de bir maliyet vardır. Çıkarılan iş, isteği yapan için görünmez olur: üye
kitabı aldığını görür, bildirimin gelip gelmediğini bilmez. İşin durumu ayrı bir kaynakla
izlenmiyorsa hata kimsenin görmediği bir yerde birikir.

İkinci maliyet gecikmedir. Yan iş "sonunda" yapılır, ama o "sonunda" ölçülmüyorsa
sınırsızdır. Üç ölçüt işi isteğin içinde tutmayı haklı kılar: yanıtın içeriği o işe
bağlıysa, iş bir değişmezi koruyorsa, ya da işin gecikmesi kullanıcı için hatadan ayırt
edilemiyorsa.

Üçüncüsü karmaşıklıktır. Bellekte iki satırla yapılan devir, kalıcı hâle geldiğinde bir
tablo, bir okuyucu döngü, bir yeniden deneme kuralı ve bir hata yalıtımı gerektirir. Bu
dersten sonrası, o dört parçanın nasıl kurulduğudur.

#### Özet

- İstek yolundaki beş adımdan ikisi yanıtı belirliyordu; kalan üçü çıkarıldığında modelde
  istek süresi 467 milisaniyeden 12 milisaniyeye, dış bağımlılık sayısı beşten ikiye indi.
- Aynı iş parçasının verimi 2,1 istek/s değerinden 83,3 istek/s değerine, istek başarı
  olasılığı 0,99501 değerinden 0,99800 değerine çıktı.
- Bir adım yanıtın içeriğini belirliyor, isteğin reddine yol açabiliyor ya da bir değişmezi
  koruyorsa isteğin içinde kalır; kalan iş ertelenebilir.
- İş çıkarıldığında yanıtın anlamı "yapıldı" olmaktan çıkıp "kabul edildi" olur; hata artık
  istemciye dönmediği için görünürlüğü ayrı kurulur.
- Yan işler bellekte biriktirildiğinde süreç sonlanınca dokuzu da kayboldu; aynı işlemde
  giden kutusuna yazıldığında dokuzu da yeniden başlatılan süreçte bulundu.

#### Sonraki Adım

Giden kutusu işi kalıcı kıldı, ama tek başına bir taşıma düzeneği değil. Satırları kim
okuyacak, iki okuyucu aynı satırı aynı anda alırsa ne olacak, bir okuyucu satırı alıp
işlemeden çökerse o iş nasıl geri dönecek? Bunlar tek bir yapının soruları: iki uç arasında
mesaj taşıyan **kuyruk**. Sonraki ders kuyruğu en küçük hâliyle kurar — üretici, tüketici,
onay ve görünmezlik süresi — ve her mesajın kaç kez teslim edildiğini sayarak teslim
garantisinin ne anlama geldiğini ölçer.

### Mesaj Kuyrukları

Source: https://academia.sh/tr/kurslar/eszamansiz-isleme/mesaj-kuyruklari

Kuyruğun en küçük hâlinin kurulması: yazma, alma, onay ve görünmezlik süresi, onaylanmayan iletinin yeniden teslim edilmesinin ölçülmesi, rekabet eden tüketicilerin tek iletiyi tek tarafa vermesi ve kuyruk derinliğinin üretim ile tüketim hızına bağlı davranışı.

Önceki ders yan işleri ödünç kaydıyla aynı işlemde giden kutusuna yazdı ve dokuz işin
süreç yeniden başladıktan sonra hâlâ orada olduğunu gösterdi. Orada bıraktığımız sorular
şunlardı: bu satırları kim okuyacak, iki okuyucu aynı satırı aynı anda alırsa ne olacak,
bir okuyucu satırı alıp işlemeden çökerse iş nasıl geri dönecek?

Üçünün de yanıtı tek bir yapıdadır. **Mesaj kuyruğu** (message queue), bir tarafın yazdığı
ve başka bir tarafın okuduğu, okunanın ne zaman silineceğine okuyanın karar verdiği kalıcı
bir listedir. Bu ders kuyruğu en küçük hâliyle kurar ve her iletinin kaç kez teslim
edildiğini sayar.

#### Kuyruğun Dört İşlemi

Yazan tarafa **üretici** (producer), okuyan tarafa **tüketici** (consumer) denir. Aralarındaki
sözleşme dört işlemden oluşur. Üretici `yaz` ile iletiyi kuyruğa bırakır. Tüketici `al` ile
bir ileti ister. İş bitince `onayla` ile iletinin silinmesini söyler. Dördüncüsü işlem değil,
bir süredir: alınan ileti onaylanana kadar başka tüketiciye verilmez, ama bu koruma sonsuza
kadar sürmez — **görünmezlik süresi** (visibility timeout) dolduğunda ileti yeniden görünür
olur.

Aşağıdaki model bu dördünü tutar. Zaman gerçek saatten değil, açıkça ilerletilen bir sanal
saatten okunur; böylece ölçümler makineden bağımsız olur.

```js
// kuyruk.mjs — onay ve gorunmezlik suresi olan en kucuk kuyruk modeli
export class Kuyruk {
  constructor(gorunmezlik = 30) {
    this.gorunmezlik = gorunmezlik;   // alinan ileti kac ms boyunca baskasina verilmez
    this.iletiler = [];               // {id, govde, gorunur, teslim}
    this.sonrakiId = 1;
    this.simdi = 0;                   // sanal saat: gercek zamana bagli degil
    this.toplamTeslim = 0;
  }
  yaz(govde) {                        // uretici tarafi
    this.iletiler.push({ id: this.sonrakiId++, govde, gorunur: 0, teslim: 0 });
    return this.iletiler.at(-1).id;
  }
  al() {                              // tuketici tarafi
    const i = this.iletiler.find((m) => m.gorunur <= this.simdi);
    if (i === undefined) return null;
    i.gorunur = this.simdi + this.gorunmezlik;
    i.teslim += 1;
    this.toplamTeslim += 1;
    return { id: i.id, govde: i.govde, teslim: i.teslim };
  }
  onayla(id) {                        // is bitti: ileti kuyruktan silinir
    const once = this.iletiler.length;
    this.iletiler = this.iletiler.filter((m) => m.id !== id);
    return once !== this.iletiler.length;
  }
  ilerlet(ms) { this.simdi += ms; }
  get derinlik() { return this.iletiler.length; }
  get gorunur() { return this.iletiler.filter((m) => m.gorunur <= this.simdi).length; }
}
```

Beklenen durumda üretici beş iş yazar, tek tüketici hepsini alıp onaylar.

```js
// mutlu-yol.mjs — bes is yazilir, tek tuketici hepsini alip onaylar
import { Kuyruk } from "./kuyruk.mjs";
const k = new Kuyruk();
const ISLER = [
  "gecikme_bildirimi odunc=1", "gecikme_bildirimi odunc=2", "stok_ozeti sube=1",
  "rezervasyon_ilerlet kitap=3", "rapor_satiri odunc=1",
];
for (const is of ISLER) k.yaz(is);
console.log(`uretildi=${ISLER.length}  kuyruk derinligi=${k.derinlik}`);

let islenen = 0;
for (let ileti = k.al(); ileti !== null; ileti = k.al()) {
  islenen += 1;
  k.onayla(ileti.id);
}
console.log(`islenen=${islenen}  toplam teslim=${k.toplamTeslim}  kalan derinlik=${k.derinlik}`);
```

```sh
node mutlu-yol.mjs
```

```
uretildi=5  kuyruk derinligi=5
islenen=5  toplam teslim=5  kalan derinlik=0
```

Beş iş, beş teslim, sıfır kalan. Üretici ile tüketici arasında hiçbir doğrudan çağrı yok:
üretici tüketicinin çalışıp çalışmadığını bilmiyor, tüketici işi kimin ürettiğini bilmiyor.
Kuyruk ikisini **zamanda** ayırır — bu, alan olaylarındaki gevşek bağlanmanın üstüne eklenen
ikinci bir ayrımdır.

#### Onay Neden Ayrı Bir Adım

`al` iletiyi silmez. Silseydi, işi alan tüketicinin çökmesi işin kaybolması demek olurdu.
Bunun yerine ileti geçici olarak görünmez yapılır ve silme kararı onaya bırakılır. Aşağıdaki
ölçüm bir tüketicinin ikinci işi onaylamadan sonlandığı durumu koşturur.

```js
// onaysiz.mjs — onaylanmayan ileti gorunmezlik suresi dolunca yeniden teslim edilir
import { Kuyruk } from "./kuyruk.mjs";
const k = new Kuyruk(30);
for (const is of ["gecikme_bildirimi odunc=1", "stok_ozeti sube=1", "rapor_satiri odunc=1"])
  k.yaz(is);

// Birinci tuketici: ikinci isi alirken sureci sonlaniyor, onay gondermiyor.
const cokecek = new Set(["stok_ozeti sube=1"]);
for (let ileti = k.al(); ileti !== null; ileti = k.al()) {
  const durum = cokecek.has(ileti.govde) ? "onaysiz kaldi" : "onaylandi";
  if (durum === "onaylandi") k.onayla(ileti.id);
  console.log(`t=${k.simdi} tuketici-1 ileti=${ileti.id} teslim=${ileti.teslim} ${durum}`);
}
console.log(`t=${k.simdi} derinlik=${k.derinlik} gorunur=${k.gorunur}`);

k.ilerlet(30);                       // gorunmezlik suresi doldu
console.log(`t=${k.simdi} derinlik=${k.derinlik} gorunur=${k.gorunur}`);

// Ikinci tuketici ayni isi bulur.
for (let ileti = k.al(); ileti !== null; ileti = k.al()) {
  k.onayla(ileti.id);
  console.log(`t=${k.simdi} tuketici-2 ileti=${ileti.id} teslim=${ileti.teslim} onaylandi`);
}
console.log(`toplam teslim=${k.toplamTeslim}  yazilan ileti=3  kalan derinlik=${k.derinlik}`);
```

```sh
node onaysiz.mjs
```

```
t=0 tuketici-1 ileti=1 teslim=1 onaylandi
t=0 tuketici-1 ileti=2 teslim=1 onaysiz kaldi
t=0 tuketici-1 ileti=3 teslim=1 onaylandi
t=0 derinlik=1 gorunur=0
t=30 derinlik=1 gorunur=1
t=30 tuketici-2 ileti=2 teslim=2 onaylandi
toplam teslim=4  yazilan ileti=3  kalan derinlik=0
```

Üç ileti yazıldı, dört teslim yapıldı. İkinci ileti iki kez teslim edildi çünkü ilk teslim
onaysız kaldı. Ölçümün en önemli satırı ortadaki ikisidir: onaysız ileti kuyrukta duruyordu
(`derinlik=1`) ama görünür değildi (`gorunur=0`); süre dolunca görünür oldu. Görünmezlik
süresi, "bu iş şu an başka biri tarafından yapılıyor" varsayımının son kullanma tarihidir.

Sürenin seçimi bir ödünleşmedir. İşin en uzun süresinden kısa seçilirse, iş hâlâ sürerken
ileti ikinci bir tüketiciye verilir ve aynı iş iki kez yapılır. Çok uzun seçilirse, gerçekten
çöken bir tüketicinin bıraktığı iş uzun süre kimseye ulaşmaz. Uzun süren işler için doğru
çözüm süreyi büyütmek değil, tüketicinin çalışırken süreyi uzatmasıdır.

Buradan kuyruğun temel garantisi çıkar: **onaylanmayan iş kaybolmaz, ama kaybolmaması
yinelenebilmesi pahasınadır.** Teslim sayısı iletinin işlenme sayısı değildir; yalnız
kuyruğun onu kaç kez verdiğidir.

#### Rakip Tüketiciler

Kuyruğun ikinci temel özelliği, aynı kuyruktan okuyan birden çok tüketicinin işi
**paylaşmasıdır**. Bu düzene rekabet eden tüketiciler denir: her ileti tam olarak bir tüketiciye
gider, çünkü alınan ileti diğerleri için görünmez olur.

```js
// rakip-tuketiciler.mjs — ayni kuyruktan okuyan uc tuketici; her ileti tek tuketiciye gider
import { Kuyruk } from "./kuyruk.mjs";
const k = new Kuyruk();
for (let i = 1; i <= 12; i++) k.yaz(`gecikme_bildirimi odunc=${i}`);

const alan = new Map();                       // ileti -> onu isleyen tuketici
const sayac = { "tuketici-1": 0, "tuketici-2": 0, "tuketici-3": 0 };
const adlar = Object.keys(sayac);
for (let sira = 0; k.derinlik > 0; sira++) {
  const ad = adlar[sira % adlar.length];
  const ileti = k.al();
  if (ileti === null) break;
  alan.set(ileti.id, ad);
  sayac[ad] += 1;
  k.onayla(ileti.id);
}
console.log(`dagitim: ${Object.entries(sayac).map(([a, n]) => `${a}=${n}`).join("  ")}`);
console.log(`farkli ileti sayisi=${alan.size}  toplam teslim=${k.toplamTeslim}  kalan=${k.derinlik}`);
```

```sh
node rakip-tuketiciler.mjs
```

```
dagitim: tuketici-1=4  tuketici-2=4  tuketici-3=4
farkli ileti sayisi=12  toplam teslim=12  kalan=0
```

On iki iş üç tüketiciye dörder dağıldı. Farklı ileti sayısı ile toplam teslim sayısının eşit
olması, hiçbir iletinin iki tüketiciye birden gitmediğini söyler. Gecikme bildirimi işi
üçe bölünebildiği için tüketici eklemek doğrudan kapasite eklemektir; ödünç verme isteğinin
kendisinde böyle bir bölünebilirlik yoktur.

#### Kuyruk Derinliği

Kuyrukta bekleyen ileti sayısına **kuyruk derinliği** denir ve tek bir şeyi ölçer: üretim
hızı ile tüketim hızının farkını. Aşağıdaki koşum her turda beş iş üretir, her tüketici
turda üç iş bitirir.

```js
// derinlik.mjs — uretim hizi tuketim hizini asinca kuyruk derinligi buyur
import { Kuyruk } from "./kuyruk.mjs";

function kosum(tuketiciSayisi, tur = 6, uretim = 5, tuketimHizi = 3) {
  const k = new Kuyruk();
  const derinlikler = [];
  for (let t = 1; t <= tur; t++) {
    for (let i = 0; i < uretim; i++) k.yaz(`is t=${t} n=${i}`);
    for (let i = 0; i < tuketiciSayisi * tuketimHizi; i++) {
      const ileti = k.al();
      if (ileti === null) break;
      k.onayla(ileti.id);
    }
    k.ilerlet(1000);
    derinlikler.push(k.derinlik);
  }
  return derinlikler;
}

for (const n of [1, 2, 3]) {
  const d = kosum(n);
  console.log(`tuketici=${n}  tur sonu derinlikleri: ${d.join(" ")}`);
}
```

```sh
node derinlik.mjs
```

```
tuketici=1  tur sonu derinlikleri: 2 4 6 8 10 12
tuketici=2  tur sonu derinlikleri: 0 0 0 0 0 0
tuketici=3  tur sonu derinlikleri: 0 0 0 0 0 0
```

Tek tüketiciyle derinlik her turda ikişer artıyor; kuyruk işi yutmuyor, yalnız erteliyor.
İki tüketici kapasiteyi altıya çıkardığı için derinlik sıfırda kalıyor, üçüncü tüketici hiçbir
şey eklemiyor. Kuyruk bir hız düzenleyici değildir; ani yükü düzleştirir, sürekli farkı
kapatmaz. Derinliğin zamanla artması tüketici eksikliğinin doğrudan göstergesidir ve
izlenmesi gereken ilk ölçüttür.

#### Kuyruğun Verdiği ve Vermediği

Kuyruk üç şeyi verir: iletinin kalıcılığı, işin bölünebilmesi ve tarafların zamanda
ayrılması. Vermedikleri de aynı ölçüde belirleyicidir.

Tek teslim vermez. Ölçümde gördüğümüz gibi onaylanmayan ileti yeniden teslim edilir; işlemenin
tekrar altında etkisiz olması tüketicinin sorumluluğudur. Etkisiz İşlemler dersinde kurulan
`islenen_ileti` bekçisi tam bu koşul içindir.

Sıra vermez. Rakip tüketiciler işi paylaştığı anda, iki iletinin bitiş sırası yazılma
sırasından farklı olabilir. Aynı kitabın iki olayı iki tüketiciye düşerse ikincisi önce
bitebilir.

Yayın vermez. Bir ileti tam olarak bir tüketiciye gider. Aynı olayın hem bildirim hem rapor
hem de arama dizini tarafından görülmesi gerekiyorsa kuyruk tek başına yetmez.

#### Özet

- Kuyruk dört işlemle tanımlanır: üreticinin yazması, tüketicinin alması, işi bitirince
  onaylaması ve alınan iletinin görünmezlik süresi boyunca başkasına verilmemesi.
- Onaysız kalan ileti kuyrukta durur ama görünmez olur; süre dolunca yeniden görünür oldu ve
  üç yazılan ileti için toplam dört teslim ölçüldü.
- Görünmezlik süresi işin en uzun süresinden kısa seçilirse aynı iş iki kez yapılır, çok uzun
  seçilirse çöken tüketicinin işi uzun süre bekler.
- Rakip tüketicilerde her ileti tek tarafa gider: on iki iş üç tüketiciye dörder dağıldı,
  farklı ileti sayısı ile toplam teslim sayısı eşit çıktı.
- Kuyruk derinliği üretim ile tüketim hızının farkını ölçer; tek tüketicide her turda ikişer
  arttı, ikinci tüketici eklendiğinde sıfırda kaldı, üçüncüsü hiçbir şey değiştirmedi.

#### Sonraki Adım

Kuyruğun vermediklerinden sonuncusu bir sonraki dersin konusudur. Ödünç verme olayı yalnız
bildirim servisini değil, şube stok özetini ve aylık raporu da ilgilendiriyor. Aynı olayı üç
tarafa da ulaştırmak için üç ayrı kuyruğa üç kopya yazmak, üreticiyi yeniden tüketicilerin
listesine bağlar — tam da kaçınmak istediğimiz şeye. Sonraki ders bu bağı koparan yayımla–abone
ol modelini kurar, tek iletinin abone sayısına göre kaç teslime dönüştüğünü sayar ve yavaş
bir abonenin diğerlerini etkileyip etkilemediğini ölçer.

### Yayımla–Abone Ol

Source: https://academia.sh/tr/kurslar/eszamansiz-isleme/yayimla-abone-ol

Tek iletinin birden çok ilgiliye ulaştırılması: konu ile abonelik ayrımı, tek yayımın abone sayısına göre kaç teslime dönüştüğünün sayılması, yavaş abonenin birikiminin diğerlerinden yalıtılması, süzgeçle elenen kopyaların ölçülmesi ve yayımdan sonra kurulan aboneliğin geçmişi görememesi.

Önceki ders kuyruğun vermediklerini üç maddede toplamıştı; sonuncusu yayındı. Bir ileti tam
olarak bir tüketiciye gider, çünkü alınan ileti diğerleri için görünmez olur. Oysa ödünç
verme olayı üç ayrı tarafı ilgilendiriyor: üyeye gecikme bildirimi gönderen taraf, şube stok
özetini yeniden hesaplayan taraf ve aylık rapora satır ekleyen taraf. Aynı olayın üçüne birden
ulaşması gerekiyor.

Bu dersin sorusu, o "üçüne birden"in nereye yazılacağıdır. Yanıt yalnız bir dağıtım
düzeneği değil, aynı zamanda üreticinin kimin dinlediğini bilmemesini sağlayan bir ayrımdır.

#### Üreticinin Üç Hedefi Sorunu

En kısa çözüm, üreticinin üç kuyruğa üç kopya yazmasıdır. Ölçülebilir bir bedeli vardır:
üretici artık üç hedefin adını bilir. Dördüncü ilgili taraf — diyelim ki arama dizini —
eklendiğinde ödünç verme kodunun değişmesi gerekir. Beşincisi eklendiğinde yine değişir.
Kuyruk üretici ile tüketiciyi zamanda ayırmıştı, ama üretici hâlâ tüketicilerin **listesini**
taşıyor.

İş Mantığı Yerleşimi konusunda alan olaylarının çözdüğü bağlanma sorunu budur: yayımlayan
taraf dinleyicileri saymaz, yalnız olduğu şeyi bildirir. Yayımla–abone ol (publish–subscribe),
aynı ayrımı süreç sınırının ötesine taşır. Üretici tek bir **konuya** (topic) yazar; kimin
okuyacağını konu bilir.

#### Konu ve Abonelik

Ayrımın iki adı vardır ve karıştırılmamaları gerekir. **Konu**, iletilerin yazıldığı adlı
kanaldır. **Abonelik** (subscription), bir ilgili tarafın o konudaki kendi teslim sırasıdır.
Yayımlanan her ileti, yayım anında var olan her aboneliğe birer kopya olarak düşürülür;
abonelikler birbirinin ilerlemesini etkilemez.

Aşağıdaki model bu ikisini tutar. Zaman gerçek saatten değil, her abonelik için açıkça
harcanan bir bütçeden okunur; böylece ölçümler makineden bağımsız kalır.

```js
// konu.mjs — konu ve abonelikler: yayimlanan ileti her abonelige birer kopya dusurur
export class Konu {
  constructor() {
    this.abonelikler = new Map();   // ad -> {kuyruk, sure, suzgec, islenen, elenen}
    this.yayin = 0;                 // yayimlanan ileti sayisi
    this.kopya = 0;                 // aboneliklere dusurulen kopya sayisi
    this.teslim = 0;                // aboneler tarafindan islenen kopya sayisi
  }
  aboneOl(ad, { sure = 1, suzgec = () => true } = {}) {
    this.abonelikler.set(ad, { ad, kuyruk: [], sure, suzgec, islenen: 0, elenen: 0 });
  }
  yayimla(tur, govde) {                       // uretici tarafi: tek cagri
    const ileti = { id: ++this.yayin, tur, govde };
    for (const a of this.abonelikler.values()) {
      if (!a.suzgec(tur)) { a.elenen += 1; continue; }
      a.kuyruk.push(ileti);
      this.kopya += 1;
    }
    return ileti.id;
  }
  ilerlet(butce) {                            // her abonelik kendi hizinda ilerler
    for (const a of this.abonelikler.values()) {
      let kalan = butce;
      while (a.kuyruk.length > 0 && kalan >= a.sure) {
        a.kuyruk.shift(); a.islenen += 1; this.teslim += 1; kalan -= a.sure;
      }
    }
  }
  bekleyen(ad) { return this.abonelikler.get(ad).kuyruk.length; }
  islenen(ad) { return this.abonelikler.get(ad).islenen; }
}
```

Üreticinin kaç çağrı yaptığı ile sistemin kaç teslim yaptığı ayrı iki sayıdır. Aşağıdaki
koşum abone sayısını birden dörde çıkarırken üretici tarafını hiç değiştirmez.

```js
// tek-yayin.mjs — tek yayimla cagrisinin abone sayisina gore kac teslime dondugu
import { Konu } from "./konu.mjs";

for (const aboneler of [["bildirim"], ["bildirim", "stok-ozeti"],
                        ["bildirim", "stok-ozeti", "rapor"],
                        ["bildirim", "stok-ozeti", "rapor", "arama-dizini"]]) {
  const k = new Konu();
  for (const ad of aboneler) k.aboneOl(ad);
  for (const n of [1, 2, 3, 4]) k.yayimla("odunc_verildi", { oduncId: n });
  k.ilerlet(1000);
  console.log(`abone=${aboneler.length}  yayimla cagrisi=${k.yayin}  ` +
              `dusurulen kopya=${k.kopya}  islenen teslim=${k.teslim}`);
}
```

```sh
node tek-yayin.mjs
```

```
abone=1  yayimla cagrisi=4  dusurulen kopya=4  islenen teslim=4
abone=2  yayimla cagrisi=4  dusurulen kopya=8  islenen teslim=8
abone=3  yayimla cagrisi=4  dusurulen kopya=12  islenen teslim=12
abone=4  yayimla cagrisi=4  dusurulen kopya=16  islenen teslim=16
```

Dört satırın hepsinde yayım çağrısı dört; düşürülen kopya sayısı abone sayısıyla çarpılıyor.
Bu çarpım yayımla–abone ol modelinin hem gücü hem maliyetidir. Gücü: üretici dört aboneli
durumda da tek satır yazar, ilgili taraf eklemek üreticiye dokunmaz. Maliyeti: sistemin
taşıdığı iş, yayım sayısı çarpı abone sayısı kadar büyür. Yayma (fan-out) çarpanı, iletiyi
üretmenin değil taşımanın maliyetini belirler.

Aboneliğin **kendi** teslim sırası olması, önceki dersteki rekabet eden tüketici düzenini geçersiz
kılmaz; onunla birleşir. Bir abonelik içinde birden çok işçi çalışabilir ve o aboneliğe düşen
kopyaları aralarında paylaşır. İki düzenin ayrımı tek cümleyle tutulur: abonelikler arasında
**kopyalama**, abonelik içinde **paylaşma** vardır.

#### Yavaş Abone Diğerlerini Bekletir mi

Ayrı teslim sırasının asıl sınavı budur. Aylık rapor abonesi diğerlerinden yavaş çalışsın:
her iletiyi işlemesi üç zaman birimi sürsün, diğerlerininki bir. Her tur dört olay yayımlanır
ve her abonelik turda altı zaman birimi harcayabilir.

```js
// yavas-abone.mjs — rapor abonesi digerlerinden uc kat yavas; birikim abonelik basina olculur
import { Konu } from "./konu.mjs";
const k = new Konu();
k.aboneOl("bildirim", { sure: 1 });
k.aboneOl("stok-ozeti", { sure: 1 });
k.aboneOl("rapor", { sure: 3 });

const satir = (...v) => v.map((x) => String(x).padStart(12)).join("");
console.log(satir("tur", "bildirim", "stok-ozeti", "rapor"), "  (islenen/bekleyen)");
for (let tur = 1; tur <= 6; tur++) {
  for (let i = 0; i < 4; i++) k.yayimla("odunc_verildi", { tur, i });
  k.ilerlet(6);                       // her abonelik turda 6 zaman birimi harcayabilir
  const g = (ad) => `${k.islenen(ad)}/${k.bekleyen(ad)}`;
  console.log(satir(tur, g("bildirim"), g("stok-ozeti"), g("rapor")));
}
```

```sh
node yavas-abone.mjs
```

```
         tur    bildirim  stok-ozeti       rapor   (islenen/bekleyen)
           1         4/0         4/0         2/2
           2         8/0         8/0         4/4
           3        12/0        12/0         6/6
           4        16/0        16/0         8/8
           5        20/0        20/0       10/10
           6        24/0        24/0       12/12
```

Altı turun sonunda bildirim ve stok özeti yirmi dörder iletiyi bitirmiş, hiçbiri beklemiyor.
Rapor abonesi on iki iletide kalmış ve on iki ileti biriktirmiş. Birikim yalnız yavaş
aboneliktedir; hızlı olanların ilerlemesi hiç yavaşlamamıştır.

Bu yalıtımı sağlayan şey abonelik başına ayrı sıradır. Üç abone tek bir sırayı paylaşsaydı,
en yavaş olan diğerlerinin önünde durur ve tüm konunun hızını belirlerdi. Ayrı sıralar bunu
önler, ama sorunu yok etmez: yavaş abonenin birikimi bellekte ya da diskte gerçek bir yer
kaplar ve büyümesi sınırsızdır. Önceki dersin kuyruk derinliği ölçütü burada abonelik başına
izlenir; hangi abonenin geride kaldığı toplam sayıya bakarak görülmez.

#### Her Abone Her İletiyi İstemez

Konuya yalnız ödünç verme olayı yazılmaz. İade, üye kaydı ve şube eklenmesi de aynı kanaldan
geçer. Bildirim abonesi üye kaydıyla ilgilenmez, rapor abonesi yalnız ödünç verme satırı
yazar. İlgilenmediği iletiyi alıp atmak, aboneliğe düşürülen her kopyanın yazma ve okuma
maliyetini boşa harcamak demektir.

Aboneliğe bir **süzgeç** verilir: iletinin türüne bakarak kopyanın düşürülüp
düşürülmeyeceğine karar eden koşul. Süzgecin yayım tarafında çalışması belirleyicidir; elenen
ileti aboneliğe hiç yazılmaz.

```js
// suzgec.mjs — abonelik yalniz ilgilendigi turleri alir; elenen ileti kopyalanmaz
import { Konu } from "./konu.mjs";
const TURLER = ["odunc_verildi", "iade_alindi", "uye_kaydedildi", "odunc_verildi",
                "sube_eklendi", "iade_alindi", "uye_kaydedildi", "odunc_verildi"];

function kosum(suzgecli) {
  const k = new Konu();
  const s = (kume) => suzgecli ? (t) => kume.has(t) : () => true;
  k.aboneOl("bildirim", { suzgec: s(new Set(["odunc_verildi", "iade_alindi"])) });
  k.aboneOl("stok-ozeti", { suzgec: s(new Set(["odunc_verildi", "iade_alindi", "sube_eklendi"])) });
  k.aboneOl("rapor", { suzgec: s(new Set(["odunc_verildi"])) });
  for (const t of TURLER) k.yayimla(t, {});
  k.ilerlet(1000);
  return k;
}

for (const suzgecli of [false, true]) {
  const k = kosum(suzgecli);
  console.log(`suzgec=${suzgecli ? "var" : "yok"}  yayin=${k.yayin}  dusurulen kopya=${k.kopya}  ` +
              `bildirim=${k.islenen("bildirim")} stok-ozeti=${k.islenen("stok-ozeti")} ` +
              `rapor=${k.islenen("rapor")}`);
}
```

```sh
node suzgec.mjs
```

```
suzgec=yok  yayin=8  dusurulen kopya=24  bildirim=8 stok-ozeti=8 rapor=8
suzgec=var  yayin=8  dusurulen kopya=14  bildirim=5 stok-ozeti=6 rapor=3
```

Sekiz yayım, süzgeçsiz durumda yirmi dört kopya üretiyor; süzgeçle on dört. Rapor abonesi
sekiz yerine üç ileti işliyor ve atacağı beş iletiyi hiç görmüyor. Süzgecin bir de gizli
faydası vardır: aboneliğin ne istediği kodun içinde değil, aboneliğin tanımında durur.
Bildirim abonesinin hangi olaylarla ilgilendiği, tüketici kodunu okumadan görülür.

Süzgecin sınırı, karar verebilmek için iletinin **başlığında** ne olduğuna bağlıdır.
Türe bakan bir süzgeç ucuzdur. Gövdenin içindeki bir alana bakan süzgeç — "yalnız Merkez
şubesinin ödünçleri" — iletinin çözülmesini gerektirir ve maliyeti abonelik sayısıyla çarpılır.
Bu yüzden ayrım ölçütü genelde iletinin gövdesine değil, yayım anında verilen türe ya da
konuya konur.

#### Yayımla–Abone Ol'un Sınırı: Geçmiş Yoktur

Modelin en belirleyici özelliği ölçümde açığa çıkar. Kopya, **yayım anında** var olan
aboneliklere düşürülür. O anda olmayan bir abonelik için ileti hiç yazılmamıştır.

```js
// gec-abone.mjs — abonelik yayimdan sonra kurulursa onceki iletiler o abonelige hic dusmez
import { Konu } from "./konu.mjs";
const k = new Konu();
k.aboneOl("bildirim");
k.aboneOl("stok-ozeti");

for (const n of [1, 2, 3]) k.yayimla("odunc_verildi", { oduncId: n });
k.ilerlet(1000);
console.log(`uc yayin sonra : bildirim=${k.islenen("bildirim")} stok-ozeti=${k.islenen("stok-ozeti")}`);

k.aboneOl("arama-dizini");            // yeni ilgili taraf sonradan baglandi
for (const n of [4, 5]) k.yayimla("odunc_verildi", { oduncId: n });
k.ilerlet(1000);
console.log(`bes yayin sonra: bildirim=${k.islenen("bildirim")} stok-ozeti=${k.islenen("stok-ozeti")} ` +
            `arama-dizini=${k.islenen("arama-dizini")}`);
console.log(`toplam yayin=${k.yayin}  arama-dizininin goremedigi=${k.yayin - k.islenen("arama-dizini")}`);
```

```sh
node gec-abone.mjs
```

```
uc yayin sonra : bildirim=3 stok-ozeti=3
bes yayin sonra: bildirim=5 stok-ozeti=5 arama-dizini=2
toplam yayin=5  arama-dizininin goremedigi=3
```

Arama dizini beş ödünç olayının ikisini gördü. Kaçırdığı üçü bir yerde bekliyor değil; o
kopyalar hiç var olmadı. Aynı boşluk ikinci bir biçimde de ortaya çıkar: işlenmiş bir kopya
aboneliğin sırasından silinir, dolayısıyla arama dizini bir hata yüzünden kendi verisini
kaybederse gördüğü iletileri de yeniden okuyamaz.

Bu, modelin kusuru değil tanımıdır. Yayımla–abone ol iletiyi **dağıtır**, saklamaz. Saklamak
isteyen abone, aldığını kendi tarafında biriktirmek zorundadır. Yeni bir ilgili tarafın
geçmişe bakarak kendini kurabilmesi ya da bir abonenin geriye dönüp yeniden okuyabilmesi
gerekiyorsa, ihtiyaç duyulan şey dağıtım değil, **kalıcı bir kayıttır**.

#### Özet

- Konu iletilerin yazıldığı adlı kanaldır; abonelik bir ilgili tarafın o konudaki kendi
  teslim sırasıdır. Abonelikler arasında kopyalama, abonelik içinde paylaşma olur.
- Dört yayım çağrısı, abone sayısı birden dörde çıktığında dört yerine on altı kopyaya
  dönüştü; üretici kodu hiç değişmedi. Yayma çarpanı, taşıma maliyetini belirler.
- Ayrı teslim sıraları yavaş aboneyi yalıtır: altı turun sonunda rapor abonesi on iki ileti
  biriktirirken bildirim ve stok özeti yirmi dörder iletiyi bitirmiş ve sıfır bekleyenle
  kalmıştı.
- Süzgeç yayım tarafında çalışır: sekiz yayım süzgeçsiz yirmi dört kopya üretirken süzgeçle
  on dört kopya üretti, rapor abonesi ilgilenmediği beş iletiyi hiç görmedi.
- Kopya yalnız yayım anında var olan aboneliklere düşürülür; sonradan bağlanan arama dizini
  beş olayın üçünü hiç göremedi. Model iletiyi dağıtır, saklamaz.

#### Sonraki Adım

Kuyruk da konu da aynı varsayımı paylaşıyor: teslim edilmiş ileti tüketilmiştir, tüketilmiş
ileti silinir. Bu varsayımın bedeli son ölçümde göründü — sonradan bağlanan taraf geçmişi
göremiyor, hata sonrası hiçbir tüketici geriye dönemiyor. Sonraki ders, tüketmeyi silmekten
ayıran ikinci taşıma modelini kurar: iletilerin sona eklendiği ve okunduğunda yerinde kaldığı
kalıcı sıralı bir günlük. Aynı iş yükü hem kuyrukta hem günlükte koşturulur; yeniden okumanın
neden birinde olanaksız, ötekinde bir konum değerini geri almak kadar ucuz olduğu sayıyla
gösterilir.

### Akışlar ve Kuyruk Farkı

Source: https://academia.sh/tr/kurslar/eszamansiz-isleme/akislar-ve-kuyruk-farki

Tüketmenin silmekten ayrılması: yalnız sona ekleme yapılan günlük, tüketici başına konum, tek kopyanın abonelik başına kopyalarla bayt olarak karşılaştırılması, yeniden okumanın kuyrukta neden olanaksız akışta neden ucuz olduğunun ölçülmesi ve saklama süresinin geride kalan tüketici için anlamı.

Önceki dersin son ölçümü bir boşluk bıraktı: sonradan bağlanan arama dizini beş ödünç
olayının üçünü hiç göremedi, çünkü kopya yalnız yayım anında var olan aboneliklere düşürülür.
Aynı boşluğun ikinci yüzü de vardı — işlenmiş kopya silindiği için hiçbir tüketici geriye
dönüp yeniden okuyamıyordu.

İki eksiğin de kaynağı tek bir varsayımdır: **teslim edilen ileti tüketilmiştir, tüketilen
ileti silinir.** Bu ders o varsayımı bırakan ikinci taşıma modelini kurar ve iki modeli aynı
iş yükü üzerinde karşılaştırır.

#### Ekleme ve Konum

**Akış** (stream), iletilerin yalnız sona eklendiği ve okunduğunda yerinde kaldığı sıralı bir
kayıttır. Yapının kendisi yeni değil: Veritabanı Yönetimi kursunda yazma öncesi günlük aynı
kuralla çalışıyordu — kayıtlar sona eklenir, hiçbir kayıt yerinde değiştirilmez. Akış aynı
yapıyı iletileri taşımak için kullanır.

Kuyruktan iki noktada ayrılır. Birincisi, okuma silmez: `oku` çağrısı günlüğü değiştirmez.
İkincisi, tüketicinin nereye kadar geldiği kuyrukta değil **tüketicide** durur. Bu değere
**konum** (offset) denir ve günlükteki bir kaydın sırasını gösterir.

```js
// gunluk.mjs — yalniz sona ekleme yapilan gunluk: okuma kaydi silmez, konumu tuketici tutar
import { appendFileSync, readFileSync, writeFileSync, existsSync } from "node:fs";

export function sifirla(dosya) { writeFileSync(dosya, ""); }

export function ekle(dosya, kayit) {              // yazma yalniz sona olur
  appendFileSync(dosya, JSON.stringify(kayit) + "\n");
}

export function kayitlar(dosya) {
  if (!existsSync(dosya)) return [];
  const t = readFileSync(dosya, "utf8").trimEnd();
  return t === "" ? [] : t.split("\n").map((s) => JSON.parse(s));
}

// konumdan baslayarak en cok `adet` kayit dondurur; okuma gunlugu degistirmez
export function oku(dosya, konum, adet) {
  return kayitlar(dosya).slice(konum, konum + adet);
}
```

`ekle` işlevinin tek yazma biçimi `appendFileSync` olması bir kolaylık değil, modelin
kuralıdır. Sona ekleme, dosyanın önceki baytlarına dokunmaz; bu yüzden aynı anda okuyan bir
tüketicinin altındaki veri hiç değişmez ve okuma ile yazma birbirini kilitlemez.

#### Bir Kopya, Üç Konum

Önceki derste yayma çarpanını ölçmüştük: abone sayısı arttıkça düşürülen kopya sayısı da
çarpılıyordu. Akışta kopya sayısı birdir; çarpılan şey kopya değil, tutulan konum değeridir.
Aşağıdaki koşum aynı altı olayı iki biçimde saklar ve ikisinin diskte kapladığı yeri karşılaştırır.

```js
// uc-tuketici.mjs — tek gunluk, tuketici basina konum; kopya yerine konum tutulur
import { statSync } from "node:fs";
import { sifirla, ekle, kayitlar, oku } from "./gunluk.mjs";

const OLAYLAR = [
  { tur: "odunc_verildi", oduncId: 1, kitapId: 1, uyeId: 1 },
  { tur: "odunc_verildi", oduncId: 2, kitapId: 2, uyeId: 2 },
  { tur: "iade_alindi", oduncId: 1, kitapId: 1, uyeId: 1 },
  { tur: "odunc_verildi", oduncId: 3, kitapId: 3, uyeId: 3 },
  { tur: "rezervasyon_acildi", kitapId: 1, uyeId: 2 },
  { tur: "iade_alindi", oduncId: 2, kitapId: 2, uyeId: 2 },
];

sifirla("olaylar.log");
for (const o of OLAYLAR) ekle("olaylar.log", o);

const ADLAR = ["bildirim", "stok-ozeti", "rapor"];
for (const ad of ADLAR) {                       // abonelik basina kopya tutan olcut
  sifirla(`kopya-${ad}.log`);
  for (const o of OLAYLAR) ekle(`kopya-${ad}.log`, o);
}
const kopyaBayt = ADLAR.reduce((t, a) => t + statSync(`kopya-${a}.log`).size, 0);
console.log(`tek gunluk       : ${kayitlar("olaylar.log").length} kayit  ${statSync("olaylar.log").size} bayt`);
console.log(`abonelik kopyasi : ${ADLAR.length} dosya  ${kopyaBayt} bayt`);

const konum = { bildirim: 0, "stok-ozeti": 0, rapor: 0 };
const hiz = { bildirim: 6, "stok-ozeti": 4, rapor: 1 };   // turda okunabilen kayit sayisi
for (const ad of ADLAR) konum[ad] += oku("olaylar.log", konum[ad], hiz[ad]).length;
console.log(`bir turdan sonra konumlar: ${ADLAR.map((a) => `${a}=${konum[a]}`).join("  ")}`);
console.log(`gunluk uzunlugu okumadan sonra = ${kayitlar("olaylar.log").length}`);
console.log(`raporun geride kaldigi kayit sayisi = ${kayitlar("olaylar.log").length - konum.rapor}`);
```

```sh
node uc-tuketici.mjs
```

```
tek gunluk       : 6 kayit  337 bayt
abonelik kopyasi : 3 dosya  1011 bayt
bir turdan sonra konumlar: bildirim=6  stok-ozeti=4  rapor=1
gunluk uzunlugu okumadan sonra = 6
raporun geride kaldigi kayit sayisi = 5
```

Üç sayı belirleyici. Diskte tutulan veri 337 bayta karşı 1011 bayt: kopya tutan model, ilgili
taraf sayısıyla doğru orantılı yer kaplarken günlük tek kopya taşıyor. Üç tüketici farklı
konumlarda — 6, 4 ve 1 — yani birbirlerini beklemiyorlar; yalıtım kopyayla değil konumla
sağlanıyor. Ve en önemlisi son iki satır: altı kayıt okumadan sonra hâlâ günlükte duruyor,
raporun beş kayıt geride olduğu tek bir çıkarmayla hesaplanıyor. Bu fark, akışta doğrudan
ölçülebilen bir gecikme ölçütüdür.

Günlük dosyası satır başına bir kayıt tutar ve içeriği okunabilir durumdadır.

```sh
head -3 olaylar.log
```

```
{"tur":"odunc_verildi","oduncId":1,"kitapId":1,"uyeId":1}
{"tur":"odunc_verildi","oduncId":2,"kitapId":2,"uyeId":2}
{"tur":"iade_alindi","oduncId":1,"kitapId":1,"uyeId":1}
```

#### Yeniden Okuma

İki modelin ayrıldığı yer burasıdır. Aşağıdaki koşum altı ödünç olayını önce onaylananı silen
bir kuyruğa, sonra günlüğe koyar; her ikisinde de bir tüketici hepsini işler ve ardından
**ikinci** bir tüketici geçmişi istemeye çalışır.

```js
// yeniden-okuma.mjs — ayni is yuku once kuyrukta sonra gunlukte; gecmis yeniden okunmaya calisilir
import { sifirla, ekle, kayitlar, oku } from "./gunluk.mjs";

const OLAYLAR = [1, 2, 3, 4, 5, 6].map((n) => ({ tur: "odunc_verildi", oduncId: n }));

// Onaylanan iletiyi silen kuyruk: 02. derste kurulan davranisin ozu
class Kuyruk {
  constructor() { this.iletiler = []; }
  yaz(g) { this.iletiler.push(g); }
  al() { return this.iletiler.length === 0 ? null : this.iletiler[0]; }
  onayla() { this.iletiler.shift(); }            // onay: ileti kuyruktan silinir
  get derinlik() { return this.iletiler.length; }
}

const k = new Kuyruk();
for (const o of OLAYLAR) k.yaz(o);
let birinci = 0;
for (let i = k.al(); i !== null; i = k.al()) { birinci += 1; k.onayla(); }
let ikinci = 0;
for (let i = k.al(); i !== null; i = k.al()) { ikinci += 1; k.onayla(); }
console.log(`kuyruk: yazilan=${OLAYLAR.length} birinci tuketici=${birinci} ` +
            `derinlik=${k.derinlik} sonra baglanan tuketici=${ikinci}`);

sifirla("olaylar.log");
for (const o of OLAYLAR) ekle("olaylar.log", o);
let konum = 0;
for (const kayit of oku("olaylar.log", konum, 100)) konum += 1;
const sonraki = oku("olaylar.log", 0, 100).length;      // konumu sifira alan ikinci tuketici
console.log(`akis  : yazilan=${OLAYLAR.length} birinci tuketici=${konum} ` +
            `gunluk uzunlugu=${kayitlar("olaylar.log").length} sonra baglanan tuketici=${sonraki}`);

// Ayni tuketici kendi hatasini duzeltip dorduncu kayittan yeniden isleyebilir mi?
const geriAl = oku("olaylar.log", 3, 100);
console.log(`akis  : konum 3'e geri alindi -> yeniden okunan kayit=${geriAl.length} ` +
            `ilk odunc=${geriAl[0].oduncId}`);
```

```sh
node yeniden-okuma.mjs
```

```
kuyruk: yazilan=6 birinci tuketici=6 derinlik=0 sonra baglanan tuketici=0
akis  : yazilan=6 birinci tuketici=6 gunluk uzunlugu=6 sonra baglanan tuketici=6
akis  : konum 3'e geri alindi -> yeniden okunan kayit=3 ilk odunc=4
```

Kuyrukta ikinci tüketici sıfır kayıt okuyabildi. Bu bir yapılandırma eksikliği değil: onay
iletiyi sildiği için okunacak bir şey kalmamıştı. Akışta ikinci tüketici altı kaydın hepsini
okudu ve bunun için günlükte hiçbir şey değişmedi.

Üçüncü satır **yeniden okumanın** (replay) asıl kullanımını gösterir. Rapor tüketicisi
dördüncü olaydan itibaren yanlış hesap yaptığını fark ederse, düzeltilmiş kodla konumunu 3
değerine geri alması yeter; günlük ona aynı kayıtları aynı sırayla verir. Kuyrukta bunun
karşılığı yoktur, çünkü geri alınacak bir konum yoktur — kuyruğun tuttuğu tek durum, hangi
iletinin hâlâ orada olduğudur.

Bunun bedeli de aynı yerdedir. Konum tüketicide olduğu için **onu kaydetmek de tüketicinin
işidir**. Kuyrukta "işlendi" bilgisini kuyruk tutar; akışta tüketici, konumu kendi kalıcı
deposuna yazmadıkça yeniden başladığında nereden devam edeceğini bilemez. Konumun ne zaman
yazılacağı — işten önce mi sonra mı — bir sonraki dersin konusudur.

#### Saklama Süresi

Kuyruk tükettikçe boşalır. Günlük boşalmaz; yalnız büyür. Bu yüzden akışta silme kararı
tüketime değil bir kurala bağlanır: **saklama süresi** (retention). Kural süreye, boyuta ya
da kayıt sayısına dayanabilir; ortak yanı, tüketicilerin nerede olduğuna bakmamasıdır.

```js
// saklama.mjs — saklama suresi dolan kayitlar budanir; geride kalan tuketici ne kaybeder
import { writeFileSync } from "node:fs";
import { sifirla, ekle, kayitlar, oku } from "./gunluk.mjs";

let ilk = 0;                                  // gunlukte duran en eski kaydin konumu
function buda(dosya, tut) {                   // en yeni `tut` kayit birakilir
  const hepsi = kayitlar(dosya);
  const atilan = Math.max(0, hepsi.length - tut);
  writeFileSync(dosya, hepsi.slice(atilan).map((k) => JSON.stringify(k) + "\n").join(""));
  ilk += atilan;
  return atilan;
}
function okuKonumdan(dosya, konum, adet) {    // mutlak konum, budanmis gunlukte
  if (konum < ilk) return null;               // istenen kayit artik yok
  return oku(dosya, konum - ilk, adet);
}

sifirla("olaylar.log");
for (let n = 1; n <= 10; n++) ekle("olaylar.log", { tur: "odunc_verildi", oduncId: n });

const konum = { bildirim: 10, rapor: 2 };     // rapor sekiz kayit geride
console.log(`budamadan once: gunluk=${kayitlar("olaylar.log").length} kayit  ilk konum=${ilk}`);
console.log(`  rapor konum=${konum.rapor} -> okunabilen=${okuKonumdan("olaylar.log", konum.rapor, 100).length}`);

const atilan = buda("olaylar.log", 4);
console.log(`budama sonrasi: atilan=${atilan}  gunluk=${kayitlar("olaylar.log").length} kayit  ilk konum=${ilk}`);
const raporOkuma = okuKonumdan("olaylar.log", konum.rapor, 100);
console.log(`  bildirim konum=${konum.bildirim} -> okunabilen=${okuKonumdan("olaylar.log", konum.bildirim, 100).length}`);
console.log(`  rapor konum=${konum.rapor} -> ${raporOkuma === null ? "konum budandi, kayit geri alinamaz" : raporOkuma.length}`);
console.log(`raporun kaybettigi kayit sayisi = ${ilk - konum.rapor}`);
```

```sh
node saklama.mjs
```

```
budamadan once: gunluk=10 kayit  ilk konum=0
  rapor konum=2 -> okunabilen=8
budama sonrasi: atilan=6  gunluk=4 kayit  ilk konum=6
  bildirim konum=10 -> okunabilen=0
  rapor konum=2 -> konum budandi, kayit geri alinamaz
raporun kaybettigi kayit sayisi = 4
```

Budamadan önce rapor sekiz kaydı okuyabiliyordu. En yeni dört kayıt bırakılınca günlüğün en
eski konumu 6 oldu ve raporun konumu bu değerin gerisinde kaldı; artık okuyabileceği bir yer
yok. Kaybettiği kayıt sayısı iki konumun farkıdır: dört.

Bildirim tüketicisi aynı budamadan hiç etkilenmedi, çünkü konumu günlüğün sonundaydı ve
okuyacak yeni kaydı zaten yoktu. Kuralın izlenmesi gereken ölçütü buradan çıkar: en geride
kalan tüketicinin konumu ile günlüğün en eski konumu arasındaki fark. Bu fark sıfıra
yaklaştığında saklama süresi ya uzatılır ya da geride kalan tüketici hızlandırılır.

#### Hangi Model Neyi Çözer

İki model birbirinin yerine geçmez; farklı soruların yanıtıdır.

Kuyruk, işin **bölünmesi** için kurulur. Her ileti bir kez bir tüketiciye gider, iş
tüketiciler arasında paylaşılır, işlenen iş silinerek yer açılır. Gecikme bildirimi gönderme
işi böyledir: kim gönderdiği önemsizdir, gönderildikten sonra iletinin saklanmasının bir
değeri yoktur.

Akış, olayın **kaydı** için kurulur. Her tüketici tüm kayıtları kendi konumundan okur, aynı
kaydı birden çok taraf görebilir, geçmiş silinmediği için yeniden okunabilir. Ödünç
olaylarından türetilen şube stok özeti böyledir: hesap değişirse özet günlükten yeniden
üretilir.

Ayrımı tek bir soruyla yapmak yeterlidir: **aynı kaydın ikinci kez okunması anlamlı mı?**
Yanıt hayırsa kuyruk, evetse akış. Bu ayrım maliyetsiz değildir — akış saklama alanı,
konum yönetimi ve budama kuralı ister; kuyruk bunların hiçbirini istemez.

#### Özet

- Akış, iletilerin yalnız sona eklendiği ve okumanın kaydı silmediği sıralı bir günlüktür;
  tüketicinin nereye kadar geldiği kuyrukta değil, tüketicideki konum değerinde durur.
- Altı olay tek günlükte 337 bayt, abonelik başına kopyalandığında üç dosyada 1011 bayt yer
  kapladı; üç tüketici 6, 4 ve 1 konumlarında birbirini beklemeden ilerledi.
- Aynı iş yükünde sonradan bağlanan tüketici kuyrukta sıfır, akışta altı kayıt okuyabildi;
  konumu 3 değerine geri alan tüketici üç kaydı yeniden okudu.
- Konum tüketicide durduğu için onu kalıcı olarak kaydetmek de tüketicinin işidir; kuyrukta
  bu bilgiyi kuyruğun kendisi tutar.
- Günlük tüketimle boşalmaz, saklama kuralıyla budanır: en yeni dört kayıt bırakıldığında en
  eski konum 6 oldu ve konumu 2 olan tüketici dört kaydı geri alamaz duruma düştü.

#### Sonraki Adım

Her iki modelde de bir soru yanıtsız kaldı: işin yapılması ile "yapıldı" bilgisinin
yazılması iki ayrı adım. Kuyrukta onay işten önce mi sonra mı gönderilir, akışta konum işten
önce mi sonra mı kaydedilir? Sıralamanın iki seçeneği vardır ve ikisi de bir şey kaybeder —
biri iş yapılmadan işlenmiş sayar, öteki aynı işi iki kez yaptırır. Sonraki ders aynı iş
yükünü üç kipte koşturur: en çok bir kez, en az bir kez ve tekillik anahtarıyla tekilleştirme.
Kayıp sayısı ile yinelenen teslim sayısı ölçülür ve "tam bir kez"in uçtan uca neden bir kip
değil, bir sonuç olduğu gösterilir.

### Teslim Semantiği

Source: https://academia.sh/tr/kurslar/eszamansiz-isleme/teslim-semantigi

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.

Ö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.

```sh
# 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.

```js
// 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)}`);
}
```

```sh
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.

```js
// 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)}`);
}
```

```sh
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.

### Sıralama ve Bölümleme

Source: https://academia.sh/tr/kurslar/eszamansiz-isleme/siralama-ve-bolumleme

Sıra garantisinin koşutlukla ödünleşmesi: rekabet eden tüketicilerin aynı anahtarın olaylarını ters sırada bitirmesinin ters çift sayısıyla ölçülmesi, anahtara göre bölümlemenin anahtar içi sırayı korurken genel sırayı bırakması, bölüm sayısının koşutluğa etkisi ve sıcak anahtarın bölüm yükünü dengesizleştirmesi.

Önceki ders teslim sayısı ile etki sayısını ayırdı, ama iki ölçüm de teslimin sırasına hiç
bakmadı. Ödünç sisteminde sıranın belirleyici olduğu yerler var. Aynı kitabın "verildi" ve
"iade alındı" olayları ters sırayla işlenirse şube stok özeti kitabı hem rafta hem üyede
gösterir; kitabın son durumu, olayların gerçek zamanına değil işlenme sırasına göre oluşur.

Bu ders bir gerilimi ölçer. Şimdiye kadar işi hızlandırmanın yolu tüketici eklemekti;
tüketici eklemek ise sırayı bozar. Sorunun sayısal karşılığını görmek için önce sıra ihlalini
ölçülebilir bir şeye çevirmek gerekiyor.

#### Sıra İhlali Nasıl Sayılır

Olaylara üretildikleri sırayı veren bir numara verilir. İşlendikten sonra olaylar bitiş
zamanlarına göre dizilir. İki olay bu dizide üretim sırasının tersine yerleşmişse bir **ters
çift** (inversion) oluşur; Algoritmalar kursunda sıralama başarımını ölçmek için kullanılan
sayının aynısıdır. İki ayrı sayı tutulur: tüm çiftler üzerinden **genel** ters çift ve yalnız
aynı kitabın olayları arasındaki **anahtar içi** ters çift.

Ayrım önemlidir, çünkü ödünç sistemi genel sıra istemez. İki farklı kitabın olaylarının hangi
sırayla bittiği hiçbir şeyi değiştirmez. Bozulması yanlış sonuç üreten şey, **aynı kitabın**
olaylarının sırasıdır.

```js
// bolum.mjs — sabit tohumlu olay dizisi, bolumleme kosumu ve sira ihlali sayaci
export function uretec(tohum) {                 // sabit tohumlu dogrusal eslesmeli uretec
  let x = tohum;
  return () => (x = (x * 1103515245 + 12345) % 2147483648) / 2147483648;
}

export function olaylar(adet = 24, tohum = 20250720) {
  const rast = uretec(tohum);
  const TUR = ["verildi", "iade_alindi", "rezerve_edildi"];
  const liste = [];
  for (let sira = 0; sira < adet; sira++) {
    const kitapId = 1 + Math.floor(rast() * 6);          // 1..6 arasi kitap
    liste.push({ sira, kitapId, tur: TUR[Math.floor(rast() * 3)], sure: 1 + Math.floor(rast() * 3) });
  }
  return liste;
}

const sirala = (a) => [...a].sort((x, y) => x.bitis - y.bitis || x.yer - y.yer);

// Her bolum tek isci tarafindan, gelis sirasiyla islenir.
export function bolumlu(liste, bolumSayisi) {
  const bitis = new Array(bolumSayisi).fill(0);
  const sonuc = liste.map((o) => {
    const b = o.kitapId % bolumSayisi;                    // bolum anahtari: kitap kimligi
    bitis[b] += o.sure;
    return { ...o, yer: b, bitis: bitis[b] };
  });
  return { sonuc: sirala(sonuc), sure: Math.max(...bitis), yuk: bitis };
}

// Bolumleme yok: her ileti o an bosalan isciye gider.
export function rakip(liste, isciSayisi) {
  const musait = new Array(isciSayisi).fill(0);
  const sonuc = liste.map((o) => {
    let k = 0;
    for (let i = 1; i < isciSayisi; i++) if (musait[i] < musait[k]) k = i;
    musait[k] += o.sure;
    return { ...o, yer: k, bitis: musait[k] };
  });
  return { sonuc: sirala(sonuc), sure: Math.max(...musait), yuk: musait };
}

// Bitis sirasina gore dizilmis olaylarda ters cift sayisi.
export function ihlal(bitisSirali) {
  let genel = 0, anahtarIci = 0;
  for (let i = 0; i < bitisSirali.length; i++)
    for (let j = i + 1; j < bitisSirali.length; j++)
      if (bitisSirali[i].sira > bitisSirali[j].sira) {
        genel += 1;
        if (bitisSirali[i].kitapId === bitisSirali[j].kitapId) anahtarIci += 1;
      }
  return { genel, anahtarIci };
}
```

#### Tüketici Eklemek Sırayı Bozar

İlk ölçüm rekabet eden tüketici düzenini koşturur: her ileti o an boşalan işçiye gider, bölümleme
yoktur. Yirmi dört olay altı farklı kitaba dağılmıştır ve süreleri sabit tohumla üretilmiştir.

```js
// rakip-sira.mjs — bolumleme olmadan isci eklemek: sure duser, anahtar ici sira bozulur
import { olaylar, rakip, ihlal } from "./bolum.mjs";
const liste = olaylar();

console.log(`olay sayisi=${liste.length}  farkli kitap=${new Set(liste.map((o) => o.kitapId)).size}`);
console.log("isci  sure  genel ters cift  anahtar ici ters cift");
for (const n of [1, 2, 3, 4]) {
  const k = rakip(liste, n);
  const i = ihlal(k.sonuc);
  console.log(`${String(n).padStart(4)}${String(k.sure).padStart(6)}` +
              `${String(i.genel).padStart(16)}${String(i.anahtarIci).padStart(23)}`);
}
```

```sh
node rakip-sira.mjs
```

```
olay sayisi=24  farkli kitap=6
isci  sure  genel ters cift  anahtar ici ters cift
   1    44               0                      0
   2    22               4                      1
   3    15               9                      1
   4    12              17                      3
```

Tek işçide iki sayı da sıfır: sıra garantisi bedava değil, tek tüketici koşuluyla geliyor.
İşçi sayısı dörde çıktığında toplam süre 44'ten 12'ye iniyor, ama anahtar içi ters çift sayısı
üçe çıkıyor. Bu üç çift, aynı kitabın iki olayının ters sırayla bittiği durumlardır — stok
özetinin yanlış sonuç ürettiği yer tam burasıdır.

Sonuç bir ödünleşmedir ve her iki uç da kabul edilemez. Tek tüketici sırayı korur ama verim
üçte bire iner. Dört tüketici verimi verir ama doğruluğu bozar. Aranan şey ortada bir yerdedir:
**sıranın yalnız gerektiği yerde** korunması.

#### Anahtara Göre Bölümleme

Gerektiği yer bellidir: aynı kitabın olayları. Öyleyse iletiler işçilere değil, **anahtarlara**
göre dağıtılsın. Her ileti bir **bölüm anahtarından** (partition key) türetilen bir **bölüme**
(partition) düşsün ve her bölüm tek bir işçi tarafından geliş sırasıyla işlensin.

Kural iki koşulu birden verir. Aynı anahtarın bütün olayları aynı bölüme düşer, dolayısıyla
aralarındaki sıra korunur. Farklı bölümler birbirinden bağımsız çalıştığı için bölüm sayısı
kadar koşutluk elde edilir.

```js
// bolumlu-sira.mjs — ayni is yuku anahtara gore bolumlenince anahtar ici sira korunur
import { olaylar, bolumlu, ihlal } from "./bolum.mjs";
const liste = olaylar();

console.log("bolum  sure  genel ters cift  anahtar ici ters cift  bolum yukleri");
for (const p of [1, 2, 3, 4, 6]) {
  const k = bolumlu(liste, p);
  const i = ihlal(k.sonuc);
  console.log(`${String(p).padStart(5)}${String(k.sure).padStart(6)}` +
              `${String(i.genel).padStart(16)}${String(i.anahtarIci).padStart(23)}   ${k.yuk.join(" ")}`);
}
```

```sh
node bolumlu-sira.mjs
```

```
bolum  sure  genel ters cift  anahtar ici ters cift  bolum yukleri
    1    44               0                      0   44
    2    24              20                      0   24 20
    3    20              24                      0   20 13 11
    4    21              25                      0   3 14 21 6
    6    14              44                      0   14 10 7 6 3 4
```

Anahtar içi ters çift sütunu bölüm sayısından bağımsız olarak sıfırdır. Genel ters çift ise
bölüm sayısıyla birlikte artıp altı bölümde 44'e çıkar. İki sütunun birlikte okunması dersin
özüdür: **bölümleme, genel sırayı vererek anahtar içi sırayı satın alır.** Kaybedilen şeyin
zaten değeri yoktu; korunan şey doğruluğu belirliyordu.

Süre sütunu ikinci bir gerçeği söyler. Bölüm sayısı arttıkça süre düşüyor, ama düzenli değil:
dört bölümde 21, üç bölümde 20. Bölüm yükleri sütunu nedenini gösteriyor — dört bölümde
yükler 3, 14, 21 ve 6; en dolu bölüm işi bitirene kadar diğer üçü boşta bekliyor. Bölüm
sayısını artırmak koşutluk **kapasitesini** artırır, koşutluğun gerçekleşmesini değil. Gerçek
koşutluğu belirleyen, yükün bölümlere ne kadar dengeli dağıldığıdır.

##### Bölüm Anahtarını Seçmek

Anahtar seçimi bir başarım ayarı değil, bir doğruluk kararıdır: sıra garantisi tam olarak
anahtarın tanımladığı küme içinde geçerlidir, dışında hiç yoktur.

Kitap kimliği anahtar seçilirse aynı kitabın verilme ve iade olayları sıralı kalır, ama aynı
üyenin iki farklı kitaptaki olayları sıralı kalmaz. Üye kimliği seçilirse tersi olur.
Ölçütü belirleyen soru şudur: **hangi olay çiftinin ters sırayla işlenmesi yanlış sonuç
üretir?** Stok özeti için yanıt kitaptır, çünkü bir kitabın rafta olup olmadığı yalnız o
kitabın olaylarına bağlıdır. Üyenin açık ödünç sayısını tutan bir sayaç için yanıt üyedir.

İkisi de gerekiyorsa tek bir anahtar yetmez. Bu durumda ya daha kaba bir anahtar seçilir —
örneğin şube — ve koşutluk düşer, ya da sıraya bağlı olmayan bir etki tasarlanır. İkincisi
genelde daha ucuzdur: olaya bir sürüm numarası konur ve tüketici yalnız kendi gördüğünden
büyük sürümü uygular. Böyle bir etki geç gelen eski olayı sessizce yok sayar ve sıra
garantisine hiç ihtiyaç duymaz.

#### Sıcak Anahtar

Bölümlemenin dengesizliği rastgele değildir; genellikle tek bir anahtarın diğerlerinden çok
daha fazla olay üretmesinden gelir. Çok istenen bir kitap, işlemlerin yarısından fazlasını
tek başına doğurabilir. Böyle bir anahtara **sıcak anahtar** (hot key) denir.

```js
// sicak-anahtar.mjs — tek kitabin yuku tek bolumde toplanir; bolum eklemek onu bolmez
import { uretec, bolumlu } from "./bolum.mjs";

function sicakOlaylar(adet = 30, tohum = 7) {
  const rast = uretec(tohum);
  const liste = [];
  for (let sira = 0; sira < adet; sira++) {
    const kitapId = rast() < 0.6 ? 3 : 1 + Math.floor(rast() * 6);   // kitap 3 cok istenen kitap
    liste.push({ sira, kitapId, sure: 1 + Math.floor(rast() * 3) });
  }
  return liste;
}

const liste = sicakOlaylar();
const toplam = liste.reduce((t, o) => t + o.sure, 0);
const sicakYuk = liste.filter((o) => o.kitapId === 3).reduce((t, o) => t + o.sure, 0);
console.log(`olay=${liste.length}  toplam sure=${toplam}  kitap 3 olay=${liste.filter((o) => o.kitapId === 3).length} yuk=${sicakYuk}`);
console.log("bolum  sure  bolum yukleri");
for (const p of [1, 2, 4, 8]) {
  const k = bolumlu(liste, p);
  console.log(`${String(p).padStart(5)}${String(k.sure).padStart(6)}   ${k.yuk.join(" ")}`);
}

const p = 2;
const sicakBolum = 3 % p;
const bekleme = bolumlu(liste, p).sonuc.map((o) => ({ ...o, bekleme: o.bitis - o.sure }));
const ort = (d) => d.length === 0 ? "-" : (d.reduce((t, o) => t + o.bekleme, 0) / d.length).toFixed(1);
const engellenen = bekleme.filter((o) => o.yer === sicakBolum && o.kitapId !== 3);
let t = 0;                                  // ayni olaylar sicak anahtarsiz bir bolumde olsaydi
const yalniz = [...engellenen].sort((a, b) => a.sira - b.sira).map((o) => { const b = t; t += o.sure; return b; });
console.log(`bolum=2  sicak bolumdeki kitap 3 disi ${engellenen.length} olayin ortalama beklemesi = ${ort(engellenen)}`);
console.log(`         ayni olaylar sicak anahtarsiz bir bolumde olsaydi         = ` +
            `${(yalniz.reduce((a, b) => a + b, 0) / yalniz.length).toFixed(1)}`);
```

```sh
node sicak-anahtar.mjs
```

```
olay=30  toplam sure=59  kitap 3 olay=18 yuk=28
bolum  sure  bolum yukleri
    1    59   59
    2    41   18 41
    4    28   9 13 9 28
    8    28   0 10 3 28 9 3 6 0
bolum=2  sicak bolumdeki kitap 3 disi 5 olayin ortalama beklemesi = 11.0
         ayni olaylar sicak anahtarsiz bir bolumde olsaydi         = 5.0
```

Bölüm sayısını dörtten sekize çıkarmak süreyi hiç değiştirmedi: ikisinde de 28. Sekiz bölümde
iki bölüm tamamen boş, biri 28 birimlik yükü tek başına taşıyor. Sayı tesadüf değil, kitap
3'ün toplam yüküne eşit. **Sıra garantisi anahtarı bölünmez kıldığı için, sıcak anahtarın
yükü sistemin alt sınırıdır**; bölüm eklemek o sınırı düşürmez.

Son iki satır ikinci bir maliyeti gösteriyor. Aynı bölüme düşen ve sıcak anahtarla hiç ilgisi
olmayan beş olay, ortalama 11 birim bekliyor; kendi başlarına bir bölümde olsalar 5 birim
beklerlerdi. Sıcak anahtarın işleri, arkalarındaki ilgisiz işleri de geciktiriyor. Bu
davranışa **sıra başı engellemesi** (head-of-line blocking) denir; ağ katmanlarında karşılaşılan
aynı olgunun kuyruk düzlemindeki karşılığıdır.

Çözüm anahtarı değiştirmekten geçer. Sıcak anahtar için sıra garantisi gerçekten gerekliyse
tek bölüm kaçınılmazdır ve tek çare o bölümün işini ucuzlatmaktır. Gerekli değilse anahtar
inceltilir: kitap yerine "kitap ve olay türü" gibi bileşik bir anahtar, sıcak kitabın
olaylarını birden çok bölüme dağıtır — ama bu, verilme ile iade arasındaki sırayı da bırakmak
demektir. Karar yine aynı soruya döner: hangi çiftin sırası doğruluğu belirliyor.

#### Özet

- Sıra ihlali ters çift sayısıyla ölçülür; ödünç sisteminde anlamlı olan genel ters çift değil,
  aynı kitabın olayları arasındaki anahtar içi ters çifttir.
- Bölümlemesiz rekabet eden tüketicilerde işçi sayısı dörde çıkınca süre 44'ten 12'ye indi, ama
  anahtar içi ters çift sayısı sıfırdan üçe çıktı.
- Anahtara göre bölümlemede anahtar içi ters çift bölüm sayısından bağımsız olarak sıfır
  kaldı; genel ters çift altı bölümde 44'e çıktı. Bölümleme genel sırayı vererek anahtar içi
  sırayı satın alır.
- Bölüm sayısını artırmak koşutluk kapasitesini artırır, koşutluğu değil: dört bölümde yükler
  3, 14, 21 ve 6 dağıldığı için süre üç bölümdekinden daha kötü çıktı.
- Sıcak anahtar bölünemediği için sistemin alt sınırını belirler — dört ve sekiz bölümde süre
  aynı 28 kaldı — ve aynı bölümdeki ilgisiz işlerin ortalama beklemesini 5 birimden 11 birime
  çıkardı.

#### Sonraki Adım

Şimdiye kadarki bütün ölçümlerde bir varsayım sessizce sürdü: yeniden denenen iş sonunda
başarılı oluyor. Çökme geçiciydi, ikinci deneme işi bitiriyordu. Gerçekte bazı iletiler ikinci
denemede de, onuncu denemede de başarısız olur — gövdesi bozuktur, atıfta bulunduğu üye
silinmiştir, iş kuralı onu hiçbir zaman kabul etmeyecektir. En az bir kez kipinde böyle bir
ileti kuyruktan hiç çıkmaz ve bölümlenmiş bir düzende arkasındaki bütün işleri durdurur.
Sonraki ders bu iletileri kuyruktan çıkarıp yalıtan yapıyı kurar: deneme sayacı, eşik ve ölü
mektup kuyruğu.

### Ölü Mektup Kuyrukları

Source: https://academia.sh/tr/kurslar/eszamansiz-isleme/olu-mektup-kuyruklari

Hiçbir zaman başarılı olmayacak iletilerin yalıtılması: eşiksiz yeniden denemenin boşa deneme sayısını sınırsız büyütmesi, deneme sayacı ile eşiğin kuyruğu boşaltması, ölü mektup kaydının taşıması gereken alanlar, nedeni giderilen iletilerin geri sürülmesi ve eşik değerinin geçici hatayı yanlış yalıtmayla ödünleşmesi.

Şimdiye kadarki bütün ölçümlerde bir varsayım sessizce sürdü: yeniden denenen iş sonunda
başarılı oluyor. Çökme geçiciydi, ikinci deneme işi bitiriyordu. Gerçekte bazı iletiler ikinci
denemede de, onuncu denemede de başarısız olur — gövdesi çözülemez, atıfta bulunduğu üye
kaydı silinmiştir, taşıdığı iş kuralı onu hiçbir zaman kabul etmeyecektir.

En az bir kez kipinde böyle bir ileti kuyruktan hiç çıkmaz. Bu ders o iletinin ne kadar
zarar verdiğini ölçer ve onu kuyruktan çıkarıp yalıtan yapıyı kurar.

#### Kalıcı Hata ile Geçici Hata

Ayrım yeniden denemenin anlamlı olup olmadığına dayanır. **Geçici hata**, koşullar değişince
ortadan kalkan hatadır: veritabanı bağlantısının kopması, dış servisin kısa süreli
erişilemezliği, iki yazmanın çatışması. Yeniden deneme bu hataların doğru yanıtıdır.

**Kalıcı hata** koşullardan değil iletinin kendisinden gelir. Aynı ileti aynı kodla her
denemede aynı sonucu verir; yeniden deneme yalnız hatayı tekrarlar. İki hata türü aynı yolu
paylaştığında, kalıcı olanı ayırmanın tek yolu deneme sayısına bakmaktır.

Ölçüm düzeneği iki bağıntı kurar: iletileri ve deneme sayaçlarını tutan iş kuyruğu, yalıtılan
iletileri tutan ölü mektup kuyruğu.

```sh
# kur.sh — is kuyrugu ve olu mektup kuyrugu bagintilari
rm -f kuyruk.db
sqlite3 kuyruk.db <<'SQL'
CREATE TABLE is_kuyrugu (ileti_id INTEGER PRIMARY KEY, tur TEXT NOT NULL, govde TEXT NOT NULL,
                         deneme INTEGER NOT NULL DEFAULT 0,
                         durum TEXT NOT NULL DEFAULT 'bekliyor');
CREATE TABLE olu_mektup (olu_id INTEGER PRIMARY KEY, ileti_id INTEGER NOT NULL, tur TEXT NOT NULL,
                         govde TEXT NOT NULL, deneme INTEGER NOT NULL, son_hata TEXT NOT NULL,
                         kaynak TEXT NOT NULL);
SQL
sqlite3 kuyruk.db "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
```

```
is_kuyrugu
olu_mektup
```

#### Eşiksiz Yeniden Denemenin Maliyeti

Yirmi gecikme bildirimi işlenecek; ikisinin — 7 ve 13 numaralı iletilerin — atıfta bulunduğu
üye kaydı yok, dolayısıyla her denemede başarısız olacaklar. Aşağıdaki koşum aynı iş yükünü
üç ayarla çalıştırır: eşiksiz sekiz tur, eşiksiz yirmi dört tur ve üç denemelik eşikle sekiz tur.

```js
// esik.mjs — esiksiz yeniden deneme ile esikli yalitmanin ayni is yukunde karsilastirilmasi
import { DatabaseSync } from "node:sqlite";

const KALICI = new Set([7, 13]);          // govdesi bozuk iletiler: her denemede basarisiz
const ADET = 20;

function tazele(db) {
  db.exec("DELETE FROM is_kuyrugu; DELETE FROM olu_mektup");
  for (let n = 1; n <= ADET; n++)
    db.prepare("INSERT INTO is_kuyrugu (ileti_id, tur, govde) VALUES (?,?,?)")
      .run(n, "gecikme_bildirimi", JSON.stringify({ oduncId: n }));
}

function isle(i) {
  if (KALICI.has(i.ileti_id)) throw new Error(`uye bulunamadi (odunc ${i.ileti_id})`);
}

function kosum(db, esik, turSayisi) {     // esik = 0 -> yalitim yok
  tazele(db);
  let teslim = 0, basarili = 0, bosa = 0;
  for (let tur = 1; tur <= turSayisi; tur++) {
    for (const i of db.prepare("SELECT * FROM is_kuyrugu WHERE durum='bekliyor' ORDER BY ileti_id").all()) {
      teslim += 1;
      const deneme = i.deneme + 1;
      db.prepare("UPDATE is_kuyrugu SET deneme=? WHERE ileti_id=?").run(deneme, i.ileti_id);
      try {
        isle(i);
        db.prepare("UPDATE is_kuyrugu SET durum='islendi' WHERE ileti_id=?").run(i.ileti_id);
        basarili += 1;
      } catch (e) {
        bosa += 1;
        if (esik > 0 && deneme >= esik) {
          db.prepare(`INSERT INTO olu_mektup (ileti_id, tur, govde, deneme, son_hata, kaynak)
                      VALUES (?,?,?,?,?,'is_kuyrugu')`)
            .run(i.ileti_id, i.tur, i.govde, deneme, e.message);
          db.prepare("UPDATE is_kuyrugu SET durum='olu' WHERE ileti_id=?").run(i.ileti_id);
        }
      }
    }
  }
  const say = (s) => db.prepare(s).get().n;
  return { teslim, basarili, bosa,
           derinlik: say("SELECT count(*) AS n FROM is_kuyrugu WHERE durum='bekliyor'"),
           olu: say("SELECT count(*) AS n FROM olu_mektup") };
}

const db = new DatabaseSync("kuyruk.db");
console.log("kip".padEnd(10) + "  tur  teslim  basarili  bosa deneme  kalan derinlik  olu mektup");
for (const [ad, esik, tur] of [["esiksiz", 0, 8], ["esiksiz", 0, 24], ["esik=3", 3, 8]]) {
  const s = kosum(db, esik, tur);
  console.log(ad.padEnd(10) +
    `${String(tur).padStart(5)}${String(s.teslim).padStart(8)}${String(s.basarili).padStart(10)}` +
    `${String(s.bosa).padStart(12)}${String(s.derinlik).padStart(16)}${String(s.olu).padStart(12)}`);
}
```

```sh
node esik.mjs
```

```
kip         tur  teslim  basarili  bosa deneme  kalan derinlik  olu mektup
esiksiz       8      34        18          16               2           0
esiksiz      24      66        18          48               2           0
esik=3        8      24        18           6               0           2
```

İlk iki satır aynı iş yükünün iki farklı süredeki görüntüsüdür ve tek farkları boşa deneme
sayısıdır: sekiz turda 16, yirmi dört turda 48. Başarılı iş her ikisinde de 18'de sabit,
kalan derinlik her ikisinde de 2. İki bozuk ileti hiçbir zaman bitmediği için her turda
yeniden teslim edilir; harcanan kapasite tur sayısıyla doğru orantılı büyür ve bir üst sınırı
yoktur.

Üçüncü satır eşiğin ne yaptığını gösteriyor. Boşa deneme 6'da durdu — iki ileti üçer kez
denendi ve kuyruktan çıkarıldı. Toplam teslim 34 yerine 24. Kazanç yalnız sayıda değil:
kalan derinlik ikiden sıfıra indi, yani kuyruk **bitmiş** durumdadır. İki bozuk ileti
kaybolmamış, ayrı bir yere alınmıştır.

Önceki dersin bölümlenmiş düzeninde aynı iletinin maliyeti daha da yüksektir. Bozuk ileti
sırayı koruyan bir bölümün başındaysa, arkasındaki bütün iletiler o bölümde bekler; sıra
garantisi hatalı iletinin atlanmasına izin vermez. Eşik, bölümü yeniden akıtan tek düzenektir.

#### Ölü Mektup Kaydı Neyi Taşımalı

Eşiği aşan iletinin gideceği yere **ölü mektup kuyruğu** (dead-letter queue) denir. Kuyruk
olmasının nedeni, orada duran şeyin bir günlük kaydı değil, hâlâ işlenebilir bir **ileti**
olmasıdır. Bu yüzden kayıt en az beş alanı taşır: iletinin gövdesi olduğu gibi, türü, kaç kez
denendiği, son hatanın metni ve hangi kuyruktan geldiği. Gövde eksikse ileti yeniden
sürülemez; hata metni yoksa nedeni araştırmak için denemeyi tekrarlamak gerekir.

Aşağıdaki koşum önce iletileri yalıtır, kayıtları yazdırır, sonra nedeni giderir ve
yalıtılanları iş kuyruğuna geri sürer.

```js
// yeniden-surme.mjs — olu mektup kaydinin icerigi ve neden giderildikten sonra geri surulmesi
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kuyruk.db");

let bozuk = new Set([7, 13]);
const isle = (i) => { if (bozuk.has(i.ileti_id)) throw new Error(`uye bulunamadi (odunc ${i.ileti_id})`); };

db.exec("DELETE FROM is_kuyrugu; DELETE FROM olu_mektup");
for (let n = 1; n <= 20; n++)
  db.prepare("INSERT INTO is_kuyrugu (ileti_id, tur, govde) VALUES (?,?,?)")
    .run(n, "gecikme_bildirimi", JSON.stringify({ oduncId: n }));

function tuket(esik = 3) {
  let islenen = 0;
  for (let tur = 1; tur <= esik; tur++)
    for (const i of db.prepare("SELECT * FROM is_kuyrugu WHERE durum='bekliyor' ORDER BY ileti_id").all()) {
      const deneme = i.deneme + 1;
      db.prepare("UPDATE is_kuyrugu SET deneme=? WHERE ileti_id=?").run(deneme, i.ileti_id);
      try {
        isle(i);
        db.prepare("UPDATE is_kuyrugu SET durum='islendi' WHERE ileti_id=?").run(i.ileti_id);
        islenen += 1;
      } catch (e) {
        if (deneme >= esik) {                       // esik asildi: ileti yalitilir
          db.prepare(`INSERT INTO olu_mektup (ileti_id, tur, govde, deneme, son_hata, kaynak)
                      VALUES (?,?,?,?,?,'is_kuyrugu')`)
            .run(i.ileti_id, i.tur, i.govde, deneme, e.message);
          db.prepare("UPDATE is_kuyrugu SET durum='olu' WHERE ileti_id=?").run(i.ileti_id);
        }
      }
    }
  return islenen;
}

const say = (s) => db.prepare(`SELECT count(*) AS n FROM ${s}`).get().n;
console.log(`birinci gecis: islenen=${tuket()}  bekleyen=${say("is_kuyrugu WHERE durum='bekliyor'")}`);
for (const r of db.prepare("SELECT * FROM olu_mektup ORDER BY ileti_id").all())
  console.log(`  ileti=${r.ileti_id} tur=${r.tur} deneme=${r.deneme} kaynak=${r.kaynak} ` +
              `govde=${r.govde} son_hata=${r.son_hata}`);

bozuk = new Set();                                   // neden giderildi: eksik uye kayitlari tamamlandi
const geri = db.prepare("SELECT * FROM olu_mektup ORDER BY ileti_id").all();
for (const r of geri) {
  db.prepare("UPDATE is_kuyrugu SET durum='bekliyor', deneme=0 WHERE ileti_id=?").run(r.ileti_id);
  db.prepare("DELETE FROM olu_mektup WHERE olu_id=?").run(r.olu_id);
}
console.log(`geri surulen=${geri.length}  ikinci gecis: islenen=${tuket()}`);
console.log(`sonuc: islenen=${say("is_kuyrugu WHERE durum='islendi'")} ` +
            `bekleyen=${say("is_kuyrugu WHERE durum='bekliyor'")} olu mektup=${say("olu_mektup")}`);
```

```sh
node yeniden-surme.mjs
```

```
birinci gecis: islenen=18  bekleyen=0
  ileti=7 tur=gecikme_bildirimi deneme=3 kaynak=is_kuyrugu govde={"oduncId":7} son_hata=uye bulunamadi (odunc 7)
  ileti=13 tur=gecikme_bildirimi deneme=3 kaynak=is_kuyrugu govde={"oduncId":13} son_hata=uye bulunamadi (odunc 13)
geri surulen=2  ikinci gecis: islenen=2
sonuc: islenen=20 bekleyen=0 olu mektup=0
```

Ölü mektup kaydının kendisi hatanın tanısını veriyor: aynı türden iki ileti, aynı hata metni,
üçer deneme. Eksik üye kayıtları tamamlandıktan sonra iki ileti iş kuyruğuna geri sürüldü,
deneme sayaçları sıfırlandı ve ikinci geçişte işlendi. Yirmi iletinin yirmisi de sonunda
işlenmiş oldu.

**Yeniden sürme** (redrive) işleminin iki koşulu vardır. Deneme sayacı sıfırlanmalıdır, yoksa
ileti eşiği zaten aşmış olduğu için ilk hatada yeniden yalıtılır. Ve nedenin gerçekten
giderilmiş olması gerekir; giderilmeden yapılan geri sürme, aynı iletileri aynı sayıda
denemeyle ölü mektup kuyruğuna geri gönderir. İkinci koşul, geri sürmenin bir düğmeye değil,
bir tanıya bağlanmasını zorunlu kılar.

#### Eşik Değerini Seçmek

Eşik iki hatayı birbirinden ayırt edemez; yalnız deneme sayısına bakar. Bu yüzden değeri iki
kusur arasında bir seçimdir: küçük eşik, dördüncü denemede geçecek geçici bir hatayı kalıcı
sanıp yalıtır; büyük eşik, kalıcı hatalı iletiyi gereğinden çok kez dener.

Aşağıdaki koşum yirmi iletiden ikisini kalıcı bozuk, beşini üçüncü denemede geçen, dördünü
beşinci denemede geçen iletiler olarak kurar ve eşiği bir ile sekiz arasında değiştirir.

```js
// esik-secimi.mjs — esik degeri: gecici hatayi yanlis yalitmak ile bosa deneme arasindaki secim
const ILETILER = [];                       // gerekli = kacinci denemede basarili olur, 0 = hicbir zaman
for (let n = 1; n <= 20; n++) {
  const gerekli = n === 7 || n === 13 ? 0 : (n % 5 === 0 ? 5 : (n % 3 === 0 ? 3 : 1));
  ILETILER.push({ ileti_id: n, gerekli });
}

function kosum(esik) {
  let bosa = 0, yanlisYalitilan = 0, kaliciYalitilan = 0, islenen = 0;
  for (const i of ILETILER) {
    for (let deneme = 1; deneme <= esik; deneme++) {
      if (i.gerekli !== 0 && deneme >= i.gerekli) { islenen += 1; break; }
      bosa += 1;
      if (deneme === esik) {               // esik doldu: ileti olu mektup kuyruguna
        if (i.gerekli === 0) kaliciYalitilan += 1; else yanlisYalitilan += 1;
      }
    }
  }
  return { islenen, bosa, yanlisYalitilan, kaliciYalitilan };
}

console.log(`ileti=${ILETILER.length}  kalici bozuk=${ILETILER.filter((i) => i.gerekli === 0).length}  ` +
            `3 denemede gecen=${ILETILER.filter((i) => i.gerekli === 3).length}  ` +
            `5 denemede gecen=${ILETILER.filter((i) => i.gerekli === 5).length}`);
console.log("esik  islenen  bosa deneme  yanlis yalitilan  kalici yalitilan");
for (const esik of [1, 2, 3, 5, 8]) {
  const s = kosum(esik);
  console.log(`${String(esik).padStart(4)}${String(s.islenen).padStart(9)}${String(s.bosa).padStart(13)}` +
              `${String(s.yanlisYalitilan).padStart(18)}${String(s.kaliciYalitilan).padStart(18)}`);
}
```

```sh
node esik-secimi.mjs
```

```
ileti=20  kalici bozuk=2  3 denemede gecen=5  5 denemede gecen=4
esik  islenen  bosa deneme  yanlis yalitilan  kalici yalitilan
   1        9           11                 9                 2
   2        9           22                 9                 2
   3       14           28                 4                 2
   5       18           36                 0                 2
   8       18           42                 0                 2
```

Tablo iki yönde de tek yönlü. Eşik büyüdükçe yanlış yalıtılan ileti sayısı 9'dan 0'a iniyor,
boşa deneme sayısı 11'den 42'ye çıkıyor. İşlenen iş beşinci eşikte doyuma ulaşıyor: sekize
çıkarmak hiçbir ileti kazandırmıyor, yalnız iki kalıcı bozuk iletiyi üçer kez daha deniyor.

Bu, eşiğin nasıl seçileceğini de söyler. Doğru değer, işin gerçekten ihtiyaç duyduğu en yüksek
deneme sayısının biraz üstüdür; ondan sonrası saf israftır. "Gerçekten ihtiyaç duyulan deneme
sayısı" tahmin edilmez, ölçülür — başarılı işlerin deneme sayısı dağılımı bu değeri doğrudan
verir.

#### Yalıtmak Çözmek Değildir

Ölü mektup kuyruğunun tehlikesi, sorunu görünmezleştirmesidir. İletiler oradadır, kuyruk
temizdir, ölçütler sağlıklı görünür — ve üç üye bildirimini hiç almamıştır. Yalıtım, kaybı
sessiz olmaktan çıkarır ancak izlenirse.

Üç kural bunu sağlar. Ölü mektup kuyruğunun derinliği izlenen bir ölçüttür ve sıfırdan farklı
olması bir uyarı üretir; kuyruk derinliğinden farklı olarak burada "normal" bir taban değeri
yoktur. Her kaydın bir sahibi vardır: iletiyi hangi ekip üretiyorsa yalıtılan iletiyi de o
inceler. Ve kayıtların bir yaşı vardır — incelenmeden aylarca duran ölü mektup, incelenmeyecek
demektir.

Yalıtılan iletilerin **kümelenmesi** de bir tanı aracıdır. Aynı hata metnini taşıyan çok
sayıda kayıt tek bir kök nedene işaret eder ve tek tek incelenmesi gerekmez; dağınık hatalar
ise her birinin ayrı olduğunu söyler. Ölü mektup kaydında hata metninin saklanmasının asıl
karşılığı budur.

#### Özet

- Geçici hata koşullar değişince kaybolur ve yeniden denemeye yanıt verir; kalıcı hata
  iletinin kendisinden gelir ve her denemede aynı sonucu üretir.
- Eşiksiz yeniden denemede iki bozuk ileti sekiz turda 16, yirmi dört turda 48 boşa deneme
  üretti; başarılı iş her ikisinde de 18'de kaldı ve harcanan kapasitenin üst sınırı yoktu.
- Üç denemelik eşikle boşa deneme 6'da durdu, kuyruk boşaldı ve iki ileti ölü mektup
  kuyruğuna alındı; bölümlenmiş düzende eşik, bozuk iletinin durdurduğu bölümü yeniden akıtır.
- Ölü mektup kaydı gövdeyi, türü, deneme sayısını, son hata metnini ve kaynak kuyruğu taşır;
  neden giderildikten sonra deneme sayacı sıfırlanarak geri sürülen iki ileti işlendi.
- Eşik büyüdükçe yanlış yalıtılan ileti 9'dan 0'a indi, boşa deneme 11'den 42'ye çıktı; doğru
  değer, başarılı işlerin ihtiyaç duyduğu en yüksek deneme sayısının biraz üstüdür.

#### Sonraki Adım

Eşik, kaç kez deneneceğini söyledi; ne zaman deneneceğini söylemedi. Önceki ölçümlerde
denemeler arka arkaya yapıldı, aralarında hiç bekleme yoktu. Geçici hataların çoğu bir
kaynağın aşırı yüklenmesinden doğar ve beklemeden yapılan yeniden deneme o kaynağı daha da
yükler; üstelik aynı anda başarısız olan yüzlerce tüketici aynı anda yeniden deneyerek
yükü tek noktada toplar. Sonraki ders bekleme sürelerinin nasıl hesaplanacağını kurar:
üstel geri çekilme, üst sınır ve yeniden deneme dalgasını dağıtan sarsıntı.

### Zehirli Mesaj ve Yeniden Deneme

Source: https://academia.sh/tr/kurslar/eszamansiz-isleme/zehirli-mesaj-ve-yeniden-deneme

Tüketiciyi düşüren iletinin yalıtılması ve bekleme sürelerinin hesaplanması: deneme sayacının teslim anında artırılmasının zorunluluğu, anında–sabit–üstel çizelgelerin toplam bekleme ve deneme bütçesi karşılaştırması, sarsıntının yeniden deneme dalgasının tepesini düşürmesi ve görünmezlik süresiyle geri çekilmenin uyumu.

Önceki ders eşiğin kaç kez deneneceğini söylediğini, ne zaman deneneceğini söylemediğini
belirtti. Bu dersin iki konusu o boşluktan çıkıyor. Birincisi, eşiğin işlemediği bir hata
türü: iletiyi işleyen tüketiciyi ayakta bırakmayan ileti. İkincisi, denemeler arasındaki
beklemenin nasıl hesaplanacağı.

İkisi de aynı iş üzerinde ölçülecek: giden kutusundan alınan gecikme bildirimleri.

#### Zehirli Mesaj

Önceki dersteki bozuk iletiler nazik davranıyordu: bir hata fırlatıyor, tüketici hatayı
yakalıyor, deneme sayacını artırıyor ve sonraki iletiye geçiyordu. **Zehirli mesaj** (poison
message) bunu yapmaz. İşlenmesi tüketici sürecini sonlandırır — belleği tüketen bir gövde,
sonsuz döngüye giren bir çözümleme, yakalanamayan bir başarısızlık. Süreç öldüğü için hata
işleme kodu hiç çalışmaz.

Sonucu ölçülebilir bir kısır döngüdür. Süreç yeniden başlar, kuyruktan aynı iletiyi alır,
yine ölür. Deneme sayacı hata yakalandığında artırılıyorsa hiç artmaz; eşik hiçbir zaman
dolmaz; ölü mektup kuyruğu hiçbir zaman devreye girmez.

Aşağıdaki koşum beş iletilik bir kuyrukta üçüncü iletiyi öldürücü yapar ve sayacın iki farklı
konumunu karşılaştırır: hata yakalandığında artan sayaç ile teslim anında, iş başlamadan
artan sayaç.

```js
// zehirli-sayac.mjs — sureci olduren ileti: sayac hangi anda artarsa yalitim calisir
const OLDURUCU = new Set([3]);            // bu ileti tuketici surecini sonlandiriyor
const ESIK = 3;

// Tek bir surec omru: oldurucu iletide donus yapar, arkasindaki kod hic calismaz.
function surecOmru(kuyruk, sayacTeslimde, olu) {
  let teslim = 0;
  for (const i of kuyruk.filter((m) => m.durum === "bekliyor")) {
    teslim += 1;
    if (sayacTeslimde) {                  // sayac isten once, teslim aninda artar
      i.deneme += 1;
      if (i.deneme >= ESIK) { i.durum = "olu"; olu.push(i.id); continue; }
    }
    if (OLDURUCU.has(i.id)) return { teslim, oldu: true };
    i.durum = "islendi";                  // sayac hata yakalaninca artacakti; surec olunce artmaz
  }
  return { teslim, oldu: false };
}

function kosum(sayacTeslimde, enCokBaslatma = 12) {
  const kuyruk = [1, 2, 3, 4, 5].map((id) => ({ id, deneme: 0, durum: "bekliyor" }));
  const olu = [];
  let teslim = 0, olum = 0, baslatma = 0;
  for (let b = 1; b <= enCokBaslatma; b++) {
    baslatma = b;
    const r = surecOmru(kuyruk, sayacTeslimde, olu);
    teslim += r.teslim;
    if (r.oldu) olum += 1;
    if (kuyruk.every((m) => m.durum !== "bekliyor")) break;
  }
  return { baslatma, teslim, olum, olu: olu.length,
           islenen: kuyruk.filter((m) => m.durum === "islendi").length,
           kalan: kuyruk.filter((m) => m.durum === "bekliyor").length };
}

console.log("sayac konumu".padEnd(22) + "baslatma  teslim  surec olumu  islenen  olu mektup  kalan");
for (const [ad, teslimde] of [["hata yakalaninca", false], ["teslim aninda", true]]) {
  const s = kosum(teslimde);
  console.log(ad.padEnd(22) +
    `${String(s.baslatma).padStart(8)}${String(s.teslim).padStart(8)}${String(s.olum).padStart(13)}` +
    `${String(s.islenen).padStart(9)}${String(s.olu).padStart(12)}${String(s.kalan).padStart(7)}`);
}
```

```sh
node zehirli-sayac.mjs
```

```
sayac konumu          baslatma  teslim  surec olumu  islenen  olu mektup  kalan
hata yakalaninca            12      14           12        2           0      3
teslim aninda                3       7            2        4           1      0
```

İlk satırda on iki başlatmanın on ikisi ölümle bitti, yalnız iki ileti işlendi ve üç ileti
kuyrukta kaldı. On iki sayısı bir sonuç değil, koşumun üst sınırıdır: döngü orada durdurulduğu
için durdu, kendiliğinden değil. Ölü mektup sütunu sıfır — eşik vardı, ama sayaç hiç artmadığı
için hiçbir zaman dolmadı.

İkinci satırda üç başlatma yetti. Zehirli ileti iki kez teslim edildi, üçüncü teslimde sayaç
eşiğe ulaştığı için iş hiç başlatılmadan yalıtıldı; arkasındaki iki ileti de işlendi. Kuyruk
boşaldı.

Kural tek cümledir: **deneme sayacı, işin sonucundan bağımsız olarak teslim anında
artırılmalıdır.** Sayacı hata yoluna koymak, yalnız hata yolunun çalıştığı durumlar için
koruma sağlar. Önceki derste kurulan `deneme` alanının işten önce yazılmasının nedeni budur
ve aynı gerekçe kuyruğun teslim sayacı için de geçerlidir: 02. derste ölçtüğümüz teslim
sayısı `al` çağrısında artıyordu, işin bitişinde değil.

Yalıtım zehirli mesajın maliyetini sınırlar ama sıfırlamaz. İki süreç ölümü olmuştur ve o
süreçlerin o sırada elinde tuttuğu başka işler yarım kalmıştır. Bu yüzden zehirli mesaj
riski taşıyan işlerde ikinci bir savunma kullanılır: gövdenin işlenmeden önce doğrulanması.
Doğrulama Katmanları dersinde konan ölçüt burada da geçerlidir — biçimi bozuk gövde işe hiç
girmez, ölü mektup kuyruğuna doğrudan alınır.

#### Bekleme Süresinin Hesabı

Şimdiye kadarki koşumlarda denemeler arka arkaya yapıldı, aralarında hiç bekleme yoktu.
Geçici hataların çoğu bir kaynağın aşırı yüklenmesinden doğar ve beklemeden yapılan yeniden
deneme o kaynağı daha da yükler. Bekleme süresini veren kurala **geri çekilme çizelgesi**
denir.

```js
// geri-cekilme.mjs — bekleme cizelgeleri ve bir deneme butcesine sigan deneme sayisi
const TABAN = 500, TAVAN = 30_000;                 // ms
const CIZELGE = {
  "aninda": () => 0,
  "sabit 2 sn": () => 2000,
  "ustel": (n) => TABAN * 2 ** (n - 1),
  "ustel + tavan": (n) => Math.min(TAVAN, TABAN * 2 ** (n - 1)),
};
const ADLAR = Object.keys(CIZELGE);

console.log("deneme" + ADLAR.map((a) => a.padStart(15)).join(""));
for (let n = 1; n <= 8; n++)
  console.log(String(n).padStart(6) + ADLAR.map((a) => String(CIZELGE[a](n)).padStart(15)).join(""));

const toplam = (ad, adet) => Array.from({ length: adet }, (_, i) => CIZELGE[ad](i + 1))
  .reduce((t, x) => t + x, 0);
console.log("toplam" + ADLAR.map((a) => String(toplam(a, 8)).padStart(15)).join(""));

const BUTCE = 5 * 60 * 1000;                       // 5 dakikalik zaman butcesi
console.log("\ncizelge".padEnd(16) + "5 dk icinde sigan deneme  butce dolunca gecen sure");
for (const ad of ADLAR) {
  let gecen = 0, deneme = 0;
  while (gecen + CIZELGE[ad](deneme + 1) <= BUTCE && deneme < 1000) {
    deneme += 1; gecen += CIZELGE[ad](deneme);
  }
  console.log(ad.padEnd(16) + String(deneme === 1000 ? "sinirsiz" : deneme).padStart(24) +
              String(gecen).padStart(25) + " ms");
}
```

```sh
node geri-cekilme.mjs
```

```
deneme         aninda     sabit 2 sn          ustel  ustel + tavan
     1              0           2000            500            500
     2              0           2000           1000           1000
     3              0           2000           2000           2000
     4              0           2000           4000           4000
     5              0           2000           8000           8000
     6              0           2000          16000          16000
     7              0           2000          32000          30000
     8              0           2000          64000          30000
toplam              0          16000         127500          91500

cizelge        5 dk icinde sigan deneme  butce dolunca gecen sure
aninda                          sinirsiz                        0 ms
sabit 2 sn                           150                   300000 ms
ustel                                  9                   255500 ms
ustel + tavan                         14                   271500 ms
```

Dört çizelge dört ayrı davranış üretiyor. Anında yeniden deneme hiç beklemez: zaman bütçesi
hiçbir zaman dolmaz, dolayısıyla denemeye sınırı ancak eşik koyabilir. Kaynağın yükünü
azaltmak bir yana, hata anında yükü yoğunlaştırır.

Sabit aralık öngörülebilirdir ama iki uçta da yanlıştır: bir saniyede toparlanacak bir hata
için iki saniye beklemek gereksizdir, bir dakika sürecek bir kesintide iki saniyede bir
denemek kaynağı yüz elli kez boşuna yorar.

**Üstel geri çekilme** (exponential backoff) her denemede bekleme süresini katlayarak ikisini
de çözer: hata kısa sürerse erken denemeler onu yakalar, uzun sürerse denemeler seyrelir.
Ödediği bedel yedinci satırda görünüyor — bekleme 32 saniyeye çıkmış. Bu yüzden bir **tavan**
konur; tavanlı sürümde toplam bekleme 127,5 saniyeden 91,5 saniyeye iner ve beş dakikalık
bütçeye dokuz yerine on dört deneme sığar. Tavan, seyrelmenin sınırsızlaşmasını önler.

Frontend Mimarisi kursunun Yeniden Deneme ve Geri Çekilme dersinde aynı çizelgeler istemci
tarafı için kurulmuştu. Tüketici tarafındaki fark, bekleyen tarafın bir kullanıcı değil bir
ileti olmasıdır: bekleme süresince kuyruk derinliği artmaya devam eder ve gecikme yalnız o
iletiye değil, arkasındaki bütün işlere yansır.

#### Sarsıntı

Geri çekilme çizelgesi tek bir tüketici için doğru olsa da, çok sayıda tüketici aynı anda
başarısız olduğunda yeni bir sorun doğurur. Hepsi aynı çizelgeyi kullandığı için hepsi aynı
anda yeniden dener; kaynak toparlanmaya çalışırken art arda gelen dalgalarla karşılaşır.

Çözüm bekleme süresine rastgelelik katmaktır. Buna **sarsıntı** (jitter) denir. **Tam
sarsıntı** (full jitter) beklemeyi sıfır ile hesaplanan süre arasında rastgele seçer;
**ilişkisiz sarsıntı** (decorrelated jitter) her adımda bir önceki beklemeye bağlı, daha geniş
bir aralıktan seçer.

Aşağıdaki koşum aynı anda başarısız olan iki yüz tüketiciyi dört deneme boyunca izler ve
yeniden deneme anlarını yüz milisaniyelik kovalara böler. Üreteç sabit tohumludur; sayılar
her koşumda aynıdır.

```js
// sarsinti.mjs — ayni anda basarisiz olan 200 tuketicinin yeniden deneme dalgasi
const uretec = (tohum) => { let x = tohum; return () => (x = (x * 1103515245 + 12345) % 2147483648) / 2147483648; };
const TUKETICI = 200, DENEME = 4, KOVA = 100;      // kova genisligi 100 ms
const TABAN = 500, TAVAN = 30_000;

function dalga(kip, tohum = 20250720) {
  const rast = uretec(tohum);
  const anlar = [];
  for (let t = 0; t < TUKETICI; t++) {
    let an = 0, onceki = TABAN;
    for (let n = 1; n <= DENEME; n++) {
      const taban = Math.min(TAVAN, TABAN * 2 ** (n - 1));
      if (kip === "sarsintisiz") an += taban;
      else if (kip === "tam sarsinti") an += Math.round(rast() * taban);          // [0, taban]
      else {                                                                       // iliskisiz
        onceki = Math.min(TAVAN, Math.round(TABAN + rast() * (onceki * 3 - TABAN)));
        an += onceki;
      }
      anlar.push(an);
    }
  }
  const kova = new Map();
  for (const a of anlar) { const k = Math.floor(a / KOVA); kova.set(k, (kova.get(k) ?? 0) + 1); }
  return { istek: anlar.length, tepe: Math.max(...kova.values()), doluKova: kova.size,
           sonKova: Math.max(...kova.keys()) };
}

console.log("cizelge".padEnd(20) + "toplam istek  en yogun 100 ms  dolu kova  son kova");
for (const kip of ["sarsintisiz", "tam sarsinti", "iliskisiz sarsinti"]) {
  const s = dalga(kip);
  console.log(kip.padEnd(20) + String(s.istek).padStart(12) + String(s.tepe).padStart(17) +
              String(s.doluKova).padStart(11) + String(s.sonKova).padStart(10));
}
```

```sh
node sarsinti.mjs
```

```
cizelge             toplam istek  en yogun 100 ms  dolu kova  son kova
sarsintisiz                  800              200          4        75
tam sarsinti                 800               69         65        67
iliskisiz sarsinti           800               28        163       389
```

Üç çizelgede de toplam istek sayısı aynı: sekiz yüz. Değişen tek şey bu isteklerin zamana
nasıl yayıldığıdır. Sarsıntısız çizelgede sekiz yüz istek yalnız dört kovaya sığıyor ve her
kovada iki yüz istek var — iki yüz tüketicinin tamamı aynı yüz milisaniyede kapıyı çalıyor.
Tam sarsıntı tepeyi 69'a indiriyor ve isteği altmış beş kovaya yayıyor. İlişkisiz sarsıntı
tepeyi 28'e indiriyor, ama son kova 389 — yani yayılım otuz dokuz saniyeye uzuyor.

Ödünleşme buradadır. Sarsıntı, kaynağın gördüğü ani yükü düşürür; karşılığında bazı
iletilerin yeniden denenmesi geç kalır. Yükün tepesi kaynağı yeniden düşürecek büyüklükteyse
sarsıntı zorunludur; işin gecikmeye duyarlılığı yüksekse yayılım dar tutulur.

#### Deneme Bütçesi ve Görünmezlik Süresi

Yeniden deneme kuralı iki sınırla birlikte tanımlanır. **Deneme bütçesi** (retry budget)
sayıya bakar: eşik. Zaman bütçesi süreye bakar: iletinin ilk teslimden itibaren ne kadar süre
sonra ölü mektup kuyruğuna alınacağı. İkisi birlikte gerekir, çünkü tek başına eşik geri
çekilmeyle birleştiğinde süreyi öngörülemez kılar — tavanlı üstel çizelgede on dört deneme
dört buçuk dakikadan uzun sürüyordu, tavansızda dokuz deneme aynı süreyi buluyordu.

İkinci uyum kuyruğun kendisiyledir. 02. derste kurulan görünmezlik süresi, alınan iletinin ne
kadar süre başkasına verilmeyeceğini söylüyordu. Tüketici geri çekilme süresi boyunca iletiyi
elinde tutup bekliyorsa, bu bekleme görünmezlik süresinden uzun olduğu anda ileti başka bir
tüketiciye görünür olur ve aynı iş koşut yürür. Doğru yerleşim, tüketicinin beklerken iletiyi
elinde tutmamasıdır: ileti kuyruğa geri bırakılır ve yeniden görünür olacağı an geri çekilme
süresi kadar ileriye alınır. Böylece bekleme kuyruğun kendi zaman düzleminde tutulur ve
tüketici süreci bu sırada başka iş yapar.

Üçüncü bir sınır hata oranıyla ilgilidir. Bir kaynak toptan erişilemez hâle geldiğinde her
ileti aynı hatayı alır ve yeniden denemeler tek başına bir yük kaynağına dönüşür. Bu durumda
tek tek iletilerin çizelgesi değil, tüketicinin tamamının durması gerekir; yukarıda anılan
Yeniden Deneme ve Geri Çekilme dersinde tanıtılan **devre kesici** (circuit breaker) kalıbının
çözdüğü sorun budur.

#### Özet

- Zehirli mesaj yalnız başarısız olan ileti değildir; tüketici sürecini sonlandırdığı için
  hata işleme kodunu hiç çalıştırmaz.
- Sayaç hata yakalandığında artırıldığında on iki başlatmanın on ikisi ölümle bitti, iki ileti
  işlendi ve üç ileti kuyrukta kaldı; teslim anında artırıldığında üç başlatma yetti, ileti
  üçüncü teslimde yalıtıldı ve kuyruk boşaldı.
- Tavanlı üstel geri çekilme sekiz denemede toplam beklemeyi 127,5 saniyeden 91,5 saniyeye
  indirdi ve beş dakikalık bütçeye dokuz yerine on dört deneme sığdırdı.
- İki yüz tüketicinin sekiz yüz yeniden denemesi sarsıntısız çizelgede dört kovaya yığıldı ve
  tepe 200 oldu; tam sarsıntı tepeyi 69'a, ilişkisiz sarsıntı 28'e indirdi ve yayılımı
  otuz dokuz saniyeye uzattı.
- Deneme bütçesi ile zaman bütçesi birlikte tanımlanır; geri çekilme süresi görünmezlik
  süresini aşmamalı, bekleme tüketicide değil kuyruğun zaman düzleminde tutulmalıdır.

#### Sonraki Adım

Mesaj artık dayanıklıdır. Kalıcı bir yere yazılıyor, birden çok ilgiliye ulaşıyor, sırası
gerektiği yerde korunuyor, tekrarı etkisiz kılınıyor, işlenemeyeni yalıtılıyor ve yeniden
denemesi ölçülü bir çizelgeye bağlanıyor. Bütün bu ölçümlerde bir taraf ise hep verili
kabul edildi: iletiyi işleyen tüketici. Kaç tane işçi süreci vardır, biri saniyede kaç ileti
bitirir, sayıları artırıldığında ne kazanılır, üretim hızı kapasitelerini aştığında ne olur?
Sonraki konu tüketiciyi bir ölçekleme birimi olarak ele alır ve İşçi Süreçleri dersiyle
başlar.

## Arka Plan İşleri

Ölçekleme birimi olarak işçi süreci ve düşen işçinin gözetimi, yineleyen görev tanımı ile çakışmayı önleyen kilit, uzun süren işte ilerleme bildirimi ve iptal, üretim hızının tüketim hızını aştığı yerde geri basınç, aşırı yükte denetimli reddetmeyi sağlayan yük boşaltma.

### İşçi Süreçleri

Source: https://academia.sh/tr/kurslar/eszamansiz-isleme/isci-surecleri

İşin mesajdan ayrı bir ölçekleme birimi olarak ele alınması: iş havuzunun kurulması, işçi sayısının işlem hacmine etkisinin ölçülmesi, düşen işçinin gözetimle yerine konması ve zarif kapanma.

Bir önceki ders mesajın kendisini dayanıklı hale getirmeyi tamamladı: deneme sayacı,
üstel geri çekilme, sarsıntı ve ölü mektup kuyruğu. Artık bir mesaj kaybolmuyor,
başarısız olduğunda yeniden deneniyor, düzelmiyorsa yalıtılıyor.

Bu güvencelerin tamamı mesajın kendisiyle ilgiliydi. Mesajı **işleyen taraf** ise hâlâ
adsız bir "tüketici" olarak duruyor. Oysa o taraf bir süreçtir: belleği vardır, çöker,
yeniden başlar, kapatılır. Kaç tane olacağı bir karardır ve bu karar servisin saatte kaç
iş bitireceğini belirler. Bu ders tüketiciyi bir **ölçekleme birimi** (unit of scaling)
olarak ele alır ve o birimin kaç tane olması gerektiğini ölçmeyi öğretir.

#### İşçi Nedir

**İşçi** (worker), kuyruktan iş alıp işleyen bağımsız bir çalıştırma birimidir. Ayrı bir
süreç, ayrı bir iş parçacığı ya da ayrı bir kap olabilir; ortak yanı, **kendi başına
düşebilmesi ve kendi başına çoğaltılabilmesidir**.

İşçiyi istek işleyen sunucudan ayıran üç özellik vardır. İşçinin bir istemcisi yoktur;
kimse yanıtı beklemez, dolayısıyla iş uzun sürebilir. İşçinin iş kaynağı ağ dinleyicisi
değil kuyruktur; yükü kabul etmeyi reddedemez, yalnız yavaş tüketebilir. Ve işçi
**yatay** çoğaltılır: aynı kuyruktan besleneceği için ikinci bir işçi eklemek herhangi
bir eşgüdüm gerektirmez.

Kütüphane ödünç servisinde arka plana taşınan işler bellidir: gecikme bildirimlerinin
gönderilmesi, aylık ödünç raporunun üretilmesi, yüklenen kapak görselinin işlenmesi, şube
stok özetinin yeniden hesaplanması. Bu derste ilkini örnek alıyoruz.

#### İşin Kendisi

İşçi süreci tek bir sorumluluk taşır: gelen bir iş tanımını çalıştırmak ve sonucu
bildirmek. Kuyruğun nerede durduğunu, işin kaç kez denendiğini, kaç işçi olduğunu
bilmez.

```js
// isci.mjs — bir isi alir, yapar, sonucu bildirir; havuz.mjs onu fork eder
if (!process.send) {
  console.log("bu dosya tek basina calismaz; havuz.mjs onu fork eder");
  process.exit(0);
}

const SIRA = Number(process.argv[2]);            // havuzun isciye verdigi sira numarasi
const bekle = (ms) => new Promise((c) => setTimeout(c, ms));

// Gecikme bildirimi gonderimi: disariya bir cagri yapan, 100 ms suren is.
async function bildirimGonder(is) {
  if (is.bozuk) process.exit(1);                 // is surecin kendisini dusuruyor
  await bekle(100);
  return `uye ${is.uyeId} bilgilendirildi`;
}

process.on("message", async (is) => {
  const sonuc = await bildirimGonder(is);
  process.send({ tur: "bitti", isId: is.isId, sira: SIRA, sonuc });
});

process.on("SIGTERM", () => process.exit(0));    // havuz kapatirken is yarida kalmaz
```

```
bu dosya tek basina calismaz; havuz.mjs onu fork eder
```

Dosya doğrudan çalıştırıldığında `process.send` tanımsızdır ve işçi bunu söyleyip çıkar.
Anlamlı çalıştığı tek durum, bir üst sürecin onu çatallamasıdır.

`bildirimGonder` içindeki 100 milisaniyelik bekleme, dışarıya yapılan bir çağrının
yerine geçer. İşin **girdi–çıktı bağımlı** (I/O-bound) olduğunu belirtir: süre işlemci
kullanılarak değil, cevabı beklenerek geçer. Bu ayrım birazdan işçi sayısı kararında
belirleyici olacak.

#### Havuz ve Çekme Temelli Dağıtım

İşleri işçilere bölüştürmenin iki yolu vardır. **İtme temelli** dağıtımda üst süreç
işleri baştan paylaştırır: on iki iş, dört işçi, her birine üç iş. Bu yol yalnız işlerin
süreleri eşitse verimlidir; biri uzun sürerse o işçi çalışırken diğerleri boş bekler.
**Çekme temelli** (pull-based) dağıtımda ise işçi bir işi bitirdiğinde sıradakini ister.
Yük kendiliğinden dengelenir.

Aşağıdaki havuz çekme temellidir: `dagit` her çağrıldığında kuyruğun başındaki işi
gönderir, kuyruk boşaldığında işçiyi kapatır.

```js
// havuz.mjs — sabit sayida isci fork eder, is kuyrugunu onlara dagitir, sureyi olcer
import { fork } from "node:child_process";

const ISLER = Array.from({ length: 12 }, (_, i) => ({
  isId: i + 1, oduncId: 500 + i, uyeId: 100 + i,
}));

function havuzCalistir(isciSayisi, isler) {
  return new Promise((coz) => {
    const kuyruk = [...isler];
    const bitenler = [];
    const isciler = [];
    const basla = performance.now();

    const dagit = (isci) => {
      const is = kuyruk.shift();
      if (is) isci.send(is);
      else isci.kill("SIGTERM");
    };

    for (let s = 1; s <= isciSayisi; s++) {
      const isci = fork(new URL("isci.mjs", import.meta.url), [String(s)]);
      isci.on("message", (ileti) => {
        bitenler.push(ileti);
        if (bitenler.length === isler.length) {
          const sure = performance.now() - basla;
          for (const i of isciler) i.kill("SIGTERM");
          coz({ sure, bitenler });
        } else dagit(isci);
      });
      isciler.push(isci);
      dagit(isci);
    }
  });
}

for (const n of [1, 2, 4]) {
  const { sure, bitenler } = await havuzCalistir(n, ISLER);
  const dilim = Math.floor(sure / 100);   // her is 100 ms surer; dilim = 100 ms
  console.log(`isci=${n}  is=${bitenler.length}  sure=${dilim} dilim`);
}
```

```
isci=1  is=12  sure=12 dilim
isci=2  is=12  sure=6 dilim
isci=4  is=12  sure=3 dilim
```

Süre, tek bir işin süresi olan 100 milisaniyelik **dilimler** cinsinden yazdırılıyor;
böylece sonuç makinenin hızından bağımsız okunabilir hale gelir. Dilim sayısı aşağı
yuvarlanır, dolayısıyla süreç başlatma gideri sayıya yansımaz. Yavaş bir makinede
dilim sayıları bir üste kayabilir.

Okunacak şey oran: on iki iş bir işçiyle on iki dilim, iki işçiyle altı, dört işçiyle üç
dilim sürüyor. İşçi sayısı iki katına çıktığında süre yarıya iniyor. Bu, işin
**bölünebilir** olmasının doğrudan sonucudur: bir üyeye bildirim göndermek başka bir
üyeye bildirim göndermeyi beklemez.

##### Ölçeklemenin Sınırı

Oranın sonsuza kadar sürmediği iki nokta vardır.

Birincisi işin türüdür. Yukarıdaki iş girdi–çıktı bağımlıydı: işçi çoğu zaman cevabı
bekliyordu, o yüzden işçi sayısı çekirdek sayısını aşsa da kazanç sürdü. İş **işlemci
bağımlı** (CPU-bound) olsaydı — örneğin kapak görselinin yeniden boyutlandırılması —
kazanç çekirdek sayısında durur, o noktadan sonra işçi eklemek yalnız bağlam değiştirme
gideri üretirdi.

İkincisi paylaşılan kaynaktır. On iki işçi aynı veritabanı bağlantı havuzundan besleniyorsa
darboğaz işçi sayısı değil bağlantı sayısıdır. Bildirim gönderimi tek bir dış servise
gidiyorsa darboğaz o servisin kabul ettiği eşzamanlılıktır. İşçi sayısını artırmak bu iki
durumda toplam süreyi kısaltmaz, yalnız beklemeyi işçilere dağıtır.

Bu yüzden işçi sayısı bir tahmin değil, ölçüm sonucudur: sayı artırılır, toplam süre
ölçülür, süre düşmeyi bıraktığı yerde durulur.

#### Düşen İşçi ve Gözetim

İşçi bağımsız bir süreç olduğu için düşebilir: bellek biter, işletim sistemi onu
sonlandırır, işlediği iş sürecin kendisini çökertir. Havuzun buna karşı iki görevi
vardır. Düşen işçinin **yerine yenisini koymak** ve o işçinin **üzerindeki işi
kaybetmemek**.

İkincisi kritik olandır. İşçi çökerken hiçbir şey bildirmez; havuz yalnız `exit`
olayını görür. O anda işçinin hangi işi taşıdığı bilinmiyorsa iş sessizce kaybolur. Bu
yüzden havuz her işçinin üzerindeki işi ayrı bir eşlemede tutar.

```js
// dayanikli.mjs — isci dustugunde yerine yenisi konur, uzerindeki is geri kuyruga alinir
import { fork } from "node:child_process";

const ISLER = [
  { isId: 1, oduncId: 500, uyeId: 100 },
  { isId: 2, oduncId: 501, uyeId: 101 },
  { isId: 3, oduncId: 502, uyeId: 102, bozuk: true },
  { isId: 4, oduncId: 503, uyeId: 103 },
  { isId: 5, oduncId: 504, uyeId: 104 },
];
const EN_COK_DENEME = 2;

const kuyruk = [...ISLER];
const deneme = new Map();                        // isId -> kac kez denendi
const bitenler = [], oluMektup = [];
const uzerindeki = new Map();                    // isci -> o an isledigi is
let acik = 0, sonrakiSira = 1;

function isciYarat() {
  const isci = fork(new URL("isci.mjs", import.meta.url), [String(sonrakiSira++)]);
  acik++;
  isci.on("message", (m) => { uzerindeki.delete(isci); bitenler.push(m.isId); dagit(isci); });
  isci.on("exit", () => {
    acik--;
    const is = uzerindeki.get(isci);             // dolu ise: isci is uzerindeyken coktu
    if (is) {
      uzerindeki.delete(isci);
      if (deneme.get(is.isId) < EN_COK_DENEME) kuyruk.push(is);
      else oluMektup.push(is.isId);
      isciYarat();                               // coken iscinin yerine yenisi
    }
    if (acik === 0) rapor();
  });
  dagit(isci);
}

function dagit(isci) {
  const is = kuyruk.shift();
  if (!is) return isci.kill("SIGTERM");
  deneme.set(is.isId, (deneme.get(is.isId) ?? 0) + 1);
  uzerindeki.set(isci, is);
  isci.send(is);
}

function rapor() {
  console.log("biten isler       :", bitenler.sort((a, b) => a - b).join(", "));
  console.log("olu mektup kuyrugu:", oluMektup.join(", ") || "-");
  console.log("yaratilan isci    :", sonrakiSira - 1);
  console.log("3 numarali deneme :", deneme.get(3));
}

isciYarat();
isciYarat();
```

```
biten isler       : 1, 2, 4, 5
olu mektup kuyrugu: 3
yaratilan isci    : 4
3 numarali deneme : 2
```

Üç numaralı iş işçiyi çökertiyor. Havuz `exit` olayında işi geri kuyruğa alıyor ve
düşen işçinin yerine yenisini yaratıyor. İkinci deneme de aynı sonucu verdiği için iş
deneme sınırını aşıyor ve önceki derste tanıtılan ölü mektup kuyruğuna gidiyor. Diğer
dört iş etkilenmiyor; toplamda dört işçi yaratılıyor, ikisi başlangıçtaki, ikisi
çökenlerin yerine gelen.

Buradaki kalıbın adı **gözetim**dir (supervision): işi yapan birim ile onun yaşamını
yöneten birim ayrılır. İşçi kendi hatasını onarmaya çalışmaz, çöker; havuz onarır. Bu
ayrım işçinin kodunu belirgin ölçüde yalınlaştırır.

Gözetimin gizli bir maliyeti vardır. Geri kuyruğa alınan iş, çökme anında **kısmen
yapılmış** olabilir: bildirim gönderilmiş, ancak gönderildiği kaydedilmemiş olabilir. Bu
yüzden yeniden denenen her iş, Web API Tasarımı kursunda tanıtılan tekillik anahtarıyla
korunmalıdır; aksi hâlde gözetim, üyeye iki bildirim gönderme pahasına dayanıklılık satın
alır.

#### Düzgün Kapanma

Havuz işçiyi `SIGTERM` ile kapatıyor. İşçi de bu sinyali karşılayıp çıkıyor. Sinyalin
karşılanmaması durumunda süreç varsayılan davranışla anında sonlanır ve üzerindeki iş
yarıda kalır.

**Zarif kapanma** (graceful shutdown) üç adımdır: yeni iş almayı bırak, elindeki işi
bitir, sonra çık. Uygulamada üst süreç önce `SIGTERM` gönderir, işçinin bitirmesi için
sınırlı bir süre tanır, süre dolarsa `SIGKILL` ile zorlar. Bu ikinci sinyal işçi
tarafından karşılanamaz; bu yüzden tanınan sürenin en uzun işten büyük olması gerekir.

Dağıtım araçlarının işçi kaplarını durdururken izlediği sıra da budur. İşçinin sinyali
karşılamaması, her yeniden dağıtımda yarıda kalmış iş üretir.

#### Özet

- İşçi, kuyruktan iş alıp işleyen bağımsız bir çalıştırma birimidir; kendi başına düşebilir ve kendi başına çoğaltılabilir olması onu bir ölçekleme birimi yapar.
- Çekme temelli dağıtımda işçi bir işi bitirdiğinde sıradakini ister; yük, işlerin süreleri eşit olmasa da dengelenir.
- İşçi sayısının kazancı ölçülerek bulunur: girdi–çıktı bağımlı işte çekirdek sayısının ötesine geçebilir, işlemci bağımlı işte orada durur, paylaşılan kaynak varsa çok daha erken durur.
- Gözetim, düşen işçinin yerine yenisini koyar ve üzerindeki işi geri kuyruğa alır; bunun bedeli işin kısmen yapılmış olma ihtimalidir, bu yüzden yeniden denenen iş tekillik anahtarıyla korunur.
- Zarif kapanma sırası yeni iş almayı bırakmak, elindekini bitirmek ve çıkmaktır; tanınan süre en uzun işten kısa olursa her kapanış yarım iş üretir.

#### Sonraki Adım

Bu derste işler dışarıdan geldi: havuz elindeki listeyi dağıttı. Arka plan işlerinin
büyük bölümü ise dışarıdan gelmez, **takvimden** gelir. Gecikme bildirimi her sabah
altıda, aylık ödünç raporu her ayın birinde, şube stok özeti her on beş dakikada bir
üretilir. Bu işleri bir zaman ölçütüne bağlamak tek satırlık bir tanım gibi görünür, ama
altında iki soru vardır: yineleme nasıl tanımlanır ve aynı görevin iki koşumu üst üste
bindiğinde ne olur. Sonraki ders yineleyen görev tanımını kurar ve çakışmayı ölçerek
önler.

### Zamanlanmış Görevler

Source: https://academia.sh/tr/kurslar/eszamansiz-isleme/zamanlanmis-gorevler

Takvimden tetiklenen işlerin tanımı ve tehlikeleri: yineleme kuralının hesaplanması, ay sonu ve saat dilimi tuzakları, sabit hız ile sabit gecikme ayrımı ve iki koşumun üst üste binmesini kira kilidiyle önleyen ölçülmüş çözüm.

Bir önceki ders işleri dışarıdan aldı: havuzun elinde bir liste vardı, onu işçilere
dağıttı. Arka plan işlerinin önemli bir bölümü ise dışarıdan gelmez, **takvimden** gelir.
Gecikme bildirimi her sabah altıda, şube stok özeti her on beş dakikada bir, aylık ödünç
raporu her ayın birinde üretilir.

Bunu "bir zamanlayıcı kur ve işi çağır" diye özetlemek yanıltıcıdır. Zamanlanmış görevin
iki zor sorusu vardır. Birincisi yinelemenin nasıl tanımlandığı: "her ayın 31'i" olmayan
bir tarihe düştüğünde ne olur, yerel saat değiştiğinde koşum kaçar mı. İkincisi
çakışmadır: bir koşum, bir sonraki tetiklenmeden bitmezse iki koşum aynı anda çalışır ve
aynı raporu iki kez üretir. Bu ders ikisini de kurar; ikincisini ölçerek çözer.

#### Yineleme Tanımı

Yineleyen görev iki parçadan oluşur: **ne yapılacağı** ve **ne zaman yapılacağı**.
İkincisi bir veri yapısıdır ve tek bir işlevle sınanabilir: "şu andan sonraki koşum
hangi an".

```js
// zamanlama.mjs — yineleyen gorev tanimindan sonraki kosum anlarini uretir (hepsi UTC)
const TANIM = {
  gecikmeBildirimi: { tur: "gunluk", saat: 6, dakika: 0 },
  stokOzeti: { tur: "aralik", dakika: 15 },
  aylikRapor: { tur: "aylik", gun: 1, saat: 2, dakika: 30 },
  ayinSonu: { tur: "aylik", gun: 31, saat: 2, dakika: 30 },
};

const DK = 60_000;

function sonraki(tanim, su) {
  const d = new Date(su);
  if (tanim.tur === "aralik") {
    const adim = tanim.dakika * DK;
    return new Date(Math.floor(su / adim) * adim + adim);
  }
  if (tanim.tur === "gunluk") {
    const bugun = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(),
                           tanim.saat, tanim.dakika);
    return new Date(bugun > su ? bugun : bugun + 24 * 60 * DK);
  }
  // aylik: bu ayin gunu gecmisse sonraki ayin ayni gunu
  const buAy = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), tanim.gun,
                        tanim.saat, tanim.dakika);
  return new Date(buAy > su ? buAy
    : Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, tanim.gun, tanim.saat, tanim.dakika));
}

const BASLANGIC = Date.parse("2026-01-31T23:47:00Z");   // olcumun sabit baslangic ani
for (const [ad, tanim] of Object.entries(TANIM)) {
  let an = BASLANGIC;
  const anlar = [];
  for (let i = 0; i < 3; i++) {
    an = sonraki(tanim, an).getTime();
    anlar.push(new Date(an).toISOString().slice(0, 16).replace("T", " "));
  }
  console.log(ad.padEnd(17), anlar.join("  "));
}
```

```
gecikmeBildirimi  2026-02-01 06:00  2026-02-02 06:00  2026-02-03 06:00
stokOzeti         2026-02-01 00:00  2026-02-01 00:15  2026-02-01 00:30
aylikRapor        2026-02-01 02:30  2026-03-01 02:30  2026-04-01 02:30
ayinSonu          2026-03-03 02:30  2026-03-31 02:30  2026-05-01 02:30
```

Başlangıç anı sabit bir değişmez olarak yazıldığı için çıktı her koşumda aynıdır;
zamanlama kuralı böylece sınanabilir bir işleve dönüşür. "Şimdi" değerini işlevin
dışından vermek, zamanlamayı sınanabilir kılan tek karardır.

İlk üç satır beklendiği gibi. Dördüncü satır beklendiği gibi **değil**.

##### Ay Sonu Tuzağı

`ayinSonu` tanımı "her ayın 31'i saat 02:30" diyor. Üretilen anlar 3 Mart, 31 Mart ve
1 Mayıs. Şubat'ın 31'i yok; `Date.UTC(2026, 1, 31)` taşan günü sonraki aya aktarıp
3 Mart'ı üretiyor. Nisan'ın 31'i de yok, o da 1 Mayıs'a kayıyor.

Bu, hesaplamanın hatası değil, **tanımın eksikliğidir**. Ayın son gününe bağlanan bir
görev için iki politika arasında seçim yapmak gerekir: olmayan günü **atlamak** (o ay
koşum yok) ya da ayın **son gününe kelepçelemek** (28, 29 ya da 30). Hangisinin doğru
olduğu işe bağlıdır — aylık rapor kelepçelenmeli, ayın 31'inde biten bir aboneliğin
yenilenmesi ise atlanmamalıdır. Yazılmayan politika, çalışma zamanının taşma davranışına
teslim edilmiş demektir.

##### Saat Dilimi

Yukarıdaki hesabın tamamı UTC üzerinde yapıldı. Yerel saate bağlanan bir görev iki ek
sorun üretir. Yaz saati uygulamasının ileri alındığı gece, yerel saatle 02:30 **hiç
yaşanmaz**; o gece koşum kaçar. Geri alındığı gece ise 02:30 **iki kez** yaşanır; koşum
iki kez tetiklenir.

Bu yüzden zamanlama kararı iki katmana ayrılır. Görevin tetiklenme anı UTC üzerinde
tutulur ve saklanır; yerel saat yalnız kullanıcıya gösterilirken kullanılır. Yerel takvim
gerçekten gerekiyorsa (örneğin "şube kapanışından sonra") saat dilimi kimliği tanımın
parçası olur ve dönüşüm her koşumda yeniden yapılır.

#### Sabit Hız ve Sabit Gecikme

Yinelemenin ikinci ayrıntısı, bir sonraki koşumun neye göre planlandığıdır.

**Sabit hız** (fixed rate) zamanlamada koşumlar takvime çakılıdır: 02:30, 03:30, 04:30.
Koşum ne kadar sürerse sürsün bir sonrakinin anı değişmez. **Sabit gecikme** (fixed
delay) zamanlamada ise bekleme, önceki koşumun **bitişinden** sayılır: koşum 40 dakika
sürdüyse ve gecikme bir saatse, sonraki koşum 04:10'da başlar.

Aradaki fark kaymadır. Kırk dakika süren saatlik bir görev, sabit gecikmeyle günde 24
değil yaklaşık 14 kez koşar ve koşum anları her gün ilerler; bu ilerlemeye **kayma**
(drift) denir. Sabit hızda kayma yoktur, ama karşılığında koşumların üst üste binmesi
mümkün hâle gelir: koşum bir saatten uzun sürerse bir sonraki tetikleme, önceki hâlâ
çalışırken gelir.

Yani sabit hız ile sabit gecikme arasındaki seçim, kayma ile çakışma arasındaki
seçimdir. Kaymayı kabul edemiyorsak çakışmayı ayrıca engellemek zorundayız.

#### Çakışma

Çakışma yalnız uzun koşumdan doğmaz. İkinci ve daha sık karşılaşılan kaynak, uygulamanın
**birden çok örnek** olarak dağıtılmasıdır. Zamanlayıcı uygulamanın içinde çalışıyorsa,
üç örneğe dağıtılmış bir servis her sabah altıda gecikme bildirimini üç kez gönderir.
Üçüncü kaynak yeniden dağıtımdır: eski örnek kapanmadan yenisi ayağa kalkarsa tetikleme
ikiye çıkar.

Üçünün de çözümü aynıdır: görev, koşmadan önce **koşma hakkını** almak zorundadır.

#### Kira Kilidi

Bu hakkı veren yapıya **kilit** (lock) denir. Arka plan görevlerinde kullanılan biçimi
**kira kilidi**dir (lease lock): kilit süresizce değil, belirli bir süre için verilir.
Süre şart, çünkü kilidi tutan süreç çökerse kilidi bırakacak kimse kalmaz; kira, o
durumda kilidin kendiliğinden düşmesini sağlar.

Kilidin doğruluğu tek bir koşula bağlıdır: alma işlemi **atomik** olmalıdır. İki koşum
"kilit var mı" diye sorup ikisi de "yok" cevabını alırsa kilit hiçbir işe yaramaz. Bu
yüzden sorgu ile yazma tek bir ifadede birleştirilir.

```js
// gorev.mjs — aylik odunc raporunu ureten yineleyen gorev
//   node gorev.mjs <etiket>           kilitsiz kosum
//   node gorev.mjs <etiket> --kilit   kira kilidiyle kosum
import { DatabaseSync } from "node:sqlite";
import { appendFileSync } from "node:fs";

const ETIKET = process.argv[2];
const KILITLI = process.argv.includes("--kilit");
const KIRA_MS = 5000;                              // kilidin kira suresi

const db = new DatabaseSync("zamanlama.db");
db.exec("PRAGMA busy_timeout = 5000");
db.exec(`CREATE TABLE IF NOT EXISTS kilit (
           ad TEXT PRIMARY KEY, sahip TEXT NOT NULL, bitis INTEGER NOT NULL)`);

// Kilit alma tek bir atomik yazmadir: satir yoksa olusur, varsa yalniz kirasi
// dolmussa devralinir. Iki kosum ayni anda denerse yalniz biri satiri degistirir.
const kilitAl = (ad, sahip) => {
  const simdi = Date.now();
  return db.prepare(
    `INSERT INTO kilit (ad, sahip, bitis) VALUES (?, ?, ?)
       ON CONFLICT(ad) DO UPDATE SET sahip = excluded.sahip, bitis = excluded.bitis
       WHERE kilit.bitis < ?`).run(ad, sahip, simdi + KIRA_MS, simdi).changes === 1;
};
const kilitBirak = (ad, sahip) =>
  db.prepare("DELETE FROM kilit WHERE ad = ? AND sahip = ?").run(ad, sahip);

function raporUret() {
  appendFileSync("iz.log", `basladi ${ETIKET}\n`);
  const son = Date.now() + 300;                    // 300 ms suren rapor uretimi
  while (Date.now() < son);
  appendFileSync("iz.log", `bitti ${ETIKET}\n`);
}

if (!KILITLI) raporUret();
else if (kilitAl("aylik-rapor", ETIKET)) {
  try { raporUret(); } finally { kilitBirak("aylik-rapor", ETIKET); }
} else {
  appendFileSync("iz.log", `atlandi ${ETIKET}\n`);
}
```

Üç ayrıntı kilidi doğru yapan şeydir. Kilit **adı** görev adıdır, süreç adı değil; farklı
örnekler aynı adı kullanır. Kilidi bırakma `sahip` sütununu da sorgular, böylece kirası
dolduğu için kilidi devralınmış bir koşum, başkasının kilidini silemez. Ve bırakma
`finally` içindedir, dolayısıyla hata durumunda da çalışır.

#### Ölçüm

Kilidin işe yarayıp yaramadığı iddia edilmez, ölçülür. Aşağıdaki sürücü aynı görevi iki
kez aynı anda başlatır ve `iz.log` dosyasındaki başlama–bitiş imlerinden **eş zamanlı
koşum zirvesini** hesaplar.

```js
// surucu.mjs — ayni gorevi iki kez ayni anda baslatir ve izi cozumler
import { spawn } from "node:child_process";
import { writeFileSync, readFileSync, rmSync } from "node:fs";

const calistir = (argv) =>
  new Promise((c) => spawn("node", argv, { stdio: "ignore" }).on("exit", c));

function cozumle() {
  let acik = 0, zirve = 0, kosum = 0, atlanan = 0;
  for (const s of readFileSync("iz.log", "utf8").trim().split("\n")) {
    if (s.startsWith("basladi")) { acik++; kosum++; zirve = Math.max(zirve, acik); }
    else if (s.startsWith("bitti")) acik--;
    else atlanan++;
  }
  return { kosum, atlanan, zirve };
}

for (const kip of ["kilitsiz", "kilitli"]) {
  writeFileSync("iz.log", "");
  rmSync("zamanlama.db", { force: true });
  const ek = kip === "kilitli" ? ["--kilit"] : [];
  await Promise.all([
    calistir(["gorev.mjs", "A", ...ek]),
    calistir(["gorev.mjs", "B", ...ek]),
  ]);
  const { kosum, atlanan, zirve } = cozumle();
  console.log(`${kip.padEnd(9)} kosum=${kosum} atlanan=${atlanan} es zamanli zirve=${zirve}`);
}
```

```
kilitsiz  kosum=2 atlanan=0 es zamanli zirve=2
kilitli   kosum=1 atlanan=1 es zamanli zirve=1
```

Kilitsiz kipte iki koşum da rapor üretiyor ve zirve 2 çıkıyor: aylık ödünç raporu iki kez
hesaplanmış demektir. Kilitli kipte koşum sayısı 1, atlanan 1 ve zirve 1. İkinci koşum
kilidi alamadığı için işi yapmadan çıkıyor.

Atlanan koşumun **hata sayılmadığına** dikkat edin. Zamanlanmış görevlerde kilidi
alamamak beklenen bir sonuçtur; kütüklenir ama uyarı üretmez. Uyarı üreten şey, aynı
görevin uzun süre hiç koşamamış olmasıdır.

#### Kaçırılan Koşum

Kilit çakışmayı önler ama bir soruyu açık bırakır: koşum kilit yüzünden ya da servis kapalı
olduğu için hiç yapılamadıysa, sonradan telafi edilecek mi.

İki politika vardır. **Telafisiz** politikada kaçırılan koşum unutulur; bir sonraki
tetiklemede normal koşum yapılır. Şube stok özeti gibi son durumu hesaplayan görevler
buna uygundur, çünkü yeni koşum eskisinin sonucunu zaten kapsar. **Telafili** politikada
kaçırılan her aralık için ayrı koşum üretilir. Aylık ödünç raporu buna uygundur, çünkü
her ayın raporu ayrı bir çıktıdır ve biri atlanırsa yerine geçen olmaz.

Telafili politikanın tehlikesi, uzun bir kesintiden sonra biriken koşumların hepsinin bir
anda tetiklenmesidir. Bu yüzden telafi bir üst sınırla verilir: en fazla son N aralık
telafi edilir, gerisi kütüğe düşer.

#### Özet

- Yineleyen görev tanımı "şu andan sonraki koşum anı" işlevine indirgenir; "şimdi" değerini dışarıdan almak, zamanlamayı sınanabilir kılar.
- Ayın olmayan gününe bağlanan tanımlar taşarak sonraki aya kayar; atlama ya da ayın son gününe kelepçeleme politikası açıkça yazılmalıdır.
- Sabit hız kaymayı önler ama çakışmaya açıktır; sabit gecikme çakışmayı önler ama koşum anlarını kaydırır.
- Çakışma uzun koşumdan, çok örnekli dağıtımdan ve yeniden dağıtımdan doğar; üçünün de çözümü koşmadan önce alınan kira kilididir.
- Kilidin doğruluğu almanın atomikliğine, dayanıklılığı ise kira süresinin dolmasıyla kendiliğinden düşmesine bağlıdır; ölçüm eş zamanlı koşum zirvesini 2'den 1'e indirmiştir.

#### Sonraki Adım

Bu derste görevler kısaydı: üç yüz milisaniyede biten bir rapor üretimi. Kira süresi de
buna göre seçilebildi. Ama arka plan işlerinin bir bölümü dakikalarca sürer — otuz bin
ödünç kaydını tarayan aylık rapor, yüklenen kapak görselinin farklı boyutlarda yeniden
üretilmesi. Böyle bir iş başladıktan sonra iki soru doğar: işi başlatan taraf ne kadar
ilerlendiğini nasıl öğrenir ve işten vazgeçildiğinde iş nerede durur. Sonraki ders uzun
süren işlerde ilerleme bildirimini ve iptali kurar.

### Uzun Süren İşler

Source: https://academia.sh/tr/kurslar/eszamansiz-isleme/uzun-suren-isler

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.

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.

```js
// 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,
on iki bininci kayıttan — yüzde kırklık 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.

### Geri Basınç

Source: https://academia.sh/tr/kurslar/eszamansiz-isleme/geri-basinc

Üretim hızının tüketim hızını aştığı durumun sonuçları: sınırsız kuyrukta bekleme süresinin sınırsız büyümesi, sınırlı kuyruğun üç politikasının benzetimle sayısal karşılaştırılması ve geri basıncın akış üzerinden ölçülerek üreticiye yayılması.

Bir önceki ders tek bir uzun işi ele aldı: parçalara bölmek, ilerlemeyi kaydetmek ve
iptal edilebilir kılmak yeterliydi. Gerçek bir arka planda ise işler tek tek gelmez,
**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 iki büyüklüktür.
Eşit olmalarını sağlayan hiçbir mekanizma yoktur; geliş hızını istek trafiği, bitirme
hızını işçi sayısı ve işin süresi belirler. Geliş hızı bitirme hızını aştığında kuyruk
büyür. Bu ders o büyümenin ne ürettiğini ölçer ve sistemin bunu üreticiye nasıl
bildirdiğini kurar.

#### Hız Uyuşmazlığı

Bir kuyruğa saniyede $\lambda$ iş girip saniyede $\mu$ iş çıkıyorsa, kuyruk uzunluğu
$\lambda - \mu$ hızıyla değişir. $\lambda < \mu$ olduğu sürece kuyruk boş kalır ve
bekleme süresi işin kendi süresine yaklaşır. $\lambda > \mu$ olduğu anda kuyruk her
saniye $\lambda - \mu$ kadar uzar ve bu büyüme kendiliğinden durmaz.

Büyümenin doğrudan sonucu bekleme süresidir. Kuyruk kuramının temel bağıntısı, kararlı
bir kuyrukta ortalama uzunluk $L$, varış hızı $\lambda$ ve ortalama bekleme süresi $W$
arasında şunu söyler:

$$L = \lambda W$$

Bağıntı tersten okunduğunda asıl uyarıyı verir: $W = L / \lambda$. Kuyruk uzunluğu
sınırsızsa **bekleme süresi de sınırsızdır**. Bellek dolmadan çok önce, kuyruktaki iş
hâlâ işlense bile anlamını yitirmiş olur — dört saat geciken bir gecikme bildirimi,
gönderilmemiş bildirimden farklı değildir.

Bu yüzden kuyruk uzunluğu bir kaynak sorunu değil, bir **gecikme bütçesi** sorunudur.

#### Sınırlı Kuyruk ve Üç Politika

Çözüm kuyruğu sınırlamaktır. Sınırlama tek başına bir soru doğurur: kuyruk doluyken gelen
iş ne olacak. Üç yanıt vardır ve üçü de bir şeyden vazgeçer.

**Engelleme**: üretici kuyrukta yer açılana kadar bekletilir. Hiçbir iş kaybolmaz, ama
yavaşlama üreticiye geçer — bu, dar anlamıyla **geri basınçtır** (backpressure).
**Yeniyi atma** (tail drop): kuyruk doluyken gelen iş reddedilir. **Eskiyi atma** (head
drop): kuyruğun başındaki en eski iş atılır, yeni iş sona eklenir.

Aşağıdaki benzetim dördünü de aynı yük altında koşturur: saniyede yüz iş üretiliyor,
altmış iş tüketiliyor, pencere on saniye, sınırlı kuyruğun kapasitesi yüz.

```js
// benzetim.mjs — uretim hizi tuketim hizini astiginda kuyrugun dort politikadaki davranisi
const PENCERE = 10_000;            // benzetim penceresi (ms)
const URETIM = 100;                // saniyede uretilen is
const TUKETIM = 60;                // saniyede tuketilen is
const KAPASITE = 100;              // sinirli kuyrugun kapasitesi

function kosum(politika) {
  const kuyruk = [];               // her oge: uretildigi an (ms)
  let uretilen = 0, teslim = 0, atilan = 0, bekleme = 0;
  let zirve = 0, enUzunBekleme = 0;
  let ub = 0, tb = 0;              // birikimler tam sayi: her ms'de hiz kadar artar

  for (let t = 1; t <= PENCERE; t++) {
    ub += URETIM;
    while (ub >= 1000) {
      ub -= 1000;
      uretilen++;
      if (politika === "sinirsiz" || kuyruk.length < KAPASITE) kuyruk.push(t);
      else if (politika === "yeniyi-at") atilan++;
      else if (politika === "eskiyi-at") { kuyruk.shift(); atilan++; kuyruk.push(t); }
      else { uretilen--; ub += 1000; bekleme++; break; }      // engelle: uretici bekler
    }
    tb += TUKETIM;
    while (tb >= 1000 && kuyruk.length) {
      tb -= 1000;
      enUzunBekleme = Math.max(enUzunBekleme, t - kuyruk.shift());
      teslim++;
    }
    zirve = Math.max(zirve, kuyruk.length);
  }
  return { politika, uretilen, teslim, atilan, bekleme, zirve,
           kalan: kuyruk.length, enUzunBekleme };
}

const s3 = (n) => String(n).padStart(6);
console.log("politika    uretilen teslim atilan  kalan  zirve  en-uzun-bekleme uretici-beklemesi");
for (const p of ["sinirsiz", "yeniyi-at", "eskiyi-at", "engelle"]) {
  const s = kosum(p);
  console.log(`${s.politika.padEnd(11)}${s3(s.uretilen)}${s3(s.teslim)}${s3(s.atilan)}` +
    `${s3(s.kalan)}${s3(s.zirve)}${String(s.enUzunBekleme).padStart(13)} ms` +
    `${String(s.bekleme).padStart(14)} ms`);
}
```

```
politika    uretilen teslim atilan  kalan  zirve  en-uzun-bekleme uretici-beklemesi
sinirsiz     1000   600     0   400   400         4000 ms             0 ms
yeniyi-at    1000   600   301    99   100         1664 ms             0 ms
eskiyi-at    1000   600   301    99   100          997 ms             0 ms
engelle       699   600     0    99   100         1666 ms          7490 ms
```

Benzetim gerçek zaman kullanmaz; sanal bir saati milisaniye milisaniye ilerletir ve
hızları tam sayı birikimleriyle uygular. Bu yüzden sayılar makineden bağımsız olarak
aynıdır.

#### Sayıların Okunması

Dört satır dört ayrı ödünleşimi gösteriyor.

**Sınırsız** kuyrukta hiçbir iş kaybolmadı, ama kuyruk on saniyede dört yüze çıktı ve en
uzun bekleme dört saniyeye ulaştı. Önemli olan sayının kendisi değil yönü: pencere yirmi
saniye olsaydı kuyruk sekiz yüz, bekleme sekiz saniye olurdu. Bu politika, işi kaybetmeme
karşılığında **gecikmeyi sınırsız bırakır**.

**Yeniyi atma** ve **eskiyi atma** aynı sayıda işi teslim etti (600) ve aynı sayıda işi
attı (301). Aralarındaki tek fark bekleme süresinde: 1664 ms'ye karşı 997 ms. Fark
teslim edilen işlerin **yaşından** gelir. Yeniyi atarken kuyrukta hep en eski yüz iş
durur; eskiyi atarken kuyruk sürekli tazelenir ve tüketiciye daha genç işler ulaşır.
Kayıp oranı aynı olduğunda bu, eskiyi atmayı bildirim gibi tazeliği önemli işler için
üstün kılar; sıraya girmiş bir ödünç isteği gibi **sırası korunması gereken** işlerde ise
en eskiyi atmak kabul edilemez.

**Engelleme** hiçbir işi atmadı ama yalnız 699 iş üretebildi: üretici on saniyenin 7490
milisaniyesinde bekledi. Kaybolan iş yok, çünkü hiç üretilmedi. Geri basıncın tanımı
budur — darboğaz, sonucu kendisine gelen tarafa değil, kendisini besleyen tarafa yansıtır.

Dört satırın ortak dersi şudur: hız uyuşmazlığı yok edilemez, yalnız **nereye
yazılacağı** seçilebilir. Belleğe (sınırsız kuyruk), kayba (atma) ya da üreticinin hızına
(engelleme).

#### Geri Basıncın Yayılması

Engelleme kulağa soyut gelir; oysa akış soyutlamasının içine yerleşmiş durumdadır.
`node:stream` üzerinde yazılabilir bir akışın `write` çağrısı bir mantıksal değer
döndürür: `false`, tamponun eşiği aştığını ve üretimin durdurulması gerektiğini bildirir.
Yer açıldığında akış `drain` olayını yayar.

Aşağıdaki ölçüm aynı altmış dört parçayı iki kez yazar. İlkinde dönüş değeri dinlenir,
ikincisinde yok sayılır.

```js
// akis.mjs — node:stream geri basinci: write() donusunu dinlemek ile yok saymak
import { Writable } from "node:stream";

const PARCA = Buffer.alloc(1024, 0x61);          // 1 KiB'lik is govdesi
const ADET = 64;

const yavasAlici = () => new Writable({
  highWaterMark: 16 * 1024,                      // tampon sinirimiz: 16 KiB
  write(_p, _k, bitti) { setTimeout(bitti, 5); },// her parcanin yazimi 5 ms surer
});

async function dinleyerek() {
  const alici = yavasAlici();
  let doluDonus = 0, drain = 0, zirve = 0;
  for (let i = 0; i < ADET; i++) {
    const yerVar = alici.write(PARCA);
    zirve = Math.max(zirve, alici.writableLength);
    if (!yerVar) {                               // tampon doldu: uretimi durdur
      doluDonus++;
      await new Promise((c) => alici.once("drain", () => { drain++; c(); }));
    }
  }
  await new Promise((c) => alici.end(c));
  return { doluDonus, drain, zirve };
}

async function yokSayarak() {
  const alici = yavasAlici();
  let doluDonus = 0, zirve = 0;
  for (let i = 0; i < ADET; i++) {
    if (!alici.write(PARCA)) doluDonus++;         // donus okunuyor ama uyulmuyor
    zirve = Math.max(zirve, alici.writableLength);
  }
  await new Promise((c) => alici.end(c));
  return { doluDonus, drain: 0, zirve };
}

for (const [ad, calis] of [["dinleyerek", dinleyerek], ["yok sayarak", yokSayarak]]) {
  const s = await calis();
  console.log(`${ad.padEnd(12)} dolu-donus=${String(s.doluDonus).padStart(2)}` +
    ` drain=${String(s.drain).padStart(2)} tampon-zirvesi=${s.zirve} B`);
}
```

```
dinleyerek   dolu-donus= 4 drain= 4 tampon-zirvesi=16384 B
yok sayarak  dolu-donus=49 drain= 0 tampon-zirvesi=65536 B
```

Dönüş değeri dinlendiğinde tampon zirvesi tam olarak eşiğin kendisidir: 16384 bayt. Yok
sayıldığında altmış dört parçanın tamamı tamponda birikir: 65536 bayt. Altmış dört
parçada oran dört kat; üretici sınırsız yazmaya devam ederse oran da sınırsızdır, çünkü
`false` dönüşünü yok saymak tamponu bir kuyruğa, üstelik **sınırsız** bir kuyruğa
çevirir.

Buradaki ders akışa özgü değildir. Kuyruğa iş yazan her katman aynı soruyla karşı
karşıyadır: yazma reddedildiğinde durur mu, yoksa arkasında kendi kuyruğunu mu biriktirir.
Durmayan her katman, önündeki sınırı işe yaramaz hale getirir.

#### Geri Basınç Nereye Kadar Yayılır

Zincirin son halkası kullanıcıdır ve geri basınç oraya kadar taşınamaz. İstek işleyen bir
sunucu, arka plan kuyruğu dolduğu için HTTP isteğini süresiz bekletemez; bekletirse
bağlantıları tüketir ve sorun kuyruktan ağa taşınır.

Bu yüzden geri basınç sistemin **iç** halkalarında uygulanır: işçi ile veritabanı
arasında, kuyruk yazıcısı ile kuyruk arasında, akış üreticisi ile alıcısı arasında. Dış
sınırda ise başka bir mekanizma gerekir — gelen isteği bekletmek yerine **açıkça
reddetmek**.

#### Özet

- Üretim hızı tüketim hızını aştığında kuyruk her saniye aradaki fark kadar uzar; büyüme kendiliğinden durmaz.
- $L = \lambda W$ bağıntısı gereği sınırsız kuyruk uzunluğu sınırsız bekleme süresi demektir; kuyruk sınırı bir bellek kararı değil gecikme bütçesi kararıdır.
- Sınırlı kuyrukta üç politika vardır: engelleme işi kaybetmez ama üreticiyi yavaşlatır, yeniyi atma ile eskiyi atma aynı kaybı üretir ama teslim edilen işlerin yaşı farklıdır.
- Geri basınç, darboğazın sonucunu kendisini besleyen tarafa yansıtmasıdır; `node:stream` bunu yazma dönüşü ve boşalma olayıyla uygular ve tampon zirvesini eşikte tutar.
- Yazma dönüşünü yok sayan katman, önündeki her sınırı geçersiz kılar; geri basınç zincirin tamamında uygulanmadıkça hiçbir yerinde uygulanmış sayılmaz.

#### Sonraki Adım

Geri basınç iç halkalarda çalışır, ama zincirin dış ucunda duran istemci bekletilemez.
Kuyruk dolduğunda, işçiler yetişemediğinde ve gecikme bütçesi tükendiğinde sunucunun
elinde tek seçenek kalır: gelen işi kabul etmemek. Bunu rastgele yapmak sistemi
kararsızlaştırır; hangi isteğin reddedileceği, reddin istemciye nasıl bildirileceği ve
reddedilen ile kabul edilenin gecikme farkının ne olduğu ayrı kararlardır. Sonraki ders
aşırı yükte denetimli reddetmeyi kurar ve reddin gecikmeye etkisini ölçer.

### Yük Boşaltma

Source: https://academia.sh/tr/kurslar/eszamansiz-isleme/yuk-bosaltma

Aşırı yükte denetimli reddetme: eşzamanlılık eşiğine dayalı kabul denetimi, kabul edilenle reddedilenin gecikme farkının ölçümü, reddin HTTP'deki biçimi ve yeniden deneme fırtınasının önlenmesi.

Bir önceki ders geri basıncı kurdu ve bir sınırla bitti: geri basınç sistemin iç
halkalarında çalışır, dış ucunda duran istemciye taşınamaz. İstek işleyen bir sunucu,
arka plan kuyruğu dolduğu için isteği süresiz bekletemez.

O zaman aşırı yükte ne yapılır. Yanıt, kabul edilmesi güç olduğu için sık atlanır:
**isteği reddetmek**. Reddetmek bir arıza değil, bir tasarım kararıdır. Reddetmeyen bir
sunucu istekleri kabul etmeye devam eder, her birinin gecikmesi büyür ve bir noktadan
sonra hiçbir istek zamanında karşılanmaz. Bu duruma **çöküş** denir ve reddetmekten çok
daha kötüdür: reddedilen istek en azından hızlıca reddedilmiştir. Bu ders reddin nasıl
denetimli yapılacağını kurar ve iki durumun gecikmesini ölçerek karşılaştırır.

#### Kabul Denetimi

**Yük boşaltma** (load shedding), sistemin kapasitesini aşan işi kabul etmeyip geri
çevirmesidir. Kararı veren yapıya **kabul denetimi** (admission control) denir ve iş
başlamadan **önce** çalışır.

Kabul denetimi, hız sınırlamadan iki noktada ayrılır. Hız sınırlama belirli bir
istemcinin belirli bir sürede yapabileceği istek sayısını sınırlar; ölçütü istemcidir ve
amacı adil paylaşımdır. Kabul denetiminin ölçütü ise **sistemin o anki durumudur**;
amacı, kabul edilmiş işlerin zamanında bitmesini korumaktır. Aynı istemci, sistem boşken
kabul edilir, doluyken reddedilir.

##### Eşik Neye Konur

Eşiğin saniyedeki istek sayısına konması yaygın ama yanıltıcıdır. Saniyedeki istek sayısı
işin süresini içermez: aynı hız, iş yirmi milisaniye sürüyorsa rahat, üç yüz milisaniye
sürüyorsa taşırıcıdır.

Doğru ölçüt **eşzamanlılıktır**: aynı anda işlenen iş sayısı. Önceki derste kullanılan
$L = \lambda W$ bağıntısı bunu doğrudan verir; sistemdeki iş sayısı $L$ sabitlenirse,
ortalama süre $W$ de sabitlenmiş olur. Eşzamanlılığı sabitlemek gecikmeyi sabitlemektir.

Aşağıdaki sunucu iki eşik taşır: aynı anda işlenebilecek iş sayısı ve kuyrukta
bekleyebilecek iş sayısı. İkisinin toplamı, sistemin kabul ettiği en büyük yüktür.

```js
// kabul.mjs — is sunucusu; iki kipte calisir
//   node kabul.mjs --sinirsiz    gelen her istek kuyruga alinir
//   node kabul.mjs --denetimli   esik asilinca istek 503 ile geri cevrilir
import { createServer } from "node:http";

const DENETIMLI = process.argv.includes("--denetimli");
const EN_COK_ESZAMANLI = 2;        // ayni anda islenen is sayisi
const EN_COK_KUYRUK = 2;           // kuyrukta bekleyebilecek is sayisi
const IS_SURESI = 300;             // bir isin suresi (ms)

let calisan = 0;
const kuyruk = [];

const isYap = () => new Promise((c) => setTimeout(c, IS_SURESI));

async function baslat(gorev) {
  calisan++;
  await isYap();
  calisan--;
  gorev.yanit.writeHead(200, { "content-type": "text/plain" });
  gorev.yanit.end(`${gorev.durum}\n`);
  const sonraki = kuyruk.shift();
  if (sonraki) baslat(sonraki);
}

createServer((istek, yanit) => {
  yanit.sendDate = false;
  if (calisan < EN_COK_ESZAMANLI) return baslat({ yanit, durum: "kabul" });
  if (!DENETIMLI || kuyruk.length < EN_COK_KUYRUK)
    return kuyruk.push({ yanit, durum: "kuyruk" });
  // Esik asildi: istek beklemeye alinmaz, ne zaman tekrar denenecegi bildirilir.
  yanit.writeHead(503, { "content-type": "text/plain", "retry-after": "1" });
  yanit.end("bosaltildi\n");
}).listen(8352, "127.0.0.1", () => console.log("dinleniyor: 127.0.0.1:8352"));
```

#### Ölçüm

Aşağıdaki betik sunucuyu iki kipte de ayağa kaldırır, her seferinde on isteği aynı anda
gönderir ve sonuçları durum koduna ve yüz milisaniyelik gecikme dilimlerine göre
gruplar. Bağlantı noktası 8352 keyfîdir ve boş olmalıdır.

```bash
#!/usr/bin/env bash
# kabul.mjs'yi iki kipte baslatir, her kipte on istegi ayni anda gonderir, sonucu ozetler.
A=http://127.0.0.1:8352

olc() {
  node kabul.mjs "$1" > /dev/null & sunucu=$!
  sleep 1
  pid=()
  for i in $(seq 1 10); do
    curl -sS -o /dev/null -w '%{http_code} %{time_total}\n' "$A/is" & pid+=($!)
  done > ham.txt 2>&1
  wait "${pid[@]}"
  echo "--- $2 ---"
  awk '{ printf "durum=%s  gecikme=%2d dilim\n", $1, int($2 * 10) }' ham.txt | LC_ALL=C sort | uniq -c | sed 's/^ *//'
  kill "$sunucu"
  sleep 0.5
}

olc --sinirsiz  "kabul denetimi yok: her istek kuyruga alinir"
olc --denetimli "kabul denetimi var: esik asilinca 503"
```

```
--- kabul denetimi yok: her istek kuyruga alinir ---
2 durum=200  gecikme= 3 dilim
2 durum=200  gecikme= 6 dilim
2 durum=200  gecikme= 9 dilim
2 durum=200  gecikme=12 dilim
2 durum=200  gecikme=15 dilim
--- kabul denetimi var: esik asilinca 503 ---
2 durum=200  gecikme= 3 dilim
2 durum=200  gecikme= 6 dilim
6 durum=503  gecikme= 0 dilim
```

Her satırın başındaki sayı, o satırdan kaç istek geldiğini gösterir. Satırlar `LC_ALL=C sort`
ile dizildiği için sıra her ortamda aynıdır. Dilim sayısı yüz milisaniyelik birimdir; iş
süresi üç yüz milisaniye olduğu için ilk çift üç dilimde, ikinci çift altı dilimde yanıt
alır. Yavaş bir makinede dilim sayıları bir üste kayabilir.

İki kip aynı işi yapıyor, ama gecikme dağılımları taban tabana zıt.

Denetimsiz kipte **hiçbir istek reddedilmedi** ve kimse memnun olmadı: son iki istek bir
buçuk saniye bekledi. Yük on yerine yüz istek olsaydı son isteğin gecikmesi on beş saniye
olurdu. Kabul edilen her istek, kendinden önce kabul edilmiş herkesin gecikmesini
taşıyor.

Denetimli kipte dört istek karşılandı, altısı **sıfırıncı dilimde**, yani bekletilmeden
reddedildi. Kabul edilenlerin gecikmesi üç ve altı dilimde kaldı; sistem, kabul ettiği
isteklere verdiği sözü tuttu.

Fark tek cümleyle şudur: kabul denetimi, **belirsiz bir gecikmeyi kesin bir redde**
çevirir. İstemci için bu iyileşmedir, çünkü redde karşı bir davranışı vardır — yeniden
denemek, işi ertelemek, kullanıcıya bilgi vermek. Belirsiz bir beklemeye karşı hiçbir
davranışı yoktur.

#### Reddin Biçimi

Reddin nasıl bildirildiği, reddedilme kararı kadar önemlidir. Aşağıdaki betik sunucuyu
doygunluğa getirip bir isteğin ham yanıtını gösterir.

```bash
#!/usr/bin/env bash
# Doygunluga ulasmis sunucudan gelen reddin yanitini basliklariyla gosterir.
node kabul.mjs --denetimli > /dev/null & sunucu=$!
sleep 1
A=http://127.0.0.1:8352

pid=()
for i in 1 2 3 4; do curl -sS -o /dev/null "$A/is" & pid+=($!); done   # 2 calisan + 2 kuyruk
sleep 0.1
echo "--- doygunlukta gelen istegin yaniti ---"
curl -sS -D - "$A/is"
wait "${pid[@]}"
kill "$sunucu"
```

```
--- doygunlukta gelen istegin yaniti ---
HTTP/1.1 503 Service Unavailable
content-type: text/plain
retry-after: 1
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

bosaltildi
```

Üç ayrıntı bilinçli seçimdir. Durum kodu **503**'tür, 500 değil: 500 sunucunun
beklenmedik bir kusura düştüğünü söyler ve yeniden denemenin işe yarayıp yaramayacağı
hakkında bir şey bildirmez; 503 ise durumun **geçici** olduğunu, yani beklendiğinde
düzeleceğini söyler. Fark istemcinin davranışında görünür: 500 alan istemci neyi
beklediğini bilmez, 503 alan bilir. `Retry-After` başlığı denemenin ne zaman yapılacağını
saniye cinsinden bildirir; istemcinin tahmin yürütmesi gerekmez. Ve gövde kısadır: doygun
bir sunucunun
uzun bir hata gövdesi üretmesi, kaçınmaya çalıştığı işin kendisidir.

Bağlantı başlıkları çalışma zamanının varsayılan ayarına bağlıdır.

#### Neyi Boşaltmalı

Buraya kadar reddedilecek istek, eşiği aşan istekti. Bu en yalın politikadır ve bir
kusuru vardır: tüm işleri eşit görür. Oysa kütüphane servisinde bir ödünç iade işlemi ile
aylık rapor talebi eşit değildir.

İki ölçüt bu ayrımı verir. **Önem sırası** ile boşaltmada her işe bir sınıf verilir ve
doygunlukta önce düşük sınıf reddedilir; kritik sınıfa ayrılmış bir eşzamanlılık payı
her zaman boş tutulur. **Son tarihe göre** boşaltmada ise her iş bir gecikme bütçesiyle
gelir; kuyrukta beklerken bütçesi dolan iş, sırası geldiğinde işlenmeden atılır. İkincisi
özellikle değerlidir, çünkü kuyruğun başındaki işin çoktan anlamsızlaştığı durumu
yakalar — istemci çekilmiş olabilir.

Her iki politikada da boşaltma kararı sabit bir eşikten değil, **ölçülen bir
göstergeden** beslenmelidir: kuyrukta geçirilen süre, işçilerin doluluğu, dış bağımlılığın
gecikmesi. Sabit eşik, yükün biçimi değiştiğinde ya erken ya geç boşaltır.

#### Yeniden Deneme Fırtınası

Boşaltmanın kendisi yeni bir tehlike üretir. Aynı anda reddedilen altı istemci, `Retry-After`
süresi dolduğunda aynı anda geri döner. Bu, sunucuyu doygun tutan bir döngüye yol açar
ve ona **yeniden deneme fırtınası** (retry storm) denir.

Panzehiri bu kursta zaten kuruldu: mesajlaşma konusunda tanıtılan üstel geri çekilme ve
sarsıntı. `Retry-After` değeri bir taban süredir; istemci bu tabana rastgele bir sarsıntı
ekler ve her başarısız denemede süreyi büyütür. Ayrıca istemci tarafında bir üst deneme
sınırı bulunmalıdır, yoksa reddedilen iş sonsuza dek geri gelir.

Sunucu tarafında da bir önlem vardır: reddedilen isteğin kütüğe yazılması, kabul edilenden
**daha ucuz** olmalıdır. Doygunlukta her reddi ayrıntılı kütüklemek, boşaltmanın kazandırdığı
kapasiteyi kütükleme işine harcar.

#### Özet

- Yük boşaltma, kapasiteyi aşan işi kabul etmeyip geri çevirmektir; kararı veren kabul denetimi iş başlamadan önce çalışır.
- Eşik saniyedeki istek sayısına değil eşzamanlılığa konur; $L = \lambda W$ gereği sistemdeki iş sayısını sabitlemek gecikmeyi sabitler.
- Ölçüm, denetimsiz sunucuda gecikmenin 3 dilimden 15 dilime doğru yayıldığını, denetimli sunucuda ise kabul edilenlerin 3–6 dilimde kaldığını ve fazlanın sıfırıncı dilimde reddedildiğini gösterdi.
- Kabul denetimi belirsiz bir gecikmeyi kesin bir redde çevirir; 503 ile `Retry-After` istemciye ne zaman yeniden deneyeceğini söyler, 500 ise yalnız beklenmedik bir kusuru bildirir ve zamanlama hakkında hiçbir şey söylemez.
- Boşaltma yeniden deneme fırtınası doğurur; panzehiri istemci tarafında üstel geri çekilme ile sarsıntı, sunucu tarafında ise reddin ucuz tutulmasıdır.

#### Sonraki Adım

Buraya kadar kurulan her şey işin **sunucu tarafındaki** dayanıklılığıyla ilgiliydi: iş
bir işçiye düşer, zamanında tetiklenir, ilerlemesi kaydedilir, kuyruk taşmaz, aşırı yükte
denetimli olarak reddedilir. Bu zincirin bir ucu hâlâ açık. İşi başlatan istemci, işin
kabul edildiğini öğrendi ama sonucun **ne zaman hazır olduğunu** bilmiyor. Uzun süren
raporun ilerleme yüzdesi, rezervasyon sırasındaki konumun değişmesi, şube stok sayacının
güncellenmesi — bunların hepsi sunucu tarafında hazır, istemci tarafında görünmez.
Sonraki konu bu bilginin istemciye nasıl ulaştığını ele alır ve en yalın yoldan, yani
istemcinin tekrar tekrar sormasından başlar.

## Gerçek Zamanlı Veri

Kısa ve uzun yoklamanın istek maliyeti, tek yönlü akış için sunucu gönderimli olaylar ve yeniden bağlanma, kendi el sıkışmasıyla kurulan WebSocket sunucusu ve bağlantı yaşam döngüsü, çok örnekli kurulumda kanal dağıtımı ve parça parça üretilen akış yanıtları.

### Yoklama Yaklaşımları

Source: https://academia.sh/tr/kurslar/eszamansiz-isleme/yoklama-yaklasimlari

İstemcinin sunucudaki değişiklikten haberdar olmasının en yalın yolu: kısa yoklamanın istek ve boş yanıt maliyeti, uzun yoklamanın bağlantı tutma karşılığı, ikisinin aynı olay üzerinde ölçülmesi ve sürüm numarasıyla kapatılan olay boşluğu.

Bir önceki konu işin sunucu tarafındaki dayanıklılığını tamamladı: iş bir işçiye düşer,
zamanında tetiklenir, ilerlemesi kaydedilir, kuyruk taşmaz, aşırı yükte denetimli olarak
reddedilir. Zincirin bir ucu hâlâ açık kaldı. İşi başlatan istemci, işin kabul edildiğini
biliyor ama sonucun ne zaman hazır olduğunu bilmiyor.

Uzun süren raporun ilerleme yüzdesi, rezervasyon sırasındaki konumun değişmesi, şube stok
sayacının güncellenmesi — bunların hepsi sunucu tarafında hazır ama istemci tarafında
görünmez. HTTP'nin varsayılan yönü bunu zorlaştırır: bağlantıyı istemci açar, isteği
istemci gönderir. Sunucunun kendiliğinden konuşma imkânı yoktur.

Bu konu, bilginin istemciye ulaşmasının dört yolunu kurar. En yalınından başlıyoruz:
istemcinin tekrar tekrar sorması.

#### Yoklama

**Yoklama** (polling), istemcinin durumu öğrenmek için sunucuya düzenli aralıklarla
istek göndermesidir. Hiçbir ek protokol gerektirmez; sunucu tarafında ek durum tutulmaz;
her ara katman, güvenlik duvarı ve önbellek onu olağan bir HTTP isteği olarak görür. Bu
yüzden yoklama, gerçek zamanlı veri sorununun varsayılan yanıtıdır ve pek çok durumda
doğru yanıttır.

İki biçimi vardır ve aralarındaki fark tek bir karardır: **sunucu, verecek bir şeyi
yoksa ne yapar**.

**Kısa yoklama** (short polling) biçiminde sunucu hemen cevap verir. Değişiklik yoksa
"değişiklik yok" der ve bağlantı kapanır; istemci bir süre bekleyip yeniden sorar.

**Uzun yoklama** (long polling) biçiminde sunucu cevabı geciktirir. Bir değişiklik olana
kadar bağlantıyı açık tutar; değişiklik gelirse cevabı o anda yazar. Belirli bir süre
boyunca hiçbir şey olmazsa boş cevap verir ve istemci yeniden bağlanır.

#### İki Yaklaşımı Tek Sunucuda Kurmak

Aşağıdaki sunucu, önceki konuda `is_durumu` tablosuna yazılan ilerlemeyi sunuyor. Durumun
bir **sürüm numarası** var; istemci en son gördüğü sürümü isteğe iliştiriyor.

```js
// yoklama.mjs — rapor isinin durumunu iki bicimde sunar: kisa yoklama ve uzun yoklama
import { createServer } from "node:http";

const TUTMA_SINIRI = 1000;         // uzun yoklamanin baglantiyi tutma suresi (ms)
let surum = 0, ilerleme = 0;       // rapor-2026-01 isinin durumu
const sayac = { durum_istegi: 0, bekle_istegi: 0, bos_yanit: 0 };
const bekleyen = new Set();        // acik tutulan uzun yoklama yanitlari

const govde = () => JSON.stringify({ surum, ilerleme }) + "\n";
const yaz = (yanit, metin, kod = 200) => {
  yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" });
  yanit.end(metin);
};
const bos = (yanit) => { sayac.bos_yanit++; yanit.writeHead(204).end(); };

createServer((istek, yanit) => {
  yanit.sendDate = false;
  const adres = new URL(istek.url, "http://yerel");
  const gorulen = Number(adres.searchParams.get("surum") ?? 0);

  if (adres.pathname === "/durum") {              // kisa yoklama: hemen cevap ver
    sayac.durum_istegi++;
    return surum > gorulen ? yaz(yanit, govde()) : bos(yanit);
  }

  if (adres.pathname === "/bekle") {              // uzun yoklama: degisene kadar tut
    sayac.bekle_istegi++;
    if (surum > gorulen) return yaz(yanit, govde());
    const kayit = { yanit };
    kayit.zamanlayici = setTimeout(() => { bekleyen.delete(kayit); bos(yanit); },
                                   TUTMA_SINIRI);
    return void bekleyen.add(kayit);
  }

  if (adres.pathname === "/ilerlet") {            // isci ilerlemeyi bildirdi
    surum++; ilerleme += 10;
    for (const b of bekleyen) { clearTimeout(b.zamanlayici); yaz(b.yanit, govde()); }
    bekleyen.clear();
    return yaz(yanit, govde());
  }

  if (adres.pathname === "/olcum")
    return yaz(yanit, JSON.stringify(sayac) + "\n");

  yaz(yanit, JSON.stringify({ hata: "yol yok" }) + "\n", 404);
}).listen(8361, "127.0.0.1", () => console.log("dinleniyor: 127.0.0.1:8361"));
```

İki uç arasındaki tek yapısal fark `bekleyen` kümesidir. Kısa yoklamada sunucu isteği
işleyip unutur; uzun yoklamada yanıt nesnesini saklar ve `/ilerlet` geldiğinde o saklanan
nesnelere yazar. Yani uzun yoklama, sunucuya **durum** ekler.

#### Ölçüm

Aşağıdaki betik aynı tek olayı — raporun bir adım ilerlemesini — iki yaklaşımla
karşılar ve sayar. Bağlantı noktası 8361 keyfîdir ve boş olmalıdır.

```bash
#!/usr/bin/env bash
# yoklama.mjs'yi baslatir, ayni tek olayi iki yaklasimla olcer, durdurur.
A=http://127.0.0.1:8361
B='%{http_code} %{size_download} B\n'

node yoklama.mjs > /dev/null & sunucu=$!
sleep 1
echo "--- kisa yoklama: alti istek ---"
for i in 1 2 3 4 5; do curl -sS -w "$B" "$A/durum?surum=0"; done
curl -sS -o /dev/null -X POST "$A/ilerlet"          # is bu anda ilerledi
curl -sS -w "$B" "$A/durum?surum=0"
curl -sS "$A/olcum"
kill "$sunucu"; sleep 0.3

node yoklama.mjs > /dev/null & sunucu=$!
sleep 1
echo "--- uzun yoklama: iki istek ---"
curl -sS -w "$B" "$A/bekle?surum=0" & bekleyen=$!
sleep 0.3
curl -sS -o /dev/null -X POST "$A/ilerlet"          # is bu anda ilerledi
wait $bekleyen
curl -sS -w "$B" "$A/bekle?surum=1"                 # degisim yok: sure dolar
curl -sS "$A/olcum"
kill "$sunucu"
```

```
--- kisa yoklama: alti istek ---
204 0 B
204 0 B
204 0 B
204 0 B
204 0 B
{"surum":1,"ilerleme":10}
200 26 B
{"durum_istegi":6,"bekle_istegi":0,"bos_yanit":5}
--- uzun yoklama: iki istek ---
{"surum":1,"ilerleme":10}
200 26 B
204 0 B
{"durum_istegi":0,"bekle_istegi":2,"bos_yanit":1}
```

Aynı olay, kısa yoklamada altı istekle, uzun yoklamada iki istekle karşılandı. Kısa
yoklamada beş yanıt boştu; boş yanıtların gövdesi sıfır bayt olduğu için taşınan veri
küçük, ama **istek sayısı** aynı kaldı. Her istek bir bağlantı kurulumu, bir yönlendirme
kararı, bir kimlik doğrulama ve bir kütük satırı demektir; gövdenin boş olması bunların
hiçbirini ucuzlatmaz.

Uzun yoklamada ikinci istek bir saniye açık kaldı ve değişiklik olmadığı için 204 ile
kapandı. Bu, uzun yoklamanın kaçınılmaz maliyetidir: bağlantı sonsuza kadar tutulamaz,
çünkü ara katmanların ve yük dağıtıcıların kendi zaman aşımları vardır.

#### Maliyetin Hesabı

İki yaklaşımın maliyeti, gözlem penceresi ve olay sıklığıyla hesaplanabilir. Aşağıdaki
tablo altmış saniyelik bir pencerede tek bir olay olduğu varsayımıyla çıkarılmıştır.

| Yaklaşım | İstek | Boş yanıt | En kötü gecikme |
|---|---|---|---|
| Kısa yoklama, 1 s arayla | 60 | 59 | 1 s |
| Kısa yoklama, 5 s arayla | 12 | 11 | 5 s |
| Uzun yoklama, 30 s tutma | 3 | 1 | ağ gecikmesi |

Kısa yoklamada istek sayısı `pencere / aralık` ile, en kötü gecikme ise doğrudan aralıkla
belirlenir. İkisi ters orantılıdır ve bu, kısa yoklamanın tek gerçek ödünleşimidir:
gecikmeyi yarıya indirmek istek sayısını iki katına çıkarır.

Uzun yoklamada istek sayısı `pencere / tutma süresi + olay sayısı` ile belirlenir ve
gecikme aralığa bağlı değildir. Bu yüzden uzun yoklama, seyrek ama gecikmeye duyarlı
olaylar için kısa yoklamadan belirgin ölçüde ucuzdur.

Hesabın yönü olay sıklığı arttıkça tersine döner. Saniyede birden çok olay üreten bir
kaynakta uzun yoklama her olayda bir bağlantı kurar; kısa yoklama ise bir istekte birden
çok değişikliği toplu olarak taşır. Sık değişen ve her ara değeri önemsenmeyen veriler —
şube stok sayacı gibi — kısa yoklamayla daha ucuza sunulur.

#### Sunucu Tarafındaki Bedel

Uzun yoklama sunucuya durum ekler ve bu durumun bir sınırı vardır. Açık tutulan her
istemci bir yuva (socket), bir dosya tanıtıcısı ve bir yanıt nesnesi tutar. Bin istemci
bin açık bağlantı demektir; bunlar iş yapmıyor olsa da kaynak tüketirler.

Üç sonuç doğar. Sunucunun eşzamanlı bağlantı sınırı, artık eşzamanlı **istek** sınırından
farklı bir büyüklüktür ve ona göre ayarlanmalıdır. Önceki dersteki kabul denetimi bu
bağlantıları saymamalıdır; bekleyen bir uzun yoklama işlemci kullanmaz, dolayısıyla onu
"çalışan iş" olarak saymak sunucuyu gereksiz yere doygun gösterir. Ve tutma süresi, ara
katmanların zaman aşımından **kısa** seçilmelidir; aksi hâlde bağlantıyı sunucu değil
aradaki bir katman keser ve istemci nedenini anlayamaz.

#### Olay Boşluğu

Uzun yoklamanın gözden kaçan kusuru şurasıdır: yanıt yazıldıktan sonra istemci yeniden
bağlanana kadar geçen sürede sunucu **dinlenmez**. O aralıkta olan bir olay, hiçbir açık
bağlantıya yazılamaz.

Kodda bu boşluğu kapatan şey sürüm numarasıdır. İstemci en son gördüğü sürümü isteğe
ekler; sunucu `surum > gorulen` koşulunu bağlantıyı tutmadan önce sınar ve geride kalmış
bir istemciye hemen cevap verir. Ölçümdeki ikinci uzun yoklamanın `surum=1` ile
gönderilmesinin nedeni budur.

Bu, gerçek zamanlı veri sunan her yaklaşımın ortak gereksinimidir: istemcinin nerede
kaldığını söyleyebilmesi. Sürüm numarası, olay kimliği ya da damga — adı değişir, işlevi
değişmez. Sonraki derste aynı işi `Last-Event-ID` başlığının yaptığını göreceğiz.

#### Özet

- Yoklama, istemcinin durumu öğrenmek için düzenli aralıklarla istek göndermesidir; ek protokol gerektirmediği için varsayılan yaklaşımdır.
- Kısa yoklamada sunucu hemen cevap verir ve boş yanıtlar üretir; istek sayısı `pencere / aralık` ile artar, en kötü gecikme aralığın kendisidir.
- Uzun yoklamada sunucu değişiklik olana kadar bağlantıyı tutar; ölçümde aynı olay altı istek yerine iki istekle karşılandı ve gecikme aralığa bağlı olmaktan çıktı.
- Uzun yoklamanın bedeli sunucuya eklenen durumdur: açık tutulan her bağlantı kaynak tüketir, tutma süresi ara katman zaman aşımlarından kısa seçilmelidir.
- İki yoklama arasındaki olay boşluğu, istemcinin en son gördüğü sürümü isteğe iliştirmesiyle kapatılır.

#### Sonraki Adım

Uzun yoklama, bağlantıyı tutup tek bir yanıt yazdıktan sonra kapatıyor. Her olay için
yeni bir bağlantı kurulması, olaylar sıklaştığında saçma bir düzene dönüşür: aynı
istemciye arka arkaya yazılacak on olay için on ayrı bağlantı. Oysa bağlantı zaten
açıkken üzerine birden çok olay yazılabilir. Sonraki ders bunu standartlaştıran biçimi
kurar: sunucunun tek bir açık yanıt gövdesine olayları peş peşe yazdığı, tarayıcı
tarafında yeniden bağlanmayı kendi üstlenen tek yönlü akış.

### Sunucu Gönderimli Olaylar

Source: https://academia.sh/tr/kurslar/eszamansiz-isleme/sunucu-gonderimli-olaylar

Tek bir açık yanıt gövdesine peş peşe yazılan olaylar: olay akışının satır biçimi, akış yanıtının başlık kuralları, olay kimliğiyle yeniden bağlanma ve kalp atışı satırının ara katmanlara karşı işlevi.

Uzun yoklama, bağlantıyı tutup tek bir yanıt yazdıktan sonra kapatıyordu. Rezervasyon
sırasındaki konumun beş kez değişmesi, beş ayrı bağlantı demekti: her olay için istemci
yeniden bağlanıyor, sunucu yeniden yetkilendiriyor, ara katmanlar yeniden yönlendiriyordu.

Oysa ilk bağlantı zaten açıktı. HTTP yanıt gövdesinin bir kerede yazılması zorunlu
değildir; gövde açık tutulup üzerine parça parça yazılabilir. **Sunucu gönderimli
olaylar** (Server-Sent Events, SSE) bunu standartlaştırır: sunucudan istemciye tek yönlü,
metin tabanlı, üzerinde birden çok olay taşıyan bir akış.

#### Biçim

Akışın gövdesi satır tabanlıdır ve `text/event-stream` içerik türüyle işaretlenir. Bir
olay, alan satırlarından oluşur ve **boş bir satırla** biter. Dört alan tanımlıdır; bir de
alan sayılmayan yorum satırı vardır.

`data:` olayın gövdesidir ve tek zorunlu alandır; birden çok `data:` satırı satır
sonlarıyla birleştirilir. `event:` olayın türünü verir; istemci farklı türler için farklı
işleyiciler bağlayabilir. `id:` olayın kimliğidir ve akışın kaldığı yeri işaretler.
`retry:` istemcinin kopma sonrası kaç milisaniye bekleyeceğini bildirir. İki nokta
üst üste ile başlayan satır ise yorumdur; hiçbir olay üretmez ve **kalp atışı** olarak
kullanılır.

Biçimin tamamı düz metindir, dolayısıyla akış `curl` ile doğrudan okunabilir. Bu, hata
ayıklama açısından SSE'nin en pratik yanıdır.

#### Sunucu

Aşağıdaki sunucu, bir üyenin rezervasyon sırasındaki konumunu yayınlıyor: beş adım
ilerleme ve kitabın hazır olduğunu bildiren bir kapanış olayı.

```js
// sse.mjs — rezervasyon sirasinin ilerlemesini sunucu gonderimli olaylarla yayar
import { createServer } from "node:http";

// Uyenin rezervasyon sirasindaki konumu; son olay istegin karsilandigini bildirir.
const OLAYLAR = [
  { id: 1, tur: "sira", veri: { sira: 5, tahmin_dk: 12 } },
  { id: 2, tur: "sira", veri: { sira: 4, tahmin_dk: 9 } },
  { id: 3, tur: "sira", veri: { sira: 3, tahmin_dk: 7 } },
  { id: 4, tur: "sira", veri: { sira: 2, tahmin_dk: 4 } },
  { id: 5, tur: "sira", veri: { sira: 1, tahmin_dk: 2 } },
  { id: 6, tur: "hazir", veri: { kitap: "Tutunamayanlar", sube: 3 } },
];

const sayac = { baglanti: 0, olay: 0 };
const bekle = (ms) => new Promise((c) => setTimeout(c, ms));

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  const adres = new URL(istek.url, "http://yerel");

  if (adres.pathname === "/olcum") {
    yanit.writeHead(200, { "content-type": "application/json; charset=utf-8" });
    return yanit.end(JSON.stringify(sayac) + "\n");
  }
  if (adres.pathname !== "/akis") return yanit.writeHead(404).end();

  sayac.baglanti++;
  // Akis yaniti: govde bitmez, bu yuzden uzunluk bildirilmez ve ara katmanlarin
  // yanit tamponlamasi kapatilir.
  yanit.writeHead(200, {
    "content-type": "text/event-stream; charset=utf-8",
    "cache-control": "no-store",
    "x-accel-buffering": "no",
  });

  // Istemci kaldigi yeri Last-Event-ID basligiyla bildirir; yoksa bastan baslar.
  const kaldigiYer = Number(istek.headers["last-event-id"] ?? 0);
  const kes = Number(adres.searchParams.get("kes") ?? 0);

  yanit.write("retry: 2000\n\n");                  // yeniden baglanma araligi (ms)
  yanit.write(": nabiz\n\n");                      // yorum satiri: baglantiyi canli tutar

  let gonderilen = 0;
  for (const o of OLAYLAR.filter((o) => o.id > kaldigiYer)) {
    if (kes && gonderilen === kes) break;          // baglanti kopmasinin benzetimi
    await bekle(50);
    if (yanit.writableEnded) return;               // istemci cekildi
    yanit.write(`id: ${o.id}\nevent: ${o.tur}\ndata: ${JSON.stringify(o.veri)}\n\n`);
    sayac.olay++; gonderilen++;
  }
  yanit.end();
}).listen(8371, "127.0.0.1", () => console.log("dinleniyor: 127.0.0.1:8371"));
```

#### Akışın Okunması

`curl -N` seçeneği çıktı tamponlamasını kapatır ve akışı geldiği gibi gösterir. Aşağıdaki
betik önce akışı üç olayda kestiriyor, sonra kaldığı yerden sürdürüyor. Bağlantı noktası
8371 keyfîdir ve boş olmalıdır.

```bash
#!/usr/bin/env bash
# sse.mjs'yi baslatir, akisi kesintili okur, Last-Event-ID ile devam eder, durdurur.
node sse.mjs > /dev/null & sunucu=$!
sleep 1
A=http://127.0.0.1:8371

echo "--- ilk baglanti: uc olaydan sonra sunucu kapatiyor ---"
curl -sN "$A/akis?kes=3"
echo "--- yeniden baglanti: Last-Event-ID ile kaldigi yerden ---"
curl -sN -H 'Last-Event-ID: 3' "$A/akis"
echo "--- olcum ---"
curl -sS "$A/olcum"
kill "$sunucu"
```

```
--- ilk baglanti: uc olaydan sonra sunucu kapatiyor ---
retry: 2000

: nabiz

id: 1
event: sira
data: {"sira":5,"tahmin_dk":12}

id: 2
event: sira
data: {"sira":4,"tahmin_dk":9}

id: 3
event: sira
data: {"sira":3,"tahmin_dk":7}

--- yeniden baglanti: Last-Event-ID ile kaldigi yerden ---
retry: 2000

: nabiz

id: 4
event: sira
data: {"sira":2,"tahmin_dk":4}

id: 5
event: sira
data: {"sira":1,"tahmin_dk":2}

id: 6
event: hazir
data: {"kitap":"Tutunamayanlar","sube":3}

--- olcum ---
{"baglanti":2,"olay":6}
```

Ölçüm satırı asıl kazancı gösteriyor: **altı olay, iki bağlantı**. Kesinti olmasaydı tek
bağlantı yeterdi. Aynı altı olay uzun yoklamayla taşınsaydı altı bağlantı, altı
yetkilendirme ve altı yönlendirme kararı gerekirdi.

#### Yeniden Bağlanma

Akış er ya da geç kopar: sunucu yeniden dağıtılır, ara katman zaman aşımına uğrar, ağ
kesilir. SSE'yi uzun yoklamadan ayıran şey, kopmanın **protokolün beklediği bir durum**
olmasıdır.

İki alan bu davranışı yönetir. `retry:` ile bildirilen süre, istemcinin kopma sonrası ne
kadar bekleyeceğini belirler; sunucu bu değeri yükün durumuna göre değiştirebilir ve
böylece yeniden bağlanma fırtınasını yönetir. `id:` ile bildirilen kimlik ise istemcide
saklanır ve yeniden bağlanma isteğinde `Last-Event-ID` başlığı olarak geri gönderilir.
Sunucu bu başlığı okuyup akışa oradan devam eder.

Tarayıcı tarafındaki `EventSource` arayüzü ikisini de kendiliğinden yapar: kopmayı
algılar, `retry` süresi kadar bekler, son gördüğü kimliği başlığa koyar. Sunucunun tek
sorumluluğu, o kimliğin **anlamlı bir sürdürme noktası** olmasını sağlamaktır.

Bu, sunucu tarafında bir gereksinim doğurur. Kimliğe göre sürdürebilmek için olayların
bir süre saklanması gerekir; sunucu yalnız "şu anki durumu" biliyorsa, kopma sırasında
üretilmiş olaylar kaybolur. Uygulamada iki çözüm vardır: son N olayı bellekte tutan bir
halka tampon, ya da olay yerine **durumun tamamını** göndermek. İkincisi kimliği
gereksizleştirir çünkü her olay kendi başına yeterlidir; karşılığında her olay daha
büyüktür.

#### Akış Yanıtının Kuralları

Akış yanıtı, olağan bir yanıttan dört noktada ayrılır ve bu farkların üçü başlıklarda
görünür.

Gövdenin uzunluğu **bildirilmez**, çünkü bilinmez; `Content-Length` yazılamaz. Yanıt
saklanmamalıdır; `Cache-Control: no-store` bunu söyler, aksi hâlde bir ara önbellek akışı
tamponlayıp tek parça hâlinde teslim etmeye çalışır. Ara katmanların yanıt tamponlaması
kapatılmalıdır; kod bunu `X-Accel-Buffering` başlığıyla yapıyor, bu başlık standart değil
ama yaygın olarak tanınan bir yönergedir. Dördüncü fark koddadır: her yazmadan önce
istemcinin çekilip çekilmediği sınanır, yoksa sunucu kapalı bir yuvaya yazmayı sürdürür.

Kalp atışı satırının işlevi de buradan gelir. Uzun süre veri akmayan bir bağlantıyı ara
katmanlar ölü sayıp kapatabilir. Düzenli aralıklarla gönderilen bir yorum satırı — hiçbir
olay üretmeyen `: nabiz` — bağlantıyı canlı tutar ve istemci tarafında hiçbir yan etkisi
olmaz.

#### Sınırlar

SSE tek yönlüdür. İstemci sunucuya veri gönderemez; bunun için ayrı bir HTTP isteği
açması gerekir. Rezervasyon sırasını izlemek için bu yeterlidir, ama iki tarafın da
sürekli konuştuğu bir düzen için değildir.

İkinci sınır aktarım katmanındadır. HTTP/1.1 üzerinde tarayıcıların aynı kaynağa açtığı
eşzamanlı bağlantı sayısı sınırlıdır ve sürekli açık duran bir akış bu paydan bir tanesini
kalıcı olarak tutar; birkaç sekme açan kullanıcı sınırı doldurabilir. HTTP/2 üzerinde
akışlar tek bağlantıda çoğullandığı için bu sınır ortadan kalkar.

Üçüncü sınır veri biçimidir: akış metindir, ikili veri taşınamaz. İkili veri gerekiyorsa
kodlanması gerekir ve bu, boyutu büyütür.

#### Özet

- Sunucu gönderimli olaylar, açık tutulan tek bir HTTP yanıt gövdesine peş peşe yazılan, sunucudan istemciye tek yönlü, metin tabanlı bir akıştır.
- Olay `data`, `event`, `id` ve `retry` alanlarından oluşur ve boş satırla biter; iki nokta üst üste ile başlayan yorum satırı kalp atışı olarak kullanılır.
- Ölçümde altı olay iki bağlantıyla taşındı; aynı olaylar uzun yoklamayla altı bağlantı gerektirirdi.
- Kopma protokolün beklediği bir durumdur: istemci `retry` süresi kadar bekler ve son gördüğü kimliği `Last-Event-ID` başlığıyla gönderir, sunucu akışa oradan devam eder.
- Akış yanıtı uzunluk bildirmez, saklanmamalıdır, ara katman tamponlaması kapatılmalıdır ve her yazmadan önce istemcinin bağlı olduğu sınanmalıdır.

#### Sonraki Adım

Akış tek yönlü olduğu sürece rezervasyon sırası için yeterli. Ama kütüphane servisinde
iki tarafın da konuştuğu durumlar var: görevlinin şube stok sayacını düzeltmesi, bir
üyenin sırasından çekilmesi, birden çok görevlinin aynı ödünç kaydını aynı anda düzenlemesi.
Bunların her biri için ayrı bir HTTP isteği açmak, akışın kazandırdığı gecikmeyi geri
verir. Sonraki ders çift yönlü bir bağlantının nasıl kurulduğunu ele alır: HTTP üzerinden
başlayan, el sıkışmadan sonra kendi çerçeve biçimine geçen ve iki yönde de mesaj taşıyan
bir bağlantı.

### WebSocket Sunucusu

Source: https://academia.sh/tr/kurslar/eszamansiz-isleme/websocket-sunucusu

Çift yönlü bağlantının sıfırdan kurulması: HTTP üzerinden yükseltme el sıkışması ve kabul anahtarının üretimi, çerçeve biçiminin yazılması ve çözülmesi, yoklama–karşılık ile kapanış el sıkışması ve bağlantı durumunun yatay ölçeklemeye etkisi.

Sunucu gönderimli olaylar tek yönlüydü: sunucu konuşur, istemci dinler. Kütüphane
servisinde iki tarafın da konuştuğu durumlar var — görevlinin şube stok sayacını
düzeltmesi, üyenin rezervasyon sırasından çekilmesi. Bunların her biri için ayrı bir HTTP
isteği açmak, akışın kazandırdığı gecikmeyi geri verir.

**WebSocket**, bu ihtiyaca karşılık gelen aktarımdır: HTTP üzerinden başlayan, bir el
sıkışmadan sonra kendi çerçeve biçimine geçen ve iki yönde de mesaj taşıyan tek bir
bağlantı. Bu ders onu hazır bir paket kullanmadan kurar; çünkü kurmadan öğrenilmeyen tek
şey, bağlantının **neyi tuttuğu** ve neden ölçeklemede sorun çıkardığıdır.

#### Yükseltme El Sıkışması

Bağlantı olağan bir HTTP isteğiyle başlar. İstemci `Upgrade: websocket` ve
`Connection: Upgrade` başlıklarını, ayrıca rastgele üretilmiş on altı baytlık bir değeri
base64 ile kodlayıp `Sec-WebSocket-Key` başlığında gönderir.

Sunucu bu anahtarı belirtimde sabitlenmiş bir GUID ile birleştirir, SHA-1 özetini alır,
base64 ile kodlar ve sonucu `Sec-WebSocket-Accept` başlığında geri verir. Yanıtın durum
kodu **101 Switching Protocols**'tür ve bu noktadan sonra bağlantı üzerinde HTTP yoktur.

```js
// kabul-anahtari.mjs — el sikismadaki kabul degerinin uretimi ve dogrulanmasi
import { createHash } from "node:crypto";

const GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";  // protokolde sabitlenmis deger
const anahtar = "dGhlIHNhbXBsZSBub25jZQ==";           // belirtimdeki ornek istemci anahtari
const kabul = createHash("sha1").update(anahtar + GUID).digest("base64");

console.log("Sec-WebSocket-Key   :", anahtar);
console.log("Sec-WebSocket-Accept:", kabul);
console.log("belirtimdeki deger  :", kabul === "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=");
```

```
Sec-WebSocket-Key   : dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
belirtimdeki deger  : true
```

Bu hesabın **hiçbir güvenlik işlevi yoktur**. SHA-1 burada bir imza değil, bir
doğrulamadır: karşı tarafın isteği gerçekten WebSocket yükseltmesi olarak anladığını, yani
WebSocket konuşmayan bir sunucunun bağlantıyı yanlışlıkla kabul etmediğini gösterir. Kimlik
doğrulama, yükseltme isteğinin kendisinde — çerezde, başlıkta ya da ilk mesajda — ayrıca yapılmalıdır.

#### Çerçeve Biçimi

El sıkışmadan sonra bağlantı üzerinde **çerçeveler** (frame) taşınır. Bir çerçevenin
başlığı en az iki bayttır. İlk baytın en yüksek biti `FIN`, mesajın bu çerçeveyle bitip
bitmediğini söyler; alt dört bit **işlem kodudur**: metin, ikili, kapat, yokla, karşılık.

İkinci baytın en yüksek biti **maske bitidir**, kalan yedi bit uzunluktur. Uzunluk 125'e
kadar doğrudan yazılır; 126 değeri sonraki iki baytın, 127 değeri sonraki sekiz baytın
gerçek uzunluğu taşıdığını bildirir.

Maske kuralı tek yönlüdür: istemciden sunucuya giden her çerçeve maskelenmek
**zorundadır**, sunucudan istemciye giden hiçbiri maskelenmemelidir. Maskeleme gizlilik
sağlamaz — anahtar çerçevenin içindedir. Amacı, aradaki eski bir aracının çerçeve
gövdesini geçerli bir HTTP isteği sanıp önbelleğini zehirlemesini engellemektir.

```js
// cerceve.mjs — WebSocket cerceve bicimini yazar ve cozer
import { randomBytes } from "node:crypto";

export const ISLEM = { metin: 0x1, ikili: 0x2, kapat: 0x8, yokla: 0x9, karsilik: 0xa };

// Cerceve: [FIN + islem kodu][maske biti + uzunluk][genisletilmis uzunluk?]
//          [maske anahtari?][govde].  Istemciden gelen her cerceve maskeli olmak zorunda.
export function cerceveYaz(islem, govde, maskeli = false) {
  const veri = Buffer.isBuffer(govde) ? govde : Buffer.from(String(govde), "utf8");
  const n = veri.length;
  const ek = n < 126 ? 0 : n < 65536 ? 2 : 8;
  const baslik = Buffer.alloc(2 + ek + (maskeli ? 4 : 0));
  baslik[0] = 0x80 | islem;                        // FIN = 1: cerceve tek parca
  baslik[1] = (maskeli ? 0x80 : 0) | (ek === 0 ? n : ek === 2 ? 126 : 127);
  if (ek === 2) baslik.writeUInt16BE(n, 2);
  if (ek === 8) baslik.writeBigUInt64BE(BigInt(n), 2);
  if (!maskeli) return Buffer.concat([baslik, veri]);
  const anahtar = randomBytes(4);
  anahtar.copy(baslik, 2 + ek);
  const maskelenmis = Buffer.from(veri);
  for (let i = 0; i < n; i++) maskelenmis[i] ^= anahtar[i % 4];
  return Buffer.concat([baslik, maskelenmis]);
}

// Tampondan tam bir cerceve cozer; cerceve tamamlanmamissa null doner.
export function cerceveCoz(tampon) {
  if (tampon.length < 2) return null;
  const islem = tampon[0] & 0x0f, son = (tampon[0] & 0x80) !== 0;
  const maskeli = (tampon[1] & 0x80) !== 0;
  let n = tampon[1] & 0x7f, konum = 2;
  if (n === 126) { if (tampon.length < 4) return null; n = tampon.readUInt16BE(2); konum = 4; }
  else if (n === 127) {
    if (tampon.length < 10) return null;
    n = Number(tampon.readBigUInt64BE(2)); konum = 10;
  }
  const anahtar = maskeli ? tampon.subarray(konum, konum + 4) : null;
  if (maskeli) konum += 4;
  if (tampon.length < konum + n) return null;
  const govde = Buffer.from(tampon.subarray(konum, konum + n));
  if (anahtar) for (let i = 0; i < n; i++) govde[i] ^= anahtar[i % 4];
  return { islem, son, maskeli, govde, tuketilen: konum + n };
}
```

`cerceveCoz` işlevinin tamamlanmamış çerçevede `null` döndürmesi bir ayrıntı değil,
zorunluluktur. Alt katman TCP'dir ve TCP bir **bayt akışıdır**: gönderilen bir çerçeve
iki parça hâlinde gelebilir, iki çerçeve tek parçada gelebilir. Bu yüzden okuyan taraf
gelen baytları biriktirir ve tampondan çözebildiği kadar çerçeve çeker.

#### Sunucu

Sunucu, HTTP sunucusunun `upgrade` olayına bağlanır. Bu olay, yükseltme isteği geldiğinde
ham yuvayı (socket) verir; yanıt satırını ve başlıklarını sunucunun kendisi yazar.

```js
// ws-sunucu.mjs — el sikismayi ve cerceve cozumlemesini kendisi yapan WebSocket sunucusu
import { createServer } from "node:http";
import { createHash } from "node:crypto";
import { ISLEM, cerceveYaz, cerceveCoz } from "./cerceve.mjs";

const GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
const kabulUret = (anahtar) => createHash("sha1").update(anahtar + GUID).digest("base64");

const musteri = new Set();
const kutuk = (...s) => console.log(...s);

const sunucu = createServer((istek, yanit) => {
  yanit.sendDate = false;
  yanit.writeHead(426, { "content-type": "text/plain", upgrade: "websocket" });
  yanit.end("bu uc yalniz WebSocket baglantisi kabul eder\n");
});

sunucu.on("upgrade", (istek, yuva) => {
  const anahtar = istek.headers["sec-websocket-key"];
  if (istek.headers.upgrade?.toLowerCase() !== "websocket" || !anahtar)
    return yuva.destroy();

  yuva.write("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n" +
             `Connection: Upgrade\r\nSec-WebSocket-Accept: ${kabulUret(anahtar)}\r\n\r\n`);
  musteri.add(yuva);
  kutuk(`baglanti acildi   (acik=${musteri.size})`);

  const gonder = (islem, govde) => yuva.write(cerceveYaz(islem, govde));  // sunucu maskelemez
  let tampon = Buffer.alloc(0);

  yuva.on("data", (parca) => {
    tampon = Buffer.concat([tampon, parca]);
    let c;
    while ((c = cerceveCoz(tampon))) {
      tampon = tampon.subarray(c.tuketilen);
      kutuk(`cerceve alindi    islem=0x${c.islem.toString(16)}` +
            ` maskeli=${c.maskeli} uzunluk=${c.govde.length}`);

      if (c.islem === ISLEM.metin) {
        const istem = JSON.parse(c.govde.toString());
        gonder(ISLEM.metin, JSON.stringify({ tur: "onay", sube: istem.sube, yeniStok: 41 }));
      } else if (c.islem === ISLEM.yokla) {
        gonder(ISLEM.karsilik, c.govde);            // yoklamaya ayni govdeyle karsilik
      } else if (c.islem === ISLEM.kapat) {
        const kod = c.govde.length >= 2 ? c.govde.readUInt16BE(0) : 1005;
        kutuk(`kapanis istegi    kod=${kod} neden=${c.govde.subarray(2).toString()}`);
        gonder(ISLEM.kapat, c.govde);               // kapanis el sikismasi: ayni kodla don
        yuva.end();
      }
    }
  });

  yuva.on("close", () => {
    musteri.delete(yuva);
    kutuk(`baglanti kapandi  (acik=${musteri.size})`);
  });
});

sunucu.listen(8381, "127.0.0.1", () => kutuk("dinleniyor: 127.0.0.1:8381"));
```

#### İstemci

İstemci de aynı biçimi kullanır, iki farkla: el sıkışma isteğini kendisi yazar ve
gönderdiği her çerçeveyi maskeler.

```js
// ws-istemci.mjs — ham TCP uzerinde el sikisip cerceve gonderen kucuk WebSocket istemcisi
import { connect } from "node:net";
import { createHash, randomBytes } from "node:crypto";
import { ISLEM, cerceveYaz, cerceveCoz } from "./cerceve.mjs";

const GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
const anahtar = randomBytes(16).toString("base64");
const beklenenKabul = createHash("sha1").update(anahtar + GUID).digest("base64");

const kuyruk = [], bekleyen = [];
const cerceveAl = () => kuyruk.length
  ? Promise.resolve(kuyruk.shift())
  : new Promise((c) => bekleyen.push(c));
const cerceveGeldi = (c) => (bekleyen.length ? bekleyen.shift()(c) : kuyruk.push(c));

let tampon = Buffer.alloc(0), elSikisildi = false;
const yuva = connect(8381, "127.0.0.1", () => {
  yuva.write("GET /kanal HTTP/1.1\r\nHost: 127.0.0.1:8381\r\n" +
             "Upgrade: websocket\r\nConnection: Upgrade\r\n" +
             `Sec-WebSocket-Key: ${anahtar}\r\nSec-WebSocket-Version: 13\r\n\r\n`);
});

yuva.on("error", (h) => {                        // sunucu ayakta degilse sessizce cik
  console.log("sunucuya baglanilamadi:", h.code);
  process.exit(0);
});

yuva.on("data", (parca) => {
  tampon = Buffer.concat([tampon, parca]);
  if (!elSikisildi) {
    const son = tampon.indexOf("\r\n\r\n");
    if (son < 0) return;
    const basliklar = tampon.subarray(0, son).toString();
    tampon = tampon.subarray(son + 4);
    elSikisildi = true;
    console.log("durum satiri      :", basliklar.split("\r\n")[0]);
    console.log("kabul anahtari    :",
      /sec-websocket-accept: (.+)/i.exec(basliklar)?.[1] === beklenenKabul ? "dogru" : "yanlis");
    konus();
  }
  let c;
  while ((c = cerceveCoz(tampon))) { tampon = tampon.subarray(c.tuketilen); cerceveGeldi(c); }
});

async function konus() {
  const gonder = (islem, govde) => yuva.write(cerceveYaz(islem, govde, true));

  gonder(ISLEM.metin, JSON.stringify({ tur: "stok", sube: 3, delta: -1 }));
  const y1 = await cerceveAl();
  console.log("metin karsiligi   :", y1.govde.toString(), `(maskeli=${y1.maskeli})`);

  gonder(ISLEM.yokla, "canli-mi");
  const y2 = await cerceveAl();
  console.log("yoklama karsiligi : islem=0x" + y2.islem.toString(16), y2.govde.toString());

  const neden = Buffer.concat([Buffer.from([0x03, 0xe8]), Buffer.from("is bitti")]);
  gonder(ISLEM.kapat, neden);                     // 0x03e8 = 1000, olagan kapanis
  const y3 = await cerceveAl();
  console.log("kapanis karsiligi : islem=0x" + y3.islem.toString(16),
              "kod=" + y3.govde.readUInt16BE(0));
  yuva.end();
}
```

#### Yaşam Döngüsü

Aşağıdaki betik önce yükseltmesiz bir HTTP isteği gönderiyor, sonra istemciyi
koşturuyor. Bağlantı noktası 8381 keyfîdir ve boş olmalıdır.

```bash
#!/usr/bin/env bash
# ws-sunucu.mjs'yi baslatir, once duz HTTP istegini, sonra istemciyi kosturur, durdurur.
node ws-sunucu.mjs > sunucu.log & sunucu=$!
sleep 1

echo "--- yukseltmesiz duz HTTP istegi ---"
curl -sS -i http://127.0.0.1:8381/kanal | head -3
echo "--- istemcinin gordugu ---"
node ws-istemci.mjs
sleep 0.3
echo "--- sunucunun kutugu ---"
cat sunucu.log
kill "$sunucu"
```

```
--- yukseltmesiz duz HTTP istegi ---
HTTP/1.1 426 Upgrade Required
content-type: text/plain
upgrade: websocket
--- istemcinin gordugu ---
durum satiri      : HTTP/1.1 101 Switching Protocols
kabul anahtari    : dogru
metin karsiligi   : {"tur":"onay","sube":3,"yeniStok":41} (maskeli=false)
yoklama karsiligi : islem=0xa canli-mi
kapanis karsiligi : islem=0x8 kod=1000
--- sunucunun kutugu ---
dinleniyor: 127.0.0.1:8381
baglanti acildi   (acik=1)
cerceve alindi    islem=0x1 maskeli=true uzunluk=34
cerceve alindi    islem=0x9 maskeli=true uzunluk=8
cerceve alindi    islem=0x8 maskeli=true uzunluk=10
kapanis istegi    kod=1000 neden=is bitti
baglanti kapandi  (acik=0)
```

Kütük yaşam döngüsünün dört evresini gösteriyor. **Açılış**: yükseltme kabul edilir,
bağlantı sayacı artar. **Veri**: istemciden gelen metin çerçevesi maskelidir, dönen
karşılık değildir. **Denetim**: yoklama (0x9) çerçevesine aynı gövdeyle karşılık (0xa)
verilir. **Kapanış**: 0x8 çerçevesi bir kapanış **isteğidir**, emir değil; karşı taraf
kendi kapanış çerçevesini gönderdikten sonra yuva kapanır.

Yoklama–karşılık çifti gerçek işlerde vazgeçilmezdir. TCP bağlantısı, karşı taraf sessizce
kaybolduğunda uzun süre açık görünebilir; düzenli aralıklarla gönderilen yoklamaya
karşılık gelmemesi, bağlantının ölü olduğunun tek güvenilir işaretidir. Aynı trafik, ara
katmanların boşta duran bağlantıyı kapatmasını da engeller — sunucu gönderimli olaylardaki
kalp atışı satırının burada karşılık gelen biçimidir.

#### Ölçekleme

WebSocket ile HTTP arasındaki asıl fark başlıklarda değil, **durumdadır**. Olağan bir
HTTP isteği durumsuzdur; hangi sunucu örneğine düşerse düşsün aynı sonucu verir.
WebSocket bağlantısı ise bir örneğe **bağlanır** ve bağlantı süresince orada kalır.

Bunun üç sonucu vardır. Ölçekleme birimi artık istek değil bağlantıdır; sunucunun
kapasitesi saniyedeki istek sayısıyla değil, aynı anda tutabildiği yuva ve bellek
miktarıyla ölçülür. Yeniden dağıtım her açık bağlantıyı koparır; istemci tarafında
yeniden bağlanma ve durum tazeleme zorunludur. Ve en önemlisi: aynı kanalı dinleyen iki
istemci **farklı örneklere** düşmüş olabilir. Bir örneğe gelen mesaj, diğerinin
istemcisine kendiliğinden ulaşmaz.

Bu üçüncü sonuç, çok örnekli her gerçek zamanlı kurulumun temel sorunudur.

#### Özet

- WebSocket bağlantısı HTTP isteğiyle başlar; sunucu istemci anahtarını sabit bir GUID ile birleştirip SHA-1 özetini base64 olarak geri verir ve 101 ile protokolü değiştirir.
- Kabul anahtarı bir güvenlik önlemi değil, isteğin WebSocket olarak anlaşıldığının ve eski bir yanıtın tekrarlanmadığının doğrulamasıdır.
- Çerçeve başlığı FIN bitini, işlem kodunu, maske bitini ve uzunluğu taşır; istemciden gelen her çerçeve maskeli, sunucudan gidenler maskesizdir.
- Alt katman bayt akışı olduğu için çözücü tamamlanmamış çerçevede beklemeli, tampondan çözebildiği kadar çerçeve çekmelidir.
- Yaşam döngüsü açılış, veri, yoklama–karşılık ve iki taraflı kapanış el sıkışmasıdır; bağlantının bir örneğe bağlanması, ölçekleme biriminin istek değil bağlantı olmasına yol açar.

#### Sonraki Adım

Bu derste tek bir sunucu örneği ve tek bir istemci vardı; mesajın gideceği yer belliydi.
Gerçek kurulumda ise aynı kanalı dinleyen istemciler farklı örneklere dağılır: şube stok
sayacını izleyen üç görevliden ikisi birinci örneğe, biri ikinciye bağlanmış olabilir.
Birinci örneğe gelen bir güncelleme, ikinci örnekteki görevliye ulaşmaz — çünkü o mesajı
kimse ona iletmez. Sonraki ders bu boşluğu kapatan yapıyı kurar ve mesajın bir örnekten
diğerinin abonesine gerçekten ulaştığını koşturarak gösterir.

### Yayın ve Kanal Yönetimi

Source: https://academia.sh/tr/kurslar/eszamansiz-isleme/yayin-ve-kanal-yonetimi

Çok örnekli kurulumda mesajın bütün abonelere ulaşması: kanal kavramı, yerel dağıtımın yetersizliğinin ölçülmesi, örnekler arası yayın kanalının kurulması, yayma maliyeti ve abonelik muhasebesi.

Bir önceki ders çift yönlü bağlantıyı kurdu ve bir uyarıyla bitti: WebSocket bağlantısı
bir sunucu örneğine bağlanır ve bağlantı süresince orada kalır. Aynı sonuç sunucu
gönderimli olaylar için de geçerlidir; açık tutulan her akış belirli bir örneğin
belleğinde durur.

Tek örnekli bir kurulumda bu görünmez. İki örneğe dağıtılmış bir serviste ise sonucu
şudur: şube stok sayacını izleyen üç görevliden ikisi birinci örneğe, biri ikinciye
bağlanmışsa, birinci örneğe gelen bir güncelleme üçüncü görevliye **ulaşmaz**. Bu ders
o boşluğu ölçer ve kapatır.

#### Kanal

Önce adlandırma. Bir istemcinin ilgilendiği mesaj kümesine **kanal** (channel) denir;
kütüphane servisinde `sube-3`, `odunc-1204` ya da `rapor-2026-01` gibi. Kanal, mesajlaşma
konusunda tanıtılan kuyruktan iki noktada ayrılır.

Kanalın **belleği yoktur**: yayın anında dinlemeyen bir aboneye mesaj ulaşmaz ve bir yerde
birikmez. Ve kanal **çok aboneli**dir: aynı mesajın kopyası her aboneye gider, ilk alan
mesajı tüketmiş olmaz. Yani kanal, mesajlaşma konusundaki konunun ve yayımla–abone ol
modelinin bağlantı düzeyindeki karşılığıdır.

Bu iki özellik gerçek zamanlı veri için genellikle doğrudur — şube stok sayacının üç
saniye önceki değeri kimseyi ilgilendirmez. Kalıcılık gerekiyorsa kanal yetmez; mesajın
ayrıca dayanıklı bir kuyruğa ya da tabloya yazılması gerekir.

#### Uygulama Örneği

Aşağıdaki örnek iki uç sunuyor. `/abone` istemciye bir olay akışı açar ve onu kanal
kümesine ekler; `/yayinla` bir kanala mesaj basar. İki kipte çalışıyor: yayını yalnız
kendi abonelerine veren `--yerel` kipi ve yayını aktarıcıya gönderen `--aktarici` kipi.

```js
// ornek.mjs — uygulama ornegi: istemcilere kanal aboneligi verir, yayin uretir
//   node ornek.mjs <nokta> --yerel      yayin yalniz kendi abonelerine gider
//   node ornek.mjs <nokta> --aktarici   yayin aktariciya gider, aktarici herkese dagitir
import { createServer } from "node:http";

const NOKTA = Number(process.argv[2] ?? 8392);   // varsayilan: birinci ornek
const AKTARICILI = process.argv.includes("--aktarici");
const AKTARICI = "http://127.0.0.1:8391";

const kanallar = new Map();                        // kanal -> Set<istemci yaniti>
const sayac = { abonelik: 0, yerel_teslim: 0, aktaricidan: 0 };

const govdeOku = (istek) => new Promise((c) => {
  let v = ""; istek.on("data", (p) => (v += p)); istek.on("end", () => c(v));
});

function yerelDagit(kanal, govde) {
  for (const y of kanallar.get(kanal) ?? []) {
    y.write(`data: ${govde}\n\n`);
    sayac.yerel_teslim++;
  }
}

// Aktariciyi dinleyen taraf: gelen her yayini kendi yerel abonelerine dagitir.
async function aktariciyiDinle() {
  const yanit = await fetch(`${AKTARICI}/dinle`);
  let tampon = "";
  for await (const parca of yanit.body) {
    tampon += Buffer.from(parca).toString();
    let k;
    while ((k = tampon.indexOf("\n\n")) >= 0) {
      const blok = tampon.slice(0, k); tampon = tampon.slice(k + 2);
      if (!blok.startsWith("data: ")) continue;    // yorum satiri: yok say
      const ileti = JSON.parse(blok.slice(6));
      sayac.aktaricidan++;
      yerelDagit(ileti.kanal, ileti.veri);
    }
  }
}

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  const adres = new URL(istek.url, "http://yerel");
  const kanal = adres.searchParams.get("kanal");

  if (adres.pathname === "/abone") {
    yanit.writeHead(200, { "content-type": "text/event-stream; charset=utf-8",
                           "cache-control": "no-store" });
    yanit.write(`: abone olundu kanal=${kanal} ornek=${NOKTA}\n\n`);
    if (!kanallar.has(kanal)) kanallar.set(kanal, new Set());
    kanallar.get(kanal).add(yanit);
    sayac.abonelik++;
    return void istek.on("close", () => kanallar.get(kanal).delete(yanit));
  }

  if (adres.pathname === "/yayinla") {
    const veri = await govdeOku(istek);
    if (AKTARICILI) await fetch(`${AKTARICI}/yayin`, {
      method: "POST", body: JSON.stringify({ kanal, veri }),
    });
    else yerelDagit(kanal, veri);
    return yanit.writeHead(202).end();
  }

  if (adres.pathname === "/olcum") {
    yanit.writeHead(200, { "content-type": "application/json; charset=utf-8" });
    return yanit.end(JSON.stringify({ ornek: NOKTA, ...sayac }) + "\n");
  }
  yanit.writeHead(404).end();
}).listen(NOKTA, "127.0.0.1", () => {
  if (AKTARICILI) aktariciyiDinle();
  console.log(`ornek: 127.0.0.1:${NOKTA}`);
});
```

Aboneliğin bağlantı kapandığında **silinmesi** kritik bir ayrıntıdır. `istek.on("close")`
işleyicisi olmadan kanal kümesi kapanmış yanıt nesnelerini tutmaya devam eder; bunlara
yazma denemesi hata üretir ve küme sınırsız büyür. Uzun ömürlü bağlantı tutan her yapıda
en sık görülen sızıntı budur.

#### Örnekler Arası Yayın Kanalı

Örneğin `--aktarici` kipinde konuştuğu yapı, tek işi mesajı bütün örneklere ulaştırmak
olan bir ara katmandır. Her örnek ona bir akışla bağlanır; bir örnekten gelen yayın,
bağlı olan **tüm** örneklere yazılır — yayını gönderen örnek de dâhil.

```js
// aktarici.mjs — ornekler arasi yayin kanali; her uygulama ornegine bir akis acik tutar
import { createServer } from "node:http";

const ornekler = new Set();                        // aktariciyi dinleyen ornek yanitlari
const sayac = { yayin: 0, dagitim: 0 };

const govdeOku = (istek) => new Promise((c) => {
  let v = ""; istek.on("data", (p) => (v += p)); istek.on("end", () => c(v));
});

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  const adres = new URL(istek.url, "http://yerel");

  if (adres.pathname === "/dinle") {                // ornek aktariciya baglanir
    yanit.writeHead(200, { "content-type": "text/event-stream; charset=utf-8",
                           "cache-control": "no-store" });
    yanit.write(": aktariciya baglanildi\n\n");
    ornekler.add(yanit);
    return void istek.on("close", () => ornekler.delete(yanit));
  }

  if (adres.pathname === "/yayin") {                // bir ornekten gelen yayin: herkese
    const govde = await govdeOku(istek);
    sayac.yayin++;
    for (const o of ornekler) { o.write(`data: ${govde}\n\n`); sayac.dagitim++; }
    return yanit.writeHead(202).end();
  }

  if (adres.pathname === "/olcum") {
    yanit.writeHead(200, { "content-type": "application/json; charset=utf-8" });
    return yanit.end(JSON.stringify({ ...sayac, ornek: ornekler.size }) + "\n");
  }
  yanit.writeHead(404).end();
}).listen(8391, "127.0.0.1", () => console.log("aktarici: 127.0.0.1:8391"));
```

Yayını gönderen örneğin de aktarıcıdan geri alması bir kusur değil, sadeleştirmedir:
yerel dağıtım tek bir yerde, aktarıcıdan gelen yolda yapılır. Aksi hâlde mesaj kendi
örneğinde iki kez teslim edilir.

#### Ölçüm

Aşağıdaki betik iki uygulama örneği ile bir aktarıcı çalıştırıyor. Abone **ikinci**
örneğe bağlanıyor, yayın ise **birinci** örneğe geliyor. Bağlantı noktaları 8391–8393
keyfîdir ve boş olmalıdır.

```bash
#!/usr/bin/env bash
# Iki uygulama ornegi + bir aktarici; abone ikinci ornekte, yayin birinci ornege gelir.
olc() {
  node aktarici.mjs > /dev/null & a=$!
  node ornek.mjs 8392 "$1" > /dev/null & o1=$!
  node ornek.mjs 8393 "$1" > /dev/null & o2=$!
  sleep 1

  curl -sN --max-time 1.2 "http://127.0.0.1:8393/abone?kanal=sube-3" > abone.txt &
  dinleyen=$!
  sleep 0.4
  curl -sS -o /dev/null -X POST -d '{"sube":3,"stok":41}' \
       "http://127.0.0.1:8392/yayinla?kanal=sube-3"
  wait $dinleyen

  echo "--- $2 ---"
  echo "ikinci ornekteki abonenin gordugu:"
  sed '/^$/d; s/^/  /' abone.txt
  echo "sayaclar:"
  curl -sS http://127.0.0.1:8392/olcum | sed 's/^/  /'
  curl -sS http://127.0.0.1:8393/olcum | sed 's/^/  /'
  curl -sS http://127.0.0.1:8391/olcum | sed 's/^/  aktarici /'
  kill $a $o1 $o2 2>/dev/null
  sleep 0.5
}

olc --yerel    "aktarici yok"
olc --aktarici "aktarici var"
```

```
--- aktarici yok ---
ikinci ornekteki abonenin gordugu:
  : abone olundu kanal=sube-3 ornek=8393
sayaclar:
  {"ornek":8392,"abonelik":0,"yerel_teslim":0,"aktaricidan":0}
  {"ornek":8393,"abonelik":1,"yerel_teslim":0,"aktaricidan":0}
  aktarici {"yayin":0,"dagitim":0,"ornek":0}
--- aktarici var ---
ikinci ornekteki abonenin gordugu:
  : abone olundu kanal=sube-3 ornek=8393
  data: {"sube":3,"stok":41}
sayaclar:
  {"ornek":8392,"abonelik":0,"yerel_teslim":0,"aktaricidan":1}
  {"ornek":8393,"abonelik":1,"yerel_teslim":1,"aktaricidan":1}
  aktarici {"yayin":1,"dagitim":2,"ornek":2}
```

Birinci koşumda abone yalnız abonelik onayını gördü; yayın hiçbir yere gitmedi. Birinci
örneğin `yerel_teslim` sayacı sıfır, çünkü o örnekte abone yok. Mesaj kaybolmadı, hata da
üretmedi — **sessizce hiçbir şey olmadı**. Çok örnekli kurulumlarda bu kusurun tehlikeli
yanı budur: tek örnekle geliştirilirken hiçbir belirti vermez.

İkinci koşumda abone mesajı aldı. Sayaçlar yolu adım adım gösteriyor: aktarıcıya bir
yayın geldi, iki örneğe dağıtıldı, her iki örnek de birer mesaj aldı, ama yalnız aboneye
sahip olan örnek yerel teslim yaptı.

#### Yayma Maliyeti

Aktarıcının `dagitim` sayacı yayının değil, kopyaların sayısıdır. Bir yayın, örnek sayısı
kadar kopyaya dönüşür. $N$ örnek ve saniyede $M$ yayın için aktarıcının yazdığı mesaj
sayısı $N \times M$'dir ve bu, abonesi olmayan örnekler için tamamen boşa giden bir
iştir. Ölçümde birinci örnek mesajı aldı ve hiçbir yere teslim etmedi.

İki azaltma yolu vardır. **Abonelik kaydı** tutmakta aktarıcı her örneğin hangi kanalları
dinlediğini bilir ve yayını yalnız ilgili örneklere gönderir. Kazanç, kanal sayısı örnek
sayısından büyük olduğunda belirgindir; bedeli, her abone açılıp kapandığında aktarıcıya
bir kayıt mesajı gitmesidir. **Kanal bölümleme**de ise kanallar örneklere önceden
paylaştırılır ve istemci ilgilendiği kanalı barındıran örneğe yönlendirilir; dağıtım
tamamen ortadan kalkar ama esneklik de kalkar, çünkü bir istemci birden çok kanalı
dinliyorsa birden çok bağlantı açması gerekir.

Ölçek büyüdükçe üçüncü bir sınır belirir: aktarıcının kendisi tek bir noktadır. Gerçek
kurulumlarda bu iş, bu kursun mesajlaşma konusunda tanıtılan yayımla–abone ol
altyapısına devredilir; aktarıcı o altyapının önündeki ince bir uyarlama katmanına
dönüşür.

#### Yapışkan Yönlendirme Yeterli mi

Sık başvurulan bir kestirme, aynı istemcinin hep aynı örneğe yönlendirilmesidir; buna
**yapışkan yönlendirme** (sticky routing) denir. Bağlantı koptuğunda istemcinin aynı
örneğe dönmesini sağladığı için yararlıdır, ama buradaki sorunu **çözmez**: aynı kanalı
dinleyen iki farklı istemci yine farklı örneklere düşebilir.

Yapışkanlık ancak kanal tabanlı olduğunda çözüm olur — yani yönlendirme kararı istemciye
değil kanala bakarak verildiğinde. Bu da yukarıdaki kanal bölümleme yaklaşımının ta
kendisidir ve yük dağıtıcının uygulama kavramlarını bilmesini gerektirir.

#### Özet

- Kanal, istemcinin ilgilendiği mesaj kümesidir; belleği yoktur ve çok abonelidir, bu yüzden kalıcılık gerektiren veriler ayrıca yazılmalıdır.
- Açık bağlantı belirli bir örneğin belleğinde durduğu için, yerel dağıtım çok örnekli kurulumda mesajı diğer örneklerin abonelerine ulaştırmaz ve bu kusur hiçbir hata üretmez.
- Örnekler arası bir yayın kanalı boşluğu kapatır; yayını gönderen örneğin de mesajı geri alması, yerel dağıtımı tek bir yolda toplar.
- Abonelik bağlantı kapandığında silinmezse kanal kümesi kapanmış yanıtları tutar ve sınırsız büyür.
- Yayma maliyeti örnek sayısıyla çarpılır; abonelik kaydı ya da kanal bölümleme bunu azaltır, yapışkan yönlendirme ise tek başına çözmez.

#### Sonraki Adım

Bu konudaki dört yaklaşım da sunucudan istemciye **olay** taşıdı: durum değişti, sıra
ilerledi, stok güncellendi. Geriye tek bir durum kalıyor. Bazen taşınacak şey bir olay
değil, **yanıtın kendisidir** — otuz bin ödünç kaydını tarayan raporun çıktısı, üretildiği
hızla istemciye akıtılabilir. Bu, yeni bir protokol gerektirmez; HTTP yanıt gövdesinin
parça parça yazılmasıyla yapılır. Sonraki ders bu son biçimi kurar ve kursu kapatır.

### Akış Yanıtları

Source: https://academia.sh/tr/kurslar/eszamansiz-isleme/akis-yanitlari

Yanıtın kendisinin parça parça üretilmesi: parçalı aktarımın çerçevelemesi, ilk bayta kadar geçen sürenin ölçülmesi, başlıklar gönderildikten sonra hata bildirmenin sınırı ve akış yanıtında geri basınç.

Bu konudaki dört yaklaşım da sunucudan istemciye **olay** taşıdı: durum değişti, sıra
ilerledi, stok güncellendi. Geriye tek bir durum kalıyor. Bazen taşınacak şey bir olay
değil, **yanıtın kendisidir**.

Otuz bin ödünç kaydını tarayan aylık rapor, sonuç tamamlanmadan da satır satır
gönderilebilir. Bu, yeni bir protokol gerektirmez; HTTP yanıt gövdesinin parça parça
yazılmasıyla yapılır ve karşılığında istemcinin ilk veriyi görmesi için beklediği süre
belirgin ölçüde kısalır.

#### İki Biçim

Bir yanıt gövdesi iki biçimde gönderilebilir. Uzunluğu biliniyorsa `Content-Length`
başlığında bildirilir ve alıcı kaç bayt bekleyeceğini baştan bilir. Uzunluğu bilinmiyorsa
**parçalı aktarım** (chunked transfer encoding) kullanılır: gövde, her biri kendi
uzunluğunu taşıyan parçalara bölünür ve son parça sıfır uzunlukla akışın bittiğini
bildirir.

Aşağıdaki sunucu aynı raporu üç yolla veriyor: tamamını biriktirip gönderen `/tam`,
her satırı ürettiği anda yazan `/akan` ve üretimi yarıda bırakan `/kesik`.

```js
// parcali.mjs — aylik odunc raporunu iki bicimde sunar
//   /tam    tum satirlar bellekte toplanir, tek parca halinde gonderilir
//   /akan   her satir uretildigi anda yazilir: parcali aktarim
//   /kesik  ucuncu satirdan sonra uretim basarisiz olur
import { createServer } from "node:http";

const SATIR = 10;
const bekle = (ms) => new Promise((c) => setTimeout(c, ms));
const satirUret = (i) =>
  `${String(i).padStart(2, "0")};sube-${1 + (i % 3)};odunc=${40 + i * 3}\n`;

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  const yol = new URL(istek.url, "http://yerel").pathname;

  if (yol === "/tam") {                            // uzunluk biliniyor: tek parca
    const parcalar = [];
    for (let i = 1; i <= SATIR; i++) { await bekle(100); parcalar.push(satirUret(i)); }
    const govde = parcalar.join("");
    yanit.writeHead(200, { "content-type": "text/csv; charset=utf-8",
                           "content-length": Buffer.byteLength(govde) });
    return yanit.end(govde);
  }

  if (yol === "/akan" || yol === "/kesik") {       // uzunluk bilinmiyor: parcali aktarim
    yanit.writeHead(200, { "content-type": "text/csv; charset=utf-8" });
    for (let i = 1; i <= SATIR; i++) {
      await bekle(100);
      if (yanit.writableEnded) return;             // istemci cekildi
      if (yol === "/kesik" && i === 4) {
        // Basliklar coktan gonderildi: durum kodu artik degistirilemez.
        yanit.end("#hata;rapor yarida kesildi;satir=3\n");
        return;
      }
      if (!yanit.write(satirUret(i))) await new Promise((c) => yanit.once("drain", c));
    }
    return yanit.end();
  }
  yanit.writeHead(404).end();
}).listen(8401, "127.0.0.1", () => console.log("dinleniyor: 127.0.0.1:8401"));
```

Parçalı aktarımı açıkça istemek gerekmez. `Content-Length` yazılmadığında ve gövde tek
çağrıda bitirilmediğinde çalışma zamanı çerçevelemeyi kendisi yapar. Yazılan tek şey
satırın kendisidir.

Akan yanıtın her yazmadan önce `writableEnded` durumunu sınaması, sunucu gönderimli
olaylardaki kuralın aynısıdır: istemci çekildiğinde üretimi sürdürmek boşa harcanan
işlemci ve kapalı yuvaya yazma hatası demektir.

#### Ölçüm

Aşağıdaki betik iki biçimi başlık, gecikme ve parça varış anları açısından karşılaştırıyor.
Ölçümü yapan `zamanlayici.mjs` her parçanın geliş anını yüz milisaniyelik dilimlerle
yazıyor.

```js
// zamanlayici.mjs — akan yanitin her parcasinin varis anini 100 ms dilim olarak yazar
const basla = performance.now();
let yanit;
try {
  yanit = await fetch("http://127.0.0.1:8401/akan");
} catch {
  console.log("sunucuya baglanilamadi");
  process.exit(0);
}
for await (const parca of yanit.body) {
  const dilim = Math.round((performance.now() - basla) / 100);
  process.stdout.write(`${String(dilim).padStart(2)} dilim  ${Buffer.from(parca)}`);
}
```

Bağlantı noktası 8401 keyfîdir ve boş olmalıdır.

```bash
#!/usr/bin/env bash
# parcali.mjs'yi baslatir; basliklari, gecikmeleri ve parca varislerini olcer.
node parcali.mjs > /dev/null & sunucu=$!
sleep 1
A=http://127.0.0.1:8401

echo "--- yanit basliklari ---"
for yol in tam akan; do
  printf '%-5s ' "$yol"
  curl -sS -o /dev/null -D - "$A/$yol" | grep -i -E 'content-length|transfer-encoding' | tr -d '\r' | paste -sd' ' -
done

echo "--- ilk bayt ve toplam sure (100 ms dilim) ---"
for yol in tam akan; do
  curl -sS -o /dev/null -w "$yol %{time_starttransfer} %{time_total}\n" "$A/$yol"
done | awk '{ printf "%-5s ilk-bayt=%2d dilim  toplam=%2d dilim\n", $1, int($2*10), int($3*10) }'

echo "--- akan yanitin parcalarinin varis anlari ---"
node zamanlayici.mjs

echo "--- ham parcali cerceveleme (ilk uc parca) ---"
curl -sS --raw -N "$A/akan" 2>/dev/null | head -c 60 | od -c | head -4

echo "--- uretim yarida kesilirse ---"
curl -sS -w 'durum=%{http_code}\n' "$A/kesik"
kill "$sunucu"
```

{/* mask: ^[ 0-9]{2}(?= dilim  (07|08|09|10);) */}
```
--- yanit basliklari ---
tam   content-length: 190
akan  Transfer-Encoding: chunked
--- ilk bayt ve toplam sure (100 ms dilim) ---
tam   ilk-bayt=10 dilim  toplam=10 dilim
akan  ilk-bayt= 1 dilim  toplam=10 dilim
--- akan yanitin parcalarinin varis anlari ---
 1 dilim  01;sube-2;odunc=43
 2 dilim  02;sube-3;odunc=46
 3 dilim  03;sube-1;odunc=49
 4 dilim  04;sube-2;odunc=52
 5 dilim  05;sube-3;odunc=55
 6 dilim  06;sube-1;odunc=58
 7 dilim  07;sube-2;odunc=61
 8 dilim  08;sube-3;odunc=64
 9 dilim  09;sube-1;odunc=67
10 dilim  10;sube-2;odunc=70
--- ham parcali cerceveleme (ilk uc parca) ---
0000000   1   3  \r  \n   0   1   ;   s   u   b   e   -   2   ;   o   d
0000020   u   n   c   =   4   3  \n  \r  \n   1   3  \r  \n   0   2   ;
0000040   s   u   b   e   -   3   ;   o   d   u   n   c   =   4   6  \n
0000060  \r  \n   1   3  \r  \n   0   3   ;   s   u   b
--- uretim yarida kesilirse ---
01;sube-2;odunc=43
02;sube-3;odunc=46
03;sube-1;odunc=49
#hata;rapor yarida kesildi;satir=3
durum=200
```

Üç sonuç okunuyor.

**Başlıklar** biçimi ele veriyor: `/tam` uzunluğu bildiriyor, `/akan` parçalı aktarım
kullanıyor. İkisi bir arada bulunamaz.

**Toplam süre iki biçimde de aynı**: on dilim. Akıtmak işi hızlandırmaz, çünkü rapor yine
aynı sürede üretiliyor. Değişen tek şey **ilk bayta kadar geçen süredir**: on dilimden bir
dilime iniyor. Kullanıcı raporun ilk satırlarını, tamamının bitmesini beklemeden görüyor.
Bu, gerçek zamanlı veri konusunun tamamındaki ortak kazancın burada aldığı biçimdir.

**Parça varışları** ilk sonucu doğruluyor: satırlar birer dilim arayla geliyor, hepsi
sonda toplu olarak değil.

#### Çerçeveleme

Ham çıktı parçalı aktarımın biçimini gösteriyor. Her parça, uzunluğunu **onaltılık**
olarak yazan bir satırla başlıyor: `13` onaltılık, yani on dokuz bayt — bir rapor satırının
uzunluğu. Ardından `\r\n`, sonra verinin kendisi, sonra yine `\r\n` geliyor.

Bu çerçeveleme, uzunluk baştan bilinmediğinde alıcının gövdenin nerede bittiğini nasıl
anlayacağı sorusunu çözer: son parça sıfır uzunlukla yazılır ve akış orada biter.
Uzunluk bildirilmeden gövdeyi bağlantıyı kapatarak bitirmek de teknik olarak mümkündür,
ama o zaman alıcı yanıtın tamamlanmış mı yoksa kesilmiş mi olduğunu ayırt edemez.

#### Başlıklar Gönderildikten Sonra

Son ölçüm bölümü akış yanıtlarının en önemli sınırını gösteriyor. `/kesik` ucunda rapor
üretimi dördüncü satırda başarısız oluyor, ama durum kodu **200** kalıyor. Çünkü başlıklar
ilk parçayla birlikte çoktan gönderildi ve HTTP'de başlıklar geri alınamaz.

Bunun üç sonucu vardır. Hata artık gövdenin **içinde** bildirilmek zorundadır; çıktı
biçiminin bunun için bir yeri olmalıdır — burada `#` ile başlayan bir bildirim satırı.
İkincisi, alıcı tarafın bu satırı okuması gerekir; okumazsa eksik raporu tam sanır.
Üçüncüsü, tamamlandığını gösteren bir **bitiş imi** yazmak, hata imi yazmaktan daha
güvenlidir: bağlantı kopması durumunda hata satırı da yazılamaz, ama bitiş iminin
yokluğu her durumda fark edilir.

HTTP bunun için ardıl başlıklar (trailer) da tanımlar; gövdenin ardından başlık
gönderilmesine izin verirler. Uygulamada ara katman desteği düzensiz olduğu için gövde
içi im daha yaygın ve daha güvenli bir seçimdir.

#### Akış Yanıtında Geri Basınç

Koddaki `yanit.write(...)` dönüşünün sınanması, geri basınç dersinde ölçülen kuralın
buradaki karşılığıdır. Ağ istemcisi yavaşsa yazılan satırlar sunucunun tamponunda birikir;
dönüş değeri yok sayılırsa tampon sınırsız büyür.

Rapor on satırlık olduğunda bu görünmez. Otuz bin satırlık bir raporu yavaş bir bağlantıya
akıtan sunucu ise, dönüşü dinlemediğinde tüm raporu bellekte tutmuş olur — yani akıtmanın
tek gerekçesi olan bellek kazancını da kaybeder. Akış yanıtının değeri, üretim hızının
tüketim hızına **bağlanmasındadır**.

#### Ne Zaman Akıtmalı

Akıtmak her yanıt için doğru değildir. Kazanç, üretim süresi uzun ve sonucun ilk parçası
tek başına anlamlıysa gerçekleşir: rapor satırları, arama sonuçları, kütük kayıtları.
Küçük ve tek parça anlamlı olan yanıtlarda — bir ödünç kaydının ayrıntısı — akıtmak yalnız
çerçeveleme gideri ekler.

İki pratik engel vardır. Ara katmanlar yanıtı tamponlayabilir ve akışı tek parça hâline
getirebilir; bu durumda ölçülen ilk bayt süresi kazanç göstermez. Ve sıkıştırma, tampon
dolmadan çıktı üretmediği için akıtmayı geciktirebilir; akış yanıtlarında sıkıştırmanın
parça sınırlarında boşaltılacak biçimde ayarlanması gerekir.

#### Özet

- Uzunluğu bilinmeyen yanıt gövdesi parçalı aktarımla gönderilir; her parça uzunluğunu onaltılık olarak yazar ve sıfır uzunluklu parça akışı bitirir.
- Akıtmak toplam süreyi kısaltmaz; ölçümde iki biçim de on dilim sürdü, ama ilk bayta kadar geçen süre on dilimden bir dilime indi.
- Başlıklar ilk parçayla gönderildiği için durum kodu sonradan değiştirilemez; hata gövde içinde bildirilir ve bitiş imi yazmak hata imi yazmaktan güvenlidir.
- Akış yanıtında yazma dönüşü dinlenmezse gövdenin tamamı sunucu tamponunda birikir ve akıtmanın bellek kazancı ortadan kalkar.
- Akıtmanın kazancı uzun üretim süresi ile ilk parçanın tek başına anlamlı olmasına bağlıdır; ara katman tamponlaması ve sıkıştırma bu kazancı görünmez kılabilir.

#### Kurs Kapanışı

Bu kurs, isteğin yanıtlanma yolunu kısaltmanın ve o yolun dışına taşınan işi güvenilir
kılmanın dört ayrı katmanını kurdu.

**Önbellekleme** hesaplanmış sonucun yeniden kullanılmasını ele aldı: istemci, kenar,
sunucu ve veritabanı katmanları; yanında okuma, içinden yazma ve arkadan yazma
stratejileri; süre, olay ve sürüm tabanlı geçersizleştirme; ad alanı ve kiracı ayrımı olan
anahtar tasarımı; sıcak anahtar ile sürü etkisinin önlenmesi; HTTP doğrulayıcıları ve
önbellek denetim yönergeleri; ve isabet oranının ölçümü. Ortak dersi şuydu: önbellek bir
hızlandırıcı değil, bayatlığın denetimli kabulüdür.

**Mesajlaşma** işin istek yolundan çıkarılmasını kurdu: eşzamansız işlemenin gerekçesi,
üretici–tüketici kuyruğu, yayımla–abone ol, akış ile kuyruk arasındaki fark, en çok bir
kez ile en az bir kez ve tam bir kez teslim semantiği, sıralama ve bölümlemenin maliyeti,
ölü mektup kuyruğu, zehirli mesaj ve geri çekilme. Ortak dersi teslim garantisinin
bedava olmadığıydı: her güvence sıralamadan, hacimden ya da gecikmeden ödün ister.

**Arka Plan İşleri** mesajı işleyen tarafı bir ölçekleme birimi olarak ele aldı: işçi
süreçleri ve gözetim, yineleyen görev tanımı ile çakışmayı önleyen kira kilidi, uzun
süren işte ilerleme bildirimi ve işbirlikçi iptal, üretim hızının tüketim hızını aştığı
yerde geri basınç ve aşırı yükte denetimli reddetmeyi sağlayan yük boşaltma. Ortak dersi
şuydu: hız uyuşmazlığı yok edilemez, yalnız nereye yazılacağı seçilebilir — belleğe,
kayba ya da üreticinin hızına.

**Gerçek Zamanlı Veri** sonucun istemciye ulaşmasını kurdu: kısa ve uzun yoklamanın istek
maliyeti, sunucu gönderimli olaylar ve olay kimliğiyle yeniden bağlanma, kendi el
sıkışmasıyla kurulan WebSocket bağlantısının yaşam döngüsü, çok örnekli kurulumda kanal
dağıtımı ve parça parça üretilen akış yanıtları. Ortak dersi açık bağlantının bir durum
olduğuydu: taşıdığı gecikme kazancının bedeli, o durumun yönetilmesidir.

Kursun boyunca sessizce korunan bir varsayım vardı. Önbellek, kuyruk, işçi ve akış hep
**tek bir uygulamanın** parçaları gibi ele alındı; hepsi aynı dağıtım biriminin içinde,
aynı kod tabanında, aynı ekibin sorumluluğundaydı. Bu varsayım kavramları öğrenmek için
gerekliydi ama gerçek sistemlerde uzun sürmez.

Sonraki kurs, **Servis Mimarileri**, tam olarak bu birimi bölmeyi ele alır: tek parça,
modüler ve dağıtık mimariler arasında ölçüte dayalı seçim yapmayı, servis sınırlarını alan
modeline göre çizmeyi ve servisler arası iletişim ile tutarlılık kalıplarını uygulamayı.
Bu kursta ölçülen kavramların hiçbiri orada geçersizleşmez; hepsi bir sınırın karşısına
geçer ve orada daha pahalı hâle gelir. Teslim garantisi artık bir kuyruğun değil iki
ekibin sözleşmesidir. Tekillik anahtarı, ağ üzerinden gelen tekrarları da karşılamak
zorundadır. Geri basınç bir süreç içinde değil, servisler arasında yayılır. Kanal
dağıtımı, örnekler arasında değil sahiplikleri farklı servisler arasında yapılır. Bu
kursta ölçülen her sayı, orada aynı soruların daha büyük ölçekteki karşılığını okumanın
aracı olur.

