İçeriğe geç
academia.sh

Ders 16 / 34

Etkisizlik Anahtarları

Yeniden denemede yinelenen etkiyi önleyen tekillik anahtarı: anahtarın kapsamı ve gövde özetiyle eşleştirilmesi, aynı anahtarla gelen ikinci isteğin ilk yanıtı yeniden sunması, farklı gövdeyle çakışmanın reddi ve anahtar deposunun yaşam süresi.

İçindekiler

Üçüncü ders POST’un etkisiz olmadığını ölçmüştü: aynı istek iki kez gönderildiğinde iki ödünç kaydı doğdu. O zaman bu, POST’un doğası diye geçildi. Sekizinci ders okuma tarafını kapatınca geriye bu açık kaldı.

Açık, gerçek bir durumdan doğar. İstemci ödünç kaydı oluşturma isteğini gönderir, ağ kopar. Uygulama Mimarisi kursundaki hata sözleşmesinin ağ hatası sınıfı tam olarak bunu anlatıyordu: istek sunucuya ulaşmış da olabilir, ulaşmamış da. Yanıt yolda kaybolduysa kayıt oluşmuştur ama istemci bunu bilmez. İstemci yeniden denerse ikinci kayıt riski vardır; denemezse üye kitabı alamamış olabilir. İki seçenek de kabul edilemez.

Bu ders üçüncü bir yol kurar: isteğe, istemcinin ürettiği bir kimlik eklemek.

Anahtarın İşi

Tekillik anahtarı (idempotency key), istemcinin bir işlem niyetine verdiği benzersiz addır. Anahtar Idempotency-Key başlığıyla taşınır ve üç kuralı vardır.

Anahtar istek başına değil, niyet başına üretilir. Kullanıcı “ödünç ver” düğmesine bastığında bir anahtar üretilir; ağ koptuğu için yapılan üç deneme aynı anahtarı taşır. Kullanıcı ikinci kez ödünç almak isterse yeni bir anahtar üretilir.

Anahtar yalnız başına yeterli değildir. Sunucu, anahtarın hangi işlemle ilişkilendirildiğini de saklamalıdır: hangi adrese, hangi gövdeyle. Aksi hâlde istemcinin yanlışlıkla aynı anahtarı başka bir istekte kullanması, o isteğin sessizce yutulmasına yol açar.

Anahtar yanıtla birlikte saklanır. İkinci istek geldiğinde işlem tekrar yapılmaz; ilk seferde üretilmiş yanıt olduğu gibi geri verilir. İstemci ilk yanıtı hiç görmemiş olsa bile sonucu öğrenir.

// anahtar-sunucusu.mjs — tekillik anahtariyla korunan odunc olusturma
import { createServer } from "node:http";
import { DatabaseSync } from "node:sqlite";
import { createHash } from "node:crypto";

const db = new DatabaseSync("kutuphane.db");
db.exec(`CREATE TABLE IF NOT EXISTS tekillik (
  anahtar   TEXT PRIMARY KEY,
  yol       TEXT NOT NULL,
  ozet      TEXT NOT NULL,
  durum     INTEGER,
  yanit     TEXT,
  olusma    TEXT NOT NULL
)`);

const govdeOku = (istek) => new Promise((coz) => {
  let veri = ""; istek.on("data", (p) => (veri += p));
  istek.on("end", () => coz(veri));
});
const gonder = (yanit, kod, nesne, ek = {}) => {
  yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8", ...ek });
  yanit.end(JSON.stringify(nesne));
};

const sunucu = createServer(async (istek, yanit) => {
  const yol = new URL(istek.url, "http://127.0.0.1").pathname;
  if (istek.method !== "POST" || yol !== "/oduncler") return gonder(yanit, 404, { hata: "yol_yok" });

  const ham = await govdeOku(istek);
  const anahtar = istek.headers["idempotency-key"];
  const ozet = createHash("sha256").update(ham).digest("hex").slice(0, 16);
  const g = JSON.parse(ham);

  // Anahtarsiz istek: her cagri yeni kayit uretir.
  if (!anahtar) return gonder(yanit, ...olustur(g));

  const kayit = db.prepare("SELECT * FROM tekillik WHERE anahtar = ?").get(anahtar);
  if (kayit) {
    // Ayni anahtar, farkli govde: istemci anahtari yeniden kullanmis.
    if (kayit.yol !== yol || kayit.ozet !== ozet)
      return gonder(yanit, 409, { hata: "anahtar_cakismasi", anahtar });
    return gonder(yanit, kayit.durum, JSON.parse(kayit.yanit), { "idempotency-replayed": "true" });
  }

  // Ayirma ve is, arada bekleme olmadan tek blokta yapilir.
  db.prepare("INSERT INTO tekillik (anahtar, yol, ozet, olusma) VALUES (?,?,?,datetime('now'))")
    .run(anahtar, yol, ozet);
  const [durum, govde] = olustur(g);
  db.prepare("UPDATE tekillik SET durum = ?, yanit = ? WHERE anahtar = ?")
    .run(durum, JSON.stringify(govde), anahtar);
  gonder(yanit, durum, govde);
});

function olustur(g) {
  if (!db.prepare("SELECT 1 FROM kitap WHERE isbn = ?").get(g.isbn))
    return [422, { hata: "dogrulama", alan: "isbn" }];
  const s = db.prepare("INSERT INTO odunc (uye, isbn, verilis, iade) VALUES (?,?,?,NULL)")
              .run(g.uye, g.isbn, g.verilis);
  return [201, { id: Number(s.lastInsertRowid), uye: g.uye, isbn: g.isbn, verilis: g.verilis }];
}

sunucu.listen(8483, "127.0.0.1", () => console.log("anahtar sunucusu 127.0.0.1:8483"));

Ölçüm

Aynı gövde iki kez gönderilir; önce anahtarsız, sonra anahtarla. Katalog, yedinci dersteki katalog.sql dosyasıyla kurulur.

# Ayni istek iki kez gonderilir: once anahtarsiz, sonra tekillik anahtariyla.
rm -f kutuphane.db && sqlite3 kutuphane.db < sema.sql && sqlite3 kutuphane.db < katalog.sql
sqlite3 kutuphane.db "DELETE FROM odunc;"
node anahtar-sunucusu.mjs & sunucu=$!
sleep 0.4

GOVDE='{"uye":"U-1001","isbn":"K-02","verilis":"2026-03-10"}'
say() { printf '%-30s odunc satiri: %s\n' "$1" "$(sqlite3 kutuphane.db 'SELECT COUNT(*) FROM odunc;')"; }
yolla() { curl -sS -D - -o /tmp/govde.$$ -X POST -H 'content-type: application/json' "$@" \
            -d "$GOVDE" http://127.0.0.1:8483/oduncler \
          | grep -iE '^(HTTP/|idempotency-replayed)' | tr -d '\r'; cat /tmp/govde.$$; echo; rm -f /tmp/govde.$$; }

echo "--- anahtarsiz, iki kez ---"
yolla; yolla; say "anahtarsiz x2"

sqlite3 kutuphane.db "DELETE FROM odunc; DELETE FROM tekillik;"
echo "--- ayni anahtarla, iki kez ---"
yolla -H 'Idempotency-Key: OD-7f21'
yolla -H 'Idempotency-Key: OD-7f21'
say "anahtarli x2"

echo "--- ayni anahtar, farkli govde ---"
curl -sS -o /dev/null -w 'durum: %{http_code}\n' -X POST -H 'content-type: application/json' \
  -H 'Idempotency-Key: OD-7f21' -d '{"uye":"U-1002","isbn":"K-03","verilis":"2026-03-11"}' \
  http://127.0.0.1:8483/oduncler
say "cakisma sonrasi"

kill $sunucu
anahtar sunucusu 127.0.0.1:8483
--- anahtarsiz, iki kez ---
HTTP/1.1 201 Created
{"id":1,"uye":"U-1001","isbn":"K-02","verilis":"2026-03-10"}
HTTP/1.1 201 Created
{"id":2,"uye":"U-1001","isbn":"K-02","verilis":"2026-03-10"}
anahtarsiz x2                  odunc satiri: 2
--- ayni anahtarla, iki kez ---
HTTP/1.1 201 Created
{"id":1,"uye":"U-1001","isbn":"K-02","verilis":"2026-03-10"}
HTTP/1.1 201 Created
idempotency-replayed: true
{"id":1,"uye":"U-1001","isbn":"K-02","verilis":"2026-03-10"}
anahtarli x2                   odunc satiri: 1
--- ayni anahtar, farkli govde ---
durum: 409
cakisma sonrasi                odunc satiri: 1

Anahtarsız iki istek iki kayıt üretti ve iki farklı kimlik döndürdü. Anahtarlı iki istek tek kayıt üretti ve aynı gövdeyi döndürdü: ikinci yanıtın id alanı da 1. İkinci yanıt ayrıca Idempotency-Replayed başlığını taşıyor; bu başlık zorunlu değildir, ama istemcinin ve kütüklerin durumu ayırt etmesini sağlar.

Son bölüm anahtarın yeniden kullanımını sınıyor. Aynı OD-7f21 anahtarı farklı bir gövdeyle geldi ve istek 409 ile reddedildi; ödünç kaydı sayısı bir kaldı. Gövde özeti saklanmasaydı bu istek sessizce yutulur, istemci ikinci ödüncün oluştuğunu sanırdı. Reddetmek doğru davranıştır: anahtarın yanlış kullanıldığını yalnızca istemci düzeltebilir.

Eş Zamanlı İstekler

İki isteğin arka arkaya gelmesi kolay durumdur. Zor durum, ikisinin aynı anda gelmesidir: istemci zaman aşımına uğrayıp yeniden denerken ilk istek hâlâ işlenmekte olabilir.

# Ayni anahtarli bes istek es zamanli gonderilir; uretilen kayit sayilir.
rm -f kutuphane.db yanit-*.json && sqlite3 kutuphane.db < sema.sql && sqlite3 kutuphane.db < katalog.sql
sqlite3 kutuphane.db "DELETE FROM odunc;"
node anahtar-sunucusu.mjs & sunucu=$!
sleep 0.4

pidler=""
for i in 1 2 3 4 5; do
  curl -sS -o "yanit-$i.json" -X POST -H 'content-type: application/json' \
    -H 'Idempotency-Key: OD-9c04' \
    -d '{"uye":"U-1002","isbn":"K-05","verilis":"2026-03-12"}' \
    http://127.0.0.1:8483/oduncler &
  pidler="$pidler $!"
done
wait $pidler

for f in yanit-*.json; do cat "$f"; echo; done | sort -u > cesitler.txt
echo "donen govde cesidi : $(wc -l < cesitler.txt | tr -d ' ')"
echo "govde              : $(cat cesitler.txt)"
echo "odunc satiri       : $(sqlite3 kutuphane.db 'SELECT COUNT(*) FROM odunc;')"

echo "--- anahtar deposunun yasi ---"
sqlite3 kutuphane.db "
  INSERT INTO tekillik (anahtar, yol, ozet, durum, yanit, olusma)
  VALUES ('OD-eski','/oduncler','0000',201,'{}',datetime('now','-30 day'));
  SELECT anahtar || '  yas: ' || CAST(julianday('now') - julianday(olusma) AS INT) || ' gun'
    FROM tekillik ORDER BY olusma;
  DELETE FROM tekillik WHERE olusma < datetime('now','-7 day');
  SELECT 'temizlik sonrasi kalan: ' || COUNT(*) FROM tekillik;"

kill $sunucu
anahtar sunucusu 127.0.0.1:8483
donen govde cesidi : 1
govde              : {"id":1,"uye":"U-1002","isbn":"K-05","verilis":"2026-03-12"}
odunc satiri       : 1
--- anahtar deposunun yasi ---
OD-eski  yas: 30 gun
OD-9c04  yas: 0 gun
temizlik sonrasi kalan: 1

Beş eş zamanlı istek tek kayıt üretti ve beşi de aynı gövdeyi aldı. Bunu sağlayan şey kodun yapısıdır: anahtarın kayda geçirilmesi ile işin yapılması arasında hiçbir bekleme noktası yok. Araya bir bekleme girerse iki istek de “anahtar yok” görüp ikisi de kayıt oluşturabilir.

Bu düzenin tek süreçli bir sunucuda yeterli olması, çok süreçli bir dağıtımda yeterli olacağı anlamına gelmez. Genel çözüm, anahtarın birincil anahtar olarak yazılmasıdır: ikinci yazma girişimi veritabanı tarafından reddedilir ve o istek bekleyip yeniden okumaya ya da “işlem sürüyor” yanıtına yönlendirilir. Bu tür yarışların işlem düzeyinde nasıl yalıtıldığı İleri SQL kursundaki işlemler konusunun kapsamındadır.

Anahtar Deposunun Yaşam Süresi

Çıktının son bölümü anahtar deposunun yaşlanmasını gösteriyor. Otuz gün önce yazılmış bir anahtar, yedi günlük saklama penceresiyle temizlendi.

Süre kararının iki sınırı vardır. Alt sınırı istemcinin en uzun deneme penceresi belirler: bir istemci yirmi dört saat sonra hâlâ yeniden deniyorsa, anahtar o süre boyunca saklanmalıdır. Uygulama Mimarisi kursundaki Yeniden Deneme ve Geri Çekilme dersinde tanımlanan deneme bütçesi bu pencereyi verir. Üst sınırı depolama maliyeti ve tabloya yazılan yanıt gövdelerinin büyüklüğü belirler.

Pencere dolduktan sonra aynı anahtarla gelen bir istek yeni bir işlem sayılır ve yeni kayıt üretir. Bu, tasarımın kabul edilmiş sınırıdır; süre seçilirken bu bilinerek seçilir.

Son bir sınır da kapsamdır. Anahtar tek başına değil, kimliği doğrulanmış istemciyle birlikte benzersiz olmalıdır: iki farklı istemcinin aynı anahtar dizgisini üretmesi, birinin isteğinin diğerinin yanıtını almasına yol açmamalıdır. Yukarıdaki tabloda anahtar tek başına birincil anahtardır; gerçek bir dağıtımda istemci kimliği de anahtarın parçası olur.

Özet

  • Ağ hatasında isteğin sunucuya ulaşıp ulaşmadığı bilinmez; yeniden denemek yinelenen etki, dememek eksik etki riski taşır.
  • Tekillik anahtarı istek başına değil niyet başına üretilir; aynı işlemin bütün denemeleri aynı anahtarı taşır.
  • Sunucu anahtarı yol ve gövde özetiyle birlikte saklar; ölçümde anahtarsız iki istek iki kayıt, anahtarlı iki istek bir kayıt üretti ve ikinci yanıt ilkinin aynısı oldu.
  • Aynı anahtarın farklı gövdeyle gelmesi 409 ile reddedilir; sessizce yutmak istemciye olmayan bir işlemi olmuş gibi gösterir.
  • Anahtarın kayda geçirilmesi ile işin yapılması arasına bekleme girmemelidir; eş zamanlı beş istek bu yapıyla tek kayıt üretti.
  • Anahtar deposunun saklama süresi istemcinin en uzun deneme penceresinden kısa olamaz; pencere dolduktan sonra aynı anahtar yeni bir işlem sayılır.

Sonraki Adım

Dokuz ders boyunca istemcinin her adresi kendisi kurduğu varsayıldı: koleksiyon adresini, sayfa imlecini, alan listesini, hatta yeni oluşturulan kaydın adresini. İstemci bunu yapabilmek için sözleşmenin biçimini kendi içine gömer — ve sözleşme değiştiğinde her istemci ayrı ayrı güncellenir. Sonraki ders bu bağımlılığı gevşetir: yanıt, istemcinin bir sonraki isteğinde kullanacağı adresleri kendi içinde taşır. Ödünç kaydının hangi işlemlere açık olduğu da aynı yerden okunur; iade edilmiş bir kayıt artık iade bağlantısı sunmaz.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat