Ders 16 / 19
Zarif Bozulma
Zarif bozulmanın koda yerleştirilmesi: çekirdek ve isteğe bağlı alan ayrımının şema nesnesinde toplanması, çağrı yerinde try/catch ile paylaşılan sarmalayıcının 7 ve 12 satır olarak karşılaştırılması, 14 çağrı yerinin 6'sının sarmalanıp 2'sinin atlanması ve yedek değerin gövdeyi gerçek sıfırdan ayırt edilemez kılması.
İçindekiler
Bölme kalıbı bozulan bağımlılığı kendi havuzuna kapattı: havuz dolduğunda çağrı hızla başarısız oluyor ve öteki bağımlılıkların havuzları etkilenmiyor. Kurtarılan şey aşağı akıştır. Yukarı akışta hiçbir şey değişmedi — çağrı yeri hâlâ bir hata nesnesi alıyor, uç hâlâ o hatayı yukarı fırlatıyor ve istemci hâlâ boş dönüyor. Panel yanıtının beş alanından dördü elde olsa bile, beşincisi gelmediği için beşi birden atılıyor.
Zarif bozulma bu boşluğu kapatır. Etkisi Dayanıklılık ve Güvenilirlik kursunda ölçüldü: hangi politikanın kaç isteği kurtardığı, arızasız günün bedeli ve çekirdek bir bağımlılık düştüğünde kalıbın elinden ne geldiği orada sayıldı. Burada ölçülen başka bir şey: bu kalıp koda nereye yerleşiyor, kaç çağrı yerini kapsıyor ve hangi çağrı yerlerini atlıyor.
Şema Nesnesi ve Çağrı Yerleri
Düzenek bölünmüş ödünç sisteminin beş servisidir. Her biri yerel bir node:http sunucusudur ve
bayrağı kapatıldığında bağlantıyı reddeder — çağıran taraf için bu, üzerinde denetimi olmayan bir
hata nesnesi demektir.
// bozulma/servisler.mjs — bolunmus odunc sisteminin yukari akis servisleri. // Her biri yerel bir node:http surecidir; CALISIYOR bayragi kapatilinca baglanti reddedilir. import http from "node:http"; export const PORT = {}; export const CALISIYOR = { uyelik: 1, odunc: 1, ucretlendirme: 1, bildirim: 1, katalog: 1 }; export const GOVDE = { uyelik: { ad: "N.A.", durum: "etkin" }, odunc: { acik: 3, gecikmis: 1 }, ucretlendirme: { borc: 12.5 }, bildirim: { okunmamis: 4 }, katalog: { oneri: ["K-1180"] }, }; const AYAK = []; export async function baslat() { for (const ad of Object.keys(GOVDE)) { const s = http.createServer((req, res) => { if (CALISIYOR[ad] === 0) { res.socket.destroy(); return; } res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify(GOVDE[ad])); }); await new Promise((r) => s.listen(0, "127.0.0.1", r)); PORT[ad] = s.address().port; AYAK.push(s); } } export function durdur() { for (const s of AYAK) s.close(); } // Yalin cagri yeri: servis dusunce hata firlatir. Sarmalanmamis her cagri boyle davranir. export async function cagir(ad) { const y = await fetch(`http://127.0.0.1:${PORT[ad]}/`); if (y.ok === false) throw new Error(`${ad}: HTTP ${y.status}`); return y.json(); }
DY10 — panelin alan ayrımı. Üye panelinin beş alanı vardır. uye ile odunc çekirdektir: bu
ikisi olmadan sayfa bir şey göstermez. borc, bildirim ve oneri isteğe bağlıdır ve her birinin
yerine konacak bir yedek değeri vardır. DY11 — makbuz ve özet uçları panelden önce yazılmıştır ve
kendi çağrılarını yaparlar; bu, bozulma kalıbı sisteme sonradan girdiğinde tipik olan durumdur.
Ayrım kodun içine dağılmaz, tek bir şema nesnesinde durur. İki yerleşim aynı şemayı okur.
// bozulma/panel.mjs — panel yanitinin semasi ve kalibin iki yerlesimi. import { cagir } from "./servisler.mjs"; // DY10: yanitin cekirdek/istege bagli ayrimi sema nesnesinde durur, kodun icine dagilmaz. export const SEMA = { uye: { servis: "uyelik", cekirdek: 1 }, odunc: { servis: "odunc", cekirdek: 1 }, borc: { servis: "ucretlendirme", cekirdek: 0, yedek: { borc: 0 } }, bildirim: { servis: "bildirim", cekirdek: 0, yedek: { okunmamis: 0 } }, oneri: { servis: "katalog", cekirdek: 0, yedek: { oneri: [] } }, }; // Yerlesim A — cagri yerinde: her istege bagli cagri kendi try/catch'ini ve kendi yedegini tasir. export async function panelA() { const y = { uye: await cagir("uyelik"), odunc: await cagir("odunc") }; try { y.borc = await cagir("ucretlendirme"); } catch { y.borc = { borc: 0 }; } try { y.bildirim = await cagir("bildirim"); } catch { y.bildirim = { okunmamis: 0 }; } try { y.oneri = await cagir("katalog"); } catch { y.oneri = { oneri: [] }; } return y; } // Yerlesim B — paylasilan sarmalayici: yedek semadan okunur, dusen alan yanitta isaretlenir. export async function istegeBagli(alan, eksik) { const t = SEMA[alan]; try { return await cagir(t.servis); } catch { eksik.push(alan); return t.yedek; } } export async function panelB() { const eksik = []; const y = { uye: await cagir("uyelik"), odunc: await cagir("odunc") }; y.borc = await istegeBagli("borc", eksik); y.bildirim = await istegeBagli("bildirim", eksik); y.oneri = await istegeBagli("oneri", eksik); return { ...y, eksik }; } // Ayni servislere dokunan oteki iki uc. Ikisi de panelden once yazildi. export async function makbuz() { const u = await cagir("uyelik"); const b = await cagir("ucretlendirme"); return { uye: u.ad, borc: b.borc }; } export async function ozet() { const o = await cagir("odunc"); const b = await cagir("bildirim"); return { acik: o.acik, okunmamis: b.okunmamis }; }
Kaynak Taraması ve Koşum
Ölçüm iki bölümdür. Önce kaynak metni taranır ve her çağrı yeri sınıflandırılır: çekirdek bir servise mi gidiyor, isteğe bağlı bir servise gidip sarmalanmış mı, yoksa isteğe bağlı bir servise gidip sarmalanmamış mı. Üçüncüsü atlanan çağrı yeridir. Sonra aynı kod arızalı senaryolarda koşturulur ve tarama ile koşum karşılaştırılır.
// bozulma/olcum.mjs — once kaynak sayilir, sonra ayni kod arizali senaryolarda kosturulur. import { readFileSync } from "node:fs"; import { baslat, durdur, CALISIYOR, GOVDE } from "./servisler.mjs"; import { SEMA, panelA, panelB, makbuz, ozet } from "./panel.mjs"; const ISTEGE_BAGLI = new Set(Object.values(SEMA).filter((t) => !t.cekirdek).map((t) => t.servis)); const yer = [], satir = {}; // her cagri yeri: hangi islev, hangi servis, sarmali mi let simdi = null; for (const s of readFileSync(new URL("./panel.mjs", import.meta.url), "utf8").split("\n")) { const bas = s.match(/^export async function (\w+)/); if (bas) simdi = bas[1]; if (simdi === null) continue; if (s.trim() !== "") satir[simdi] = (satir[simdi] ?? 0) + 1; for (const m of s.matchAll(/cagir\("(\w+)"\)/g)) // adi yazili yalin cagri yeri yer.push({ islev: simdi, servis: m[1], sarmali: s.includes("try {") }); for (const m of s.matchAll(/istegeBagli\("(\w+)"/g)) // sarmalayiciya verilmis cagri yeri yer.push({ islev: simdi, servis: SEMA[m[1]].servis, sarmali: true }); if (s.startsWith("}")) simdi = null; } const sinif = (y) => (ISTEGE_BAGLI.has(y.servis) ? (y.sarmali ? "sarmalanan" : "atlanan") : "cekirdek"); console.log("-- kaynak taramasi --"); console.log(`${"islev".padEnd(13)}${"satir".padStart(6)}${"cagri yeri".padStart(11)}${"cekirdek".padStart(9)}${"sarmalanan".padStart(11)}${"atlanan".padStart(8)}`); for (const i of [...new Set(yer.map((y) => y.islev))]) { const t = yer.filter((y) => y.islev === i); const n = (k) => String(t.filter((y) => sinif(y) === k).length); console.log(`${i.padEnd(13)}${String(satir[i]).padStart(6)}${String(t.length).padStart(11)}${n("cekirdek").padStart(9)}${n("sarmalanan").padStart(11)}${n("atlanan").padStart(8)}`); } console.log(`toplam ${yer.length} cagri yeri; ${yer.filter((y) => sinif(y) === "sarmalanan").length} sarmalandi, ${yer.filter((y) => sinif(y) === "atlanan").length} atlandi`); console.log(`yerlesim A ${satir.panelA} satir, yerlesim B ${satir.panelB}+${satir.istegeBagli}=${satir.panelB + satir.istegeBagli} satir\n`); await baslat(); const UC = { panelA, panelB, makbuz, ozet }; const SENARYO = [["hepsi saglam", []], ["ucretlendirme durdu", ["ucretlendirme"]], ["ucretlendirme+bildirim+katalog", ["ucretlendirme", "bildirim", "katalog"]], ["uyelik durdu", ["uyelik"]]]; console.log("-- kosum: dusen servise giden cagri yerlerinin akibeti --"); console.log(`${"senaryo".padEnd(31)}${"dokunulan".padStart(10)}${"yedek donen".padStart(12)}${"hata firlatan".padStart(14)}${"yanitlayan uc".padStart(14)}`); const govde = {}; for (const [ad, dusen] of SENARYO) { for (const s of Object.keys(CALISIYOR)) CALISIYOR[s] = dusen.includes(s) ? 0 : 1; let yanit = 0; for (const [uc, f] of Object.entries(UC)) { try { govde[`${ad}|${uc}`] = JSON.stringify(await f()); yanit += 1; } catch { /* uc dustu */ } } const d = yer.filter((y) => dusen.includes(y.servis)); const yedek = d.filter((y) => sinif(y) === "sarmalanan").length; console.log(`${ad.padEnd(31)}${String(d.length).padStart(10)}${String(yedek).padStart(12)}${String(d.length - yedek).padStart(14)}${`${yanit}/4`.padStart(14)}`); } for (const s of Object.keys(CALISIYOR)) CALISIYOR[s] = 1; GOVDE.ucretlendirme.borc = 0; // borcu gercekten olmayan bir uye const sifirA = JSON.stringify(await panelA()), sifirB = JSON.stringify(await panelB()); const arizaliA = govde["ucretlendirme durdu|panelA"], arizaliB = govde["ucretlendirme durdu|panelB"]; console.log("\n-- yedegin sessiz yanlisi: servis dustu / borc gercekten 0 --"); console.log(`A dustu: ${arizaliA}`); console.log(`A sifir: ${sifirA}`); console.log(`ayni govde mi: ${arizaliA === sifirA}`); console.log(`B dustu: ${arizaliB.slice(arizaliB.indexOf('"borc"'))}`); console.log(`B sifir: ${sifirB.slice(sifirB.indexOf('"borc"'))}`); console.log(`ayni govde mi: ${arizaliB === sifirB}; isaretin bedeli ${arizaliB.length - arizaliA.length} bayt, yanitin %${(((arizaliB.length - arizaliA.length) / arizaliA.length) * 100).toFixed(1)}'i`); durdur();
-- kaynak taramasi --
islev satir cagri yeri cekirdek sarmalanan atlanan
panelA 7 5 2 3 0
panelB 8 5 2 3 0
makbuz 5 2 1 0 1
ozet 5 2 1 0 1
toplam 14 cagri yeri; 6 sarmalandi, 2 atlandi
yerlesim A 7 satir, yerlesim B 8+4=12 satir
-- kosum: dusen servise giden cagri yerlerinin akibeti --
senaryo dokunulan yedek donen hata firlatan yanitlayan uc
hepsi saglam 0 0 0 4/4
ucretlendirme durdu 3 2 1 3/4
ucretlendirme+bildirim+katalog 8 6 2 2/4
uyelik durdu 3 0 3 1/4
-- yedegin sessiz yanlisi: servis dustu / borc gercekten 0 --
A dustu: {"uye":{"ad":"N.A.","durum":"etkin"},"odunc":{"acik":3,"gecikmis":1},"borc":{"borc":0},"bildirim":{"okunmamis":4},"oneri":{"oneri":["K-1180"]}}
A sifir: {"uye":{"ad":"N.A.","durum":"etkin"},"odunc":{"acik":3,"gecikmis":1},"borc":{"borc":0},"bildirim":{"okunmamis":4},"oneri":{"oneri":["K-1180"]}}
ayni govde mi: true
B dustu: "borc":{"borc":0},"bildirim":{"okunmamis":4},"oneri":{"oneri":["K-1180"]},"eksik":["borc"]}
B sifir: "borc":{"borc":0},"bildirim":{"okunmamis":4},"oneri":{"oneri":["K-1180"]},"eksik":[]}
ayni govde mi: false; isaretin bedeli 17 bayt, yanitin %11.9'i
İki Yerleşim ve Atlanan Çağrı Yeri
İki yerleşim aynı işi görüyor ve panel ucunda aynı sonucu veriyor: her ikisi de üç isteğe bağlı çağrıyı sarmalıyor, ikisi de ücretlendirme düştüğünde yanıt üretiyor. Satır bedeli farklı — çağrı yerindeki yerleşim 7 satır, paylaşılan sarmalayıcılı yerleşim 8 artı 4, toplam 12 satır. İlk bakışta çağrı yerindeki yerleşim ucuz görünüyor.
Farkı yaratan şey satır sayısı değil, yedeğin nerede yazıldığı. Çağrı yerindeki yerleşimde
{ borc: 0 } ifadesi catch bloğunun içine gömülüdür; şemadaki yedek alanı ile aralarında hiçbir
bağ yoktur. İkisi ayrı ayrı değiştirilebilir ve sessizce ayrışabilirler. Paylaşılan sarmalayıcıda
yedek tek yerden, şemadan okunur; bir alanın çekirdek olup olmadığına dair karar da orada durur.
Asıl ölçü ikinci tablodadır. Ücretlendirme servisi düştüğünde o servise giden üç çağrı yeri var: paneldeki iki yerleşim ve makbuz ucundaki çağrı. İkisi yedek değer döndürüyor, biri hâlâ hata fırlatıyor — makbuz ucu yanıt veremiyor, dört uçtan üçü ayakta. Üç servis birden düştüğünde dokunulan çağrı yeri sekize çıkıyor, altısı yedek döndürüyor, ikisi fırlatıyor ve özet ucu da düşüyor.
Bozulma kalıbı panele yerleşti, sisteme yerleşmedi. Şema nesnesi borc alanını “isteğe bağlı” ilan
ediyor, ama bu ilan yalnızca o alanı okuyan kod yolunu bağlıyor. Makbuz ucu aynı servisi çağırıyor,
aynı hatayı alıyor ve o hatayı yukarı fırlatmayı sürdürüyor. Kalıbın kapsamı, kalıbın kurulduğu
yer değil, kalıbın çağrıldığı yerlerin kümesidir. Kaynak taraması bu kümeyi altı sarmalanan ve iki
atlanan çağrı yeri olarak sayıyor; iki atlanan çağrı yeri koşumda iki düşen uca dönüşüyor.
Son satır ayrımın bir başka yüzünü gösteriyor. Üyelik servisi düştüğünde ona giden üç çağrı yerinin üçü de çekirdek sınıfındadır ve hiçbiri sarmalanmamıştır; bu bir eksiklik değil, tasarımın kendisidir. Yine de özet ucu ayakta kalıyor, çünkü üyelik onun için bir bağımlılık değil. Çekirdeklik alanın değil, ucun özelliğidir: aynı servis bir uçta çekirdek, başka bir uçta hiç yoktur.
Yedeğin Sessiz Yanlışı
Son bölüm kalıbın kendi ürettiği kusuru ölçüyor. Ücretlendirme servisi düşükken çağrı yerindeki
yerleşimin ürettiği gövde ile borcu gerçekten sıfır olan bir üyenin sağlam gündeki gövdesi
karşılaştırılıyor: bayt bayt aynı. borc: 0 değeri iki anlama geliyor ve istemcinin bu ikisini
ayırt etmesinin hiçbir yolu yok. Üye ekranında “borcunuz yok” yazıyor, gerçekte on iki buçuk liralık
bir borç var ve kimse bir hata görmüyor.
Bu, bozulma kalıbının hata fırlatan koddan daha kötü olabildiği tek noktadır. Fırlatılan hata
görünürdür: uç düşer, kayda geçer, sayaca yansır. Yedek değer görünmez, çünkü başarılı bir yanıtın
içinde durur. Sarmalayıcılı yerleşim bunu düzeltiyor: eksik listesi hangi alanın yedekle
doldurulduğunu yanıtın kendisinde taşıyor ve iki gövde artık ayrışıyor. Bedeli on yedi bayt, bu
yanıtın yüzde 11,9’u.
Bu on yedi bayt yalnızca bir ek alan değil, kalıbın gözlemlenebilir hâlidir. İşareti taşımayan bir bozulma, bu kursun ilk iki konusundaki bütün araçlar için görünmezdir: günlük kaydı üretilmez, sayaç artmaz, gösterge kıpırdamaz. Yedek değerin bir işareti yoksa bozulma ölçülemez; ölçülemeyen bozulma da bir süre sonra normal sanılır.
Özet
- Zarif bozulma koda bir şema nesnesi olarak yerleşir: hangi alanın çekirdek olduğu, hangisinin isteğe bağlı olduğu ve yedeğinin ne olduğu tek yerde durur. Kalıbın etkisi Dayanıklılık ve Güvenilirlik kursunda ölçüldü; buradaki ölçü yerleşimin kendisidir.
- İki yerleşim karşılaştırıldı: çağrı yerinde
try/catch7 satır, paylaşılan sarmalayıcı 12 satır. Satır farkının karşılığı, yedek değerincatchbloğuna gömülmek yerine şemadan okunmasıdır. - Kaynak taraması 14 çağrı yeri buldu: 6’sı sarmalandı, 2’si atlandı, kalanı çekirdek çağrılardır. Atlanan iki çağrı yeri, panelden önce yazılmış makbuz ve özet uçlarındadır.
- Koşum taramayı doğruladı: ücretlendirme düştüğünde 3 çağrı yerinin 2’si yedek döndürdü, 1’i hata fırlattı ve dört uçtan biri düştü. Üç servis birden düştüğünde 8 çağrı yerinin 6’sı yedek döndürdü ve iki uç düştü.
- İşaretsiz yedek sessizce yanlış veri üretir: servis düşükken üretilen gövde ile borcu gerçekten
sıfır olan üyenin gövdesi bayt bayt aynıdır.
eksiklistesi bu ikisini ayırır ve yanıta 17 bayt, yüzde 11,9 ekler. - Çekirdeklik alanın değil ucun özelliğidir: üyelik servisi panel ve makbuz için çekirdek, özet ucu için hiç yoktur.
Sonraki Adım
Buraya kadarki beş kalıp da aynı yöne bakıyor: aşağı akıştaki bir bağımlılık bozulduğunda bu sistemin ne yapacağı. Zaman aşımı beklemeyi kesiyor, yeniden deneme yeniden soruyor, devre kesici sormayı bırakıyor, bölme kaynakları ayırıyor, bozulma yanıtı küçültüyor. Hepsinin öznesi aynı: çağıran taraf.
Aynı sistemin öteki yüzü hiç ele alınmadı. Bu servisler yalnız çağırmıyor, çağrılıyor da; ve çağıran tarafın kaç istek göndereceğine dair hiçbir sınır yok. Bir istemcinin yanlış yazılmış bir döngüsü, tek bir uca dakikada binlerce istek gönderebilir ve buradaki hiçbir kalıp bunu durduramaz — hepsi zaten gelmiş bir isteğin başına ne geleceğini düzenler. Sıradaki ders sınırı kabul tarafına koyar ve şunu ölçer: sınırlayıcı kodun hangi katmanında durur, sınır kime uygulanır, hangi yollar sınırın dışında kalır ve anahtar yanlış seçildiğinde ne olur.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.