Ders 12 / 19
Zaman Aşımları
Her dış çağrının süre sınırı: sınırın çağrı yerine mi istemci sarmalayıcısına mı konduğunun kaynakta ölçülmesi, sarmalanan ve atlanan çağrı yerinin sayılması, iç sınır dış sınırdan büyük olduğunda üretilen öksüz tamamlanma ve iki yerleşimin sınanabilirlik farkı.
İçindekiler
Bir önceki konu göstergeleri kurdu ve şunu bıraktı: bir sınırın aşıldığı artık görülüyor, ama sistem o sırada hâlâ aynı davranıyor — ölçmek bir şeyi değiştirmiyor. Değiştiren şey koda konan bir sınırdır ve bu dersin işi o sınırın nereye konduğunu ölçmektir.
Zaman aşımının etkisi M19/K05 Dayanıklılık ve Güvenilirlik kursunda ölçüldü ve burada tekrarlanmaz. Burada ölçülen şey başkadır: aynı sınır iki ayrı katmana konduğunda kaynakta kaç satır ve kaç yapılandırma değeri oluşur, kaç çağrı yeri kapsanır ve kaç çağrı yeri atlanır.
DY1 — ölçü birimi çağrı yeridir: kaynakta dış çağrı kuran her ifade bir çağrı yeridir. Ödünç servisi dört bağımlılığa yedi çağrı yerinden gidiyor.
Düzenek
Bağımlılıklar yerel süreçlerdir: katalog ve üyelik hızlı yanıtlar, ücretlendirme 500 ms sürer, bildirim ise bağlantıyı açık tutar ve yanıtı hiç yazmaz. Ödünç servisinin kendisi de ayaktadır; iç içe sınırlar bölümünde orta katman odur.
// servisler.mjs — bolunmus odunc sisteminin bagimliliklari ve odunc servisinin kendisi. // DY2: bildirim baglantiyi acik tutar, yanit hic yazmaz. 8103/sayac tum sayaclari verir. import { createServer, request } from "node:http"; const s = { acik: { bildirim: 0 }, toplam: { bildirim: 0 }, oksuz: 0 }; // oksuz: cagiran vazgectikten sonra biten is for (const [port, ad, gecikme] of [[8101, "katalog", 5], [8102, "uyelik", 5], [8104, "ucretlendirme", 500]]) { s.acik[ad] = 0; s.toplam[ad] = 0; createServer((i, y) => { s.acik[ad] += 1; s.toplam[ad] += 1; setTimeout(() => { s.acik[ad] -= 1; y.end("{}"); }, gecikme); }).listen(port); } createServer((i, y) => { // 8103 bildirim + sayac ucu if (i.url === "/sayac") return y.end(JSON.stringify(s)); if (i.url === "/sifirla") { s.oksuz = 0; for (const k in s.toplam) s.toplam[k] = 0; return y.end("{}"); } s.acik.bildirim += 1; s.toplam.bildirim += 1; i.on("close", () => { s.acik.bildirim -= 1; }); // yanit yazilmaz: cagri asili kalir }).listen(8103); createServer((i, y) => { // 8100 odunc: kendi ic sinirini uygular let vazgecti = false; i.on("close", () => { vazgecti = !y.writableEnded; }); const r = request({ port: 8104, path: "/ucret", agent: false }, (c) => { c.resume(); c.on("end", () => { if (vazgecti) s.oksuz += 1; else y.end('{"ucret":1}'); }); }); r.setTimeout(Number(new URL(i.url, "http://x").searchParams.get("ic")), () => r.destroy(new Error("ic"))); r.on("error", () => { if (!vazgecti) y.end('{"hata":"ic"}'); }); r.end(); }).listen(8100);
Çağıran taraf üç dizinde üç kez yazılır. yok/ sınırsız hâldir; a/ sınırı çağrı yerine koyar,
her isteğin kendi setTimeout satırı ve kendi sayı değeri vardır; b/ sınırı istemci
sarmalayıcısına koyar. Üçü de aynı yedi çağrı yerinden üretilir, böylece fark yalnız yerleşimden
gelir.
// kur.mjs — ayni yedi cagri yerini uc yerlesimde uretir: sinirsiz, cagri yerinde, sarmalayicida. import { mkdirSync, writeFileSync } from "node:fs"; const ATLANAN = "bildirimYinele"; // DY4: sarmalayiciya tasinmasi unutulan cagri yeri const C = [["odunc", "kitapAra", 8101, "/ara", 200], ["odunc", "kitapDetay", 8101, "/kitap", 200], ["odunc", "uyeDogrula", 8102, "/uye", 200], ["odunc", "uyeCezasi", 8102, "/ceza", 200], ["odunc", "gecikmeUcreti", 8104, "/ucret", 800], ["bildirim", "bildirimGonder", 8103, "/gonder", 800], ["bildirim", ATLANAN, 8103, "/yinele", 800]]; const ham = ([, ad, port, yol, sinir], koy) => `export function ${ad}() { return new Promise((coz, at) => { const r = request({ port: ${port}, path: "${yol}" }, (y) => { y.resume(); y.on("end", coz); });${ koy ? `\n r.setTimeout(${sinir}, () => r.destroy(new Error("zaman asimi")));` : ""} r.on("error", at); r.end(); }); }`; const sarili = ([, ad, port, yol]) => `export const ${ad} = () => istek(${port}, "${yol}", "${yol.slice(1)}");`; for (const [d, koy, sarmala] of [["yok", false, false], ["a", true, false], ["b", false, true]]) { mkdirSync(d, { recursive: true }); for (const m of ["odunc", "bildirim"]) { const g = C.filter((x) => x[0] === m); const s = sarmala ? g.filter((x) => x[1] !== ATLANAN) : [], h = g.filter((x) => !s.includes(x)); writeFileSync(`${d}/${m}.mjs`, `// ${d}/${m}.mjs\n` + (h.length ? 'import { request } from "node:http";\n' : "") + (s.length ? 'import { istek } from "../istek.mjs";\n' : "") + [...s.map(sarili), ...h.map((x) => ham(x, koy))].join("\n") + "\n"); } }
ATLANAN bu dersin çekirdeğidir: bildirim modülündeki yineleme yolu sarmalayıcıya taşınmadı.
Sınır bir yere konduğunda, oraya uğramayan çağrı yeri kaynakta hiçbir eksiklik göstermez.
// istek.mjs — istemci sarmalayicisi: her dis cagri buradan gecer, sure siniri burada uygulanir. import { request } from "node:http"; export const VARSAYILAN = { sure: 800 }; // DY3: tek varsayilan, cagri yerinde gecilebilir export function istek(port, yol, ad, ayar = {}) { const sure = ayar.sure ?? VARSAYILAN.sure; return new Promise((coz, at) => { const r = request({ port, path: yol }, (y) => { y.resume(); y.on("end", () => coz(ad)); }); r.setTimeout(sure, () => r.destroy(new Error(`zaman asimi ${sure} ms`))); r.on("error", at); r.end(); }); }
Yerleşimin Kaynakta ve Koşumdaki Bedeli
Ölçüm betiği üç dizini tarar, üç sürümü canlı servislere karşı koşturup bağımlılıkta kaç isteğin açık kaldığını bağımlılığın kendi sayacından okur, sonra dış ve iç sınırı dört çiftle dener.
// olc.mjs — kapsanan/atlanan cagri yeri, asili cagri ve ic ice sinirlarin celismesi. import { readdirSync, readFileSync } from "node:fs"; import { request } from "node:http"; const TUR = 4, PENCERE = 1500, N = 10, BAGIMLILIK = 500; // ucretlendirmenin sabit gecikmesi 500 ms const V = { yok: "sinir yok", a: "cagri yerinde", b: "istemci sarmalayicisinda" }; const say = (m, d) => (m.match(d) || []).length; const yaz = (g, a, ...r) => console.log(String(a).padEnd(26) + r.map((x) => String(x).padStart(g)).join("")); const bekle = (ms) => new Promise((c) => setTimeout(c, ms)); const sayac = (yol = "/sayac") => new Promise((c) => request({ port: 8103, path: yol }, (r) => { let g = ""; r.on("data", (p) => (g += p)); r.on("end", () => c(g === "{}" ? null : JSON.parse(g))); }).end()); const tara = (d) => readdirSync(d).map((a) => readFileSync(`${d}/${a}`, "utf8")) // [dosya, satir, ham, sarmali, sinir] .reduce((a, m) => [a[0] + 1, a[1] + m.split("\n").filter((x) => x.trim()).length, a[2] + say(m, /\brequest\(\{/g), a[3] + say(m, /\bistek\(/g), a[4] + say(m, /\.setTimeout\(/g)], [0, 0, 0, 0, 0]); async function kosum(d) { // yedi cagri yerini canli servislere karsi kosar const m = { ...(await import(`./${d}/odunc.mjs`)), ...(await import(`./${d}/bildirim.mjs`)) }; const c = { biten: 0, hata: 0 }, ad = Object.keys(m), once = (await sayac()).acik.bildirim; for (let t = 0; t < TUR; t += 1) for (const a of ad) m[a]().then(() => { c.biten += 1; }, () => { c.hata += 1; }); await bekle(PENCERE); return { n: ad.length * TUR, ...c, acik: (await sayac()).acik.bildirim - once }; } const gecit = (dis, ic) => new Promise((coz) => { // ag gecidi -> odunc:8100 -> ucretlendirme:8104 const t0 = process.hrtime.bigint(); const bit = (s) => coz({ s, ms: Number(process.hrtime.bigint() - t0) / 1e6 }); const r = request({ port: 8100, path: `/ucret?ic=${ic}`, agent: false }, (y) => { let g = ""; y.on("data", (p) => (g += p)); y.on("end", () => bit(g.includes("hata") ? "ic hata" : "yanit")); }); r.setTimeout(dis, () => r.destroy(new Error("dis"))); r.on("error", () => bit("dis zaman asimi")); r.end(); }); const sar = readFileSync("istek.mjs", "utf8").split("\n").filter((x) => x.trim()).length; yaz(13, "yerlesim", "dosya", "satir", "cagri yeri", "sarmalanan", "atlanan", "sinir satiri"); for (const [d, ad] of Object.entries(V)) { const [dos, sat, ham, srm, snr] = tara(d), b = d === "b"; yaz(13, ad, dos + b, sat + (b ? sar : 0), ham + srm, d === "a" ? snr : srm, d === "a" ? 0 : ham, snr + b); } const sayi = ["a/odunc.mjs", "a/bildirim.mjs"].flatMap((f) => readFileSync(f, "utf8").match(/setTimeout\((\d+)/g)); console.log(`yapilandirma yuzeyi: cagri yerinde ${sayi.length} sayi, ${new Set(sayi).size} ayri deger ` + `(${[...new Set(sayi)].map((x) => x.slice(11)).join(" / ")} ms); sarmalayicida 1 varsayilan, 0 gecersiz kilma`); console.log(`\n${TUR} tur x 7 cagri yeri; gozlem penceresi ${PENCERE} ms`); yaz(13, "yerlesim", "cagri", "yanit", "zaman asimi", "asili kalan", "acik istek"); for (const [d, ad] of Object.entries(V)) { const r = await kosum(d); yaz(13, ad, r.n, r.biten, r.hata, r.n - r.biten - r.hata, r.acik); } console.log(`\nic ice sinirlar: bagimliligin sabit gecikmesi ${BAGIMLILIK} ms, her cift icin ${N} istek;` + `\n olculen ms bu kosumun ortalamasidir, 100'e yuvarlandi`); yaz(18, "dis sinir / ic sinir", "sonuc", "olculen ms", "bagimliliga cagri", "oksuz tamamlanma"); for (const [dis, ic] of [[300, 800], [800, 300], [800, 800], [800, 100]]) { await sayac("/sifirla"); const r = await Promise.all(Array.from({ length: N }, () => gecit(dis, ic))); await bekle(BAGIMLILIK + 300); // bagimliligin isini bitirmesi beklenir const s = await sayac(); yaz(18, `${dis} / ${ic}`, [...new Set(r.map((x) => x.s))].join("+"), Math.round(r.reduce((a, x) => a + x.ms, 0) / N / 100) * 100, s.toplam.ucretlendirme, s.oksuz); } process.exit(0);
node servisler.mjs > /dev/null 2>&1 & SP=$! sleep 1; node kur.mjs && node olc.mjs; kill $SP
yerlesim dosya satir cagri yeri sarmalanan atlanan sinir satiri sinir yok 2 46 7 0 7 0 cagri yerinde 2 53 7 7 0 7 istemci sarmalayicisinda 3 28 7 6 1 1 yapilandirma yuzeyi: cagri yerinde 7 sayi, 2 ayri deger (200 / 800 ms); sarmalayicida 1 varsayilan, 0 gecersiz kilma 4 tur x 7 cagri yeri; gozlem penceresi 1500 ms yerlesim cagri yanit zaman asimi asili kalan acik istek sinir yok 28 20 0 8 8 cagri yerinde 28 20 8 0 0 istemci sarmalayicisinda 28 20 4 4 4 ic ice sinirlar: bagimliligin sabit gecikmesi 500 ms, her cift icin 10 istek; olculen ms bu kosumun ortalamasidir, 100'e yuvarlandi dis sinir / ic sinir sonuc olculen ms bagimliliga cagri oksuz tamamlanma 300 / 800 dis zaman asimi 300 10 10 800 / 300 ic hata 300 10 0 800 / 800 yanit 500 10 0 800 / 100 ic hata 100 10 0
Sınırsız satır, istemcinin varsayılan bir zaman aşımı olmadığını söylüyor: 28 çağrının 20’si yanıt aldı, 8’i ne yanıt ne hata aldı, pencere kapandığında bağımlılıkta hâlâ 8 istek açıktı. Bu 8 koşum süresinden bağımsızdır — pencere 15 dakika olsaydı da 8 olurdu, çünkü o çağrıları bitirecek bir mekanizma yok.
Çağrı yerine konan sınır kaynağı 46 satırdan 53 satıra çıkarıyor — çağrı yeri başına tam bir satır — ve yapılandırma yüzeyini yedi ayrı sayıya dağıtıyor; bu yedi sayının yalnız iki ayrı değeri var, yani beşi kopya. Buna karşılık kapsam tamdır: 7 sarmalanan, 0 atlanan çağrı yeri, 0 asılı çağrı. Sınır çağrı yerindeyken hiçbir çağrı yeri unutulamaz, çünkü unutulan sınır kaynakta yazılmamış bir satır olarak görünür.
Sarmalayıcı sürümü ters yöndedir. Toplamı daha kısadır — iki modül ve sarmalayıcı birlikte 28 satır, sınırsız hâlin bile altında — çünkü sarmalayıcı yalnız sınırı değil, istek kurmanın tekrarını da soğuruyor. Kaybedilen şey bağımlılık başına ayardır: çağrı yerinde katalog 200 ms’ye ayarlıydı, sarmalayıcı sürümünde o da 800 ms’dir.
Atlanan Çağrı Yeri
Sarmalayıcı satırının son iki sütunu bu dersin asıl ölçüsüdür: 6 sarmalanan, 1 atlanan çağrı yeri. Bedeli koşumda görünüyor: 28 çağrının 4’ü asılı kalıyor, sınırsız hâlin tam yarısı.
Fark yalnız sayıda değil, bilgi durumundadır: sınırsız hâlde her çağrı yeri şüphelidir ve yazan
bunu bilir; sarmalayıcılı hâlde altı çağrı yeri güvenlidir ve bir tanesi yalan söyler. Buradan
çıkan kural şudur: sarmalayıcı yerleşimi kapsam denetimi olmadan eksiktir. tara işlevinin
yaptığı — ham çağrı kurucusunu aramak — sürekli koşması gereken bir denetimdir.
İç İçe Sınırların Çelişmesi
Üçüncü tablo aynı işi üç katmandan geçiriyor ve her katmanın sınırı ötekini bilmiyor.
İlk satır yanlış ayarın adıdır: iç sınır dış sınırdan büyük. Ağ geçidi 300 ms’de vazgeçiyor, ödünç servisi beklemeyi sürdürüyor, bağımlılık 500 ms’de işini bitiriyor ve yanıt kimseye gitmiyor: 10 öksüz tamamlanma. Hiçbir gösterge bunu hatalı saymaz; ödünç servisi kendi açısından başarılı bir çağrı yapmıştır.
İkinci satırda iç sınır dıştakinden küçüktür: ödünç servisi önce vazgeçiyor, ağ geçidine adı konmuş bir hata dönüyor, öksüz tamamlanma sıfıra iniyor. Sınırların içten dışa büyümesi bir tercih değil, bir sıralama kuralıdır. Dördüncü satır kuralın öbür ucudur: iç sınır bağımlılığın gecikmesinin altına indiğinde (100 ms’ye karşı 500 ms) bağımlılıkta hiçbir sorun yokken her çağrı başarısız oluyor.
Üç satırın ortak sütunu asıl dersi veriyor: bağımlılığa giden çağrı her ayarda 10. Zaman aşımı bağımlılığın yükünü azaltmaz, yalnız bekleyeni serbest bırakır.
Sınanabilirlik
Sınırın yeri, davranışının testte nasıl tetiklendiğini de belirler.
// test/sinir.test.mjs — ayni davranis iki yerlesimde nasil tetiklenir. import { test } from "node:test"; import assert from "node:assert/strict"; import { VARSAYILAN } from "../istek.mjs"; import * as A from "../a/bildirim.mjs"; import * as B from "../b/bildirim.mjs"; const kova = (g) => (g < 100 ? "100 ms altinda" : `~${Math.round(g / 100) * 100} ms`); const sure = async (f) => { const t = Date.now(); await assert.rejects(f()); return Date.now() - t; }; test("cagri yerindeki sinir bir sabittir: test o kadar bekler", async () => { const g = await sure(A.bildirimGonder); assert.ok(g >= 700, `sinir 800 ms; olculen ${g}`); console.log(` cagri yerinde: red ${kova(g)}`); }); test("sarmalayicidaki sinir tek yerden dusurulur", async () => { VARSAYILAN.sure = 20; const g = await sure(B.bildirimGonder); assert.ok(g < 200, `sinir 20 ms; olculen ${g}`); console.log(` sarmalayicida: red ${kova(g)}`); }); test("atlanan cagri yeri sarmalayicinin ayarindan etkilenmez", async () => { assert.equal(await Promise.race([B.bildirimYinele().then(() => "yanit", () => "red"), new Promise((c) => setTimeout(() => c("hâlâ asili"), 300))]), "hâlâ asili"); });
node servisler.mjs > /dev/null 2>&1 & SP=$! sleep 1 node --test --test-force-exit --test-reporter=tap test/sinir.test.mjs | grep -E "^(ok|not ok|# |# (tests|pass|fail) )" kill $SP
# cagri yerinde: red ~800 ms ok 1 - cagri yerindeki sinir bir sabittir: test o kadar bekler # sarmalayicida: red 100 ms altinda ok 2 - sarmalayicidaki sinir tek yerden dusurulur ok 3 - atlanan cagri yeri sarmalayicinin ayarindan etkilenmez # tests 3 # pass 3 # fail 0
Çağrı yerindeki sınır bir sabittir; testin onu değiştirecek tutamağı yoktur ve gerçek süreyi beklemek zorundadır. Sarmalayıcıdaki sınır bir değişkendir; test varsayılanı 20 ms’ye indirip aynı dalı 100 ms’nin altında doğruluyor. Koşumdan bağımsız nicelik sınırların oranıdır: kırk kat.
Üçüncü test bu dersin ölçüsünü teste çeviriyor: atlanan çağrı yeri, varsayılan 20 ms’ye indirilmiş olmasına rağmen 300 ms sonunda hâlâ asılıdır. Kapsam bir sayı olarak ölçülebiliyorsa testte de doğrulanabilir. Çağrı yerinde yerleşimde böyle bir test yazılamaz, çünkü doğrulanacak bir kapsam kavramı yoktur.
Özet
- Zaman aşımının etkisi M19/K05 Dayanıklılık ve Güvenilirlik kursunda ölçüldü; burada ölçülen sınırın kodda nereye yerleştiğidir. İstemcinin varsayılan sınırı yoktur: sınırsız sürümde 28 çağrının 8’i hiç sonuçlanmadı ve bağımlılıkta 8 istek açık kaldı.
- Çağrı yerinde yerleşim 46 satırı 53 satıra çıkarır ve yapılandırma yüzeyini yedi ayrı sayıya dağıtır (iki ayrı değer, beş kopya), ama kapsamı tamdır: 7 sarmalanan, 0 atlanan çağrı yeri.
- Sarmalayıcı yerleşimi daha kısadır (28 satır) ve yapılandırma yüzeyi tektir, ama kapsamı garanti etmez: 6 sarmalanan, 1 atlanan çağrı yeri, 4 asılı çağrı. Atlanan çağrı yeri kaynakta bir eksiklik olarak görünmez, ancak taramayla bulunur.
- İç sınır dış sınırdan büyükse dış katman vazgeçer, iç katman çalışmayı sürdürür: 10 isteğin 10’unda öksüz tamamlanma. Sınırların içten dışa büyümesi bir sıralama kuralıdır.
- Bağımlılığa giden çağrı sayısı dört ayarın hepsinde 10’dur; zaman aşımı bekleyeni serbest bırakır, bağımlılığın yükünü azaltmaz. Sınırın değişken olması sınanabilirliği belirler: atlanan çağrı yeri doğrudan test edilebilir.
Sonraki Adım
Sınır kondu ve bekleyen serbest kaldı, ama elinde bir hata var ve o hatayla ne yapılacağı söylenmedi. En yakın karşılık yeniden denemektir. Bu kararın nereye konduğu da bir yerleşim sorusudur ve zaman aşımından daha tehlikelidir: zaman aşımı bağımlılığın yükünü değiştirmezken yeniden deneme onu doğrudan çoğaltır. Sonraki ders yeniden denemenin hangi katmanda durduğunu ölçer — aynı kural üç ayrı katmana kurulur, sonra üçü aynı anda açılır ve tek bir kullanıcı isteğinin bağımlılıkta kaç çağrıya dönüştüğü sayılır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.