Ders 12 / 17
Hata ve Yeniden Deneme Kalıpları
Geçici ve kalıcı hataların ayrımı, artan bekleme süreleriyle yeniden deneme, deneme bütçesi, işlem anahtarıyla yinelemeye dayanıklılık ve toplu başarısızlığın önlenmesi.
İçindekiler
İptal ve zaman aşımı, başarısızlığı belirlenmiş hale getirdi: bir isteğin ne zaman biteceği artık biliniyor. Geriye başarısızlıktan sonra ne yapılacağı kalıyor.
Her başarısızlık aynı değildir. Bazıları kendiliğinden geçer — kısa süreli aşırı yük, tek bir paketin kaybı. Bazıları tekrarlandığında aynı sonucu verir — yanlış adres, bilinmeyen istasyon. Bu ders ayrımı kurar, yeniden denemenin doğru biçimini yazar ve hangi işlemlerin güvenle tekrarlanabileceğini belirler.
Geçici ve Kalıcı Hata
Yeniden denemenin tek gerekçesi vardır: aynı isteğin bu kez başarılı olma olasılığı. Bu olasılık yoksa deneme, hem çağıranı bekletir hem sunucuyu meşgul eder.
| Hata | Sınıf | Gerekçe |
|---|---|---|
| Bağlantı kurulamadı | Geçici | Ağ koşulu değişebilir |
| 408, 429 | Geçici | Süre veya hız sınırı; sonra denenebilir |
| 500, 502, 503, 504 | Geçici | Sunucu tarafında geçici durum |
| 400, 401, 403, 404 | Kalıcı | İstek yeniden gönderilse de aynı sonuç |
Ayrım biçimsel değil anlamsaldır: 404, “bu kaynak yok” der; yeniden sormak yanıtı değiştirmez. 503 ise “şu an hizmet veremiyorum” der; bu, zamana bağlı bir ifadedir.
Ağ hatası ile başarısız durum kodu arasındaki ayrım da burada belirleyici olur. Önceki derste gösterildiği gibi, ağ hatası isteğin sunucuya ulaşıp ulaşmadığını bilmediğimiz durumdur. Bu belirsizlik, yan etkili işlemlerde yeniden denemeyi tehlikeli kılar.
Artan Bekleme ile Yeniden Deneme
Başarısız bir isteği hemen tekrarlamak, sorunu ağırlaştırır: aşırı yüklü bir sunucu, aynı anda vazgeçen bütün istemcilerin tekrar denemesiyle karşılaşır. Bu nedenle bekleme süresi her denemede artırılır — üstel geri çekilme (exponential backoff).
import { createServer } from "node:http"; let istekSayisi = 0; const sunucu = createServer((istek, yanit) => { istekSayisi += 1; if (istekSayisi <= 2) { yanit.writeHead(503, { "content-type": "application/json" }); yanit.end(JSON.stringify({ hata: "hizmet geçici olarak kullanılamıyor" })); return; } yanit.writeHead(200, { "content-type": "application/json" }); yanit.end(JSON.stringify({ istasyon: "A1", deger: 21.4 })); }); await new Promise((karar) => sunucu.listen(0, "127.0.0.1", karar)); const taban = `http://127.0.0.1:${sunucu.address().port}`; const GECICI_DURUMLAR = new Set([408, 429, 500, 502, 503, 504]); function bekle(sure) { return new Promise((karar) => setTimeout(karar, sure)); } async function denemeliGetir(adres, denemeSayisi, tabanBekleme) { let sonHata = null; for (let deneme = 1; deneme <= denemeSayisi; deneme += 1) { const yanit = await fetch(adres); if (yanit.ok) { console.log("deneme", deneme, "— başarılı"); return await yanit.json(); } if (!GECICI_DURUMLAR.has(yanit.status)) { throw new Error(`kalıcı hata: ${yanit.status}`); } sonHata = new Error(`geçici hata: ${yanit.status}`); console.log("deneme", deneme, "— geçici hata:", yanit.status); if (deneme < denemeSayisi) { const bekleme = tabanBekleme * 2 ** (deneme - 1); console.log(" planlanan bekleme:", bekleme, "ms"); await bekle(bekleme); } } throw new Error(`deneme bütçesi tükendi: ${denemeSayisi}`, { cause: sonHata }); } console.log("sonuç:", await denemeliGetir(`${taban}/olcum/A1`, 4, 10)); sunucu.close();
deneme 1 — geçici hata: 503
planlanan bekleme: 10 ms
deneme 2 — geçici hata: 503
planlanan bekleme: 20 ms
deneme 3 — başarılı
sonuç: { istasyon: 'A1', deger: 21.4 }
Sunucu ilk iki isteği geri çevirecek biçimde kuruldu; üçüncü deneme başarılı oldu. Yazdırılan bekleme değerleri ölçüm değil, hesaplanmış plandır: taban değer her denemede ikiyle çarpılır.
Gerçek dağıtımlarda bu plana bir de rastgele sapma eklenir. Gerekçesi şudur: aynı anda başarısız olan istemciler aynı planı uygularsa, yeniden denemeleri de aynı ana denk gelir ve sunucu ikinci bir dalgayla karşılaşır. Sapma, dalgayı zamana yayar. Bu derste çıktıların belirlenimci kalması için sapma kullanılmadı.
Deneme Bütçesi ve Kalıcı Hatada Erken Çıkış
İki koruma zorunludur. Deneme sayısı sınırlanmalıdır, yoksa hizmet dönmediğinde çağıran sonsuza kadar bekler. Kalıcı hatada ise hiç beklenmemelidir.
import { createServer } from "node:http"; const sunucu = createServer((istek, yanit) => { const yol = new URL(istek.url, "http://yerel").pathname; if (yol === "/olcum/Z9") { yanit.writeHead(404, { "content-type": "application/json" }); yanit.end(JSON.stringify({ hata: "bilinmeyen istasyon" })); return; } yanit.writeHead(503, { "content-type": "application/json" }); yanit.end(JSON.stringify({ hata: "hizmet geçici olarak kullanılamıyor" })); }); await new Promise((karar) => sunucu.listen(0, "127.0.0.1", karar)); const taban = `http://127.0.0.1:${sunucu.address().port}`; const GECICI_DURUMLAR = new Set([408, 429, 500, 502, 503, 504]); function bekle(sure) { return new Promise((karar) => setTimeout(karar, sure)); } async function denemeliGetir(adres, denemeSayisi, tabanBekleme) { let sonHata = null; for (let deneme = 1; deneme <= denemeSayisi; deneme += 1) { const yanit = await fetch(adres); if (yanit.ok) return await yanit.json(); if (!GECICI_DURUMLAR.has(yanit.status)) { throw new Error(`kalıcı hata: ${yanit.status}`); } sonHata = new Error(`geçici hata: ${yanit.status}`); console.log("deneme", deneme, "— geçici hata:", yanit.status); if (deneme < denemeSayisi) await bekle(tabanBekleme * 2 ** (deneme - 1)); } throw new Error(`deneme bütçesi tükendi: ${denemeSayisi}`, { cause: sonHata }); } try { await denemeliGetir(`${taban}/olcum/Z9`, 4, 10); } catch (hata) { console.log("kalıcı hatada hiç beklenmedi:", hata.message); } try { await denemeliGetir(`${taban}/olcum/A1`, 3, 10); } catch (hata) { console.log("son hata:", hata.message); console.log("kök neden:", hata.cause.message); } sunucu.close();
kalıcı hatada hiç beklenmedi: kalıcı hata: 404 deneme 1 — geçici hata: 503 deneme 2 — geçici hata: 503 deneme 3 — geçici hata: 503 son hata: deneme bütçesi tükendi: 3 kök neden: geçici hata: 503
Bilinmeyen istasyon için tek istek yapıldı; geçici hata veren adres için üç deneme
tüketildi ve bütçe dolduğunda hata, kök nedeni taşıyarak yukarı verildi. Hata yönetimi
dersindeki cause kalıbı burada tanı için gereklidir: çağıran hem “kaç denemede
vazgeçildiğini” hem “en son ne olduğunu” görür.
Yeniden denemenin toplam süresi de bir sınırdır. Deneme sayısı ile bekleme planı birleşince, en kötü durumda geçecek süre baştan hesaplanabilir olmalıdır; iptal işareti bu toplam süreye de bağlanabilir.
Yan Etkili İşlemleri Yeniden Denemek
Ölçüm okumak zararsızdır: aynı isteği iki kez göndermek yalnız fazladan iş yapar. Ölçüm yazmak öyle değildir. Ağ hatası aldığınızda isteğin sunucuya ulaşıp ulaşmadığını bilemezsiniz; tekrar gönderirseniz aynı kayıt iki kez oluşabilir.
Çözüm, isteğe onu tekil kılan bir işlem anahtarı eklemektir. Sunucu, aynı anahtarla gelen ikinci isteği yeni bir işlem saymaz; ilk işlemin sonucunu döndürür.
import { createServer } from "node:http"; const kayitlar = []; const sunucu = createServer((istek, yanit) => { let govde = ""; istek.on("data", (parca) => { govde += parca; }); istek.on("end", () => { const anahtar = istek.headers["x-islem-anahtari"]; const varOlan = anahtar === undefined ? undefined : kayitlar.find((k) => k.anahtar === anahtar); if (varOlan === undefined) { kayitlar.push({ anahtar, veri: JSON.parse(govde) }); } yanit.writeHead(200, { "content-type": "application/json" }); yanit.end(JSON.stringify({ toplamKayit: kayitlar.length })); }); }); await new Promise((karar) => sunucu.listen(0, "127.0.0.1", karar)); const taban = `http://127.0.0.1:${sunucu.address().port}`; async function olcumYaz(basliklar) { const yanit = await fetch(`${taban}/kayit`, { method: "POST", headers: { "content-type": "application/json", ...basliklar }, body: JSON.stringify({ istasyon: "A1", deger: 21.4 }), }); return await yanit.json(); } console.log("anahtarsız 1. deneme:", await olcumYaz({})); console.log("anahtarsız 2. deneme:", await olcumYaz({})); console.log("anahtarlı 1. deneme:", await olcumYaz({ "x-islem-anahtari": "yazma-77" })); console.log("anahtarlı 2. deneme:", await olcumYaz({ "x-islem-anahtari": "yazma-77" })); sunucu.close();
anahtarsız 1. deneme: { toplamKayit: 1 }
anahtarsız 2. deneme: { toplamKayit: 2 }
anahtarlı 1. deneme: { toplamKayit: 3 }
anahtarlı 2. deneme: { toplamKayit: 3 }
Anahtarsız iki istek iki kayıt üretti; anahtarlı iki istek tek kayıt üretti. Aynı isteğin tekrarının sonucu değiştirmemesi özelliğine yinelemeye dayanıklılık (idempotence) denir; İnternet Nasıl Çalışır kursunda aynı özellik HTTP yöntemleri için tanıtılmıştı. Buradaki eklenti, özelliğin yöntemden gelmediği durumda anahtarla sağlanabilmesidir.
Kural şu biçimde özetlenir: yeniden deneme, ancak işlem yinelemeye dayanıklıysa güvenlidir. Değilse önce dayanıklı hale getirilmeli, sonra denenmelidir.
Yeniden Denemenin Sınırı
Yeniden deneme, tek bir isteği kurtarır; sistemi kurtarmaz. Hizmet gerçekten düşmüşse, bütün istemcilerin denemeye devam etmesi yükü artırır ve toparlanmayı geciktirir.
Bu nedenle uygulamalarda deneme, bir üst düzenekle sınırlandırılır: art arda başarısızlık belirli bir eşiği geçtiğinde istekler bir süre hiç gönderilmez, sonra tek bir yoklama isteğiyle durum sınanır. Aynı düşüncenin bir başka biçimi, aynı anda süren toplam deneme sayısına sınır koymaktır.
Ölçüm akışı açısından sonuç şudur: her istasyon için birkaç deneme yapılır, kalıcı hatada
hemen vazgeçilir, bütçe dolduğunda istasyon “veri yok” olarak işaretlenir ve toplama
sürer. allSettled ile kurulan dayanıklı toplama, bu davranışın doğal kabıdır.
Özet
- Yeniden deneme yalnız geçici hatalarda anlamlıdır; kalıcı hatada hem çağıran bekletilir hem sunucu boşuna meşgul edilir.
- Bekleme süresi her denemede artırılır; sabit aralıklı denemeler aşırı yüklü bir hizmete ikinci dalga olarak çarpar.
- Deneme sayısı ve toplam süre sınırlanmalı, bütçe dolduğunda hata kök nedeniyle birlikte yukarı verilmelidir.
- Yan etkili işlemler ancak yinelemeye dayanıklıysa yeniden denenebilir; dayanıklılık işlem anahtarıyla sağlanır.
- Yeniden deneme tek isteği kurtarır; sistem düzeyinde ayrıca bir sınırlama düzeneği gerekir.
Sonraki Adım
Ağ konusu tamamlandı: istek yapıldı, sınırlandı, iptal edildi ve dayanıklı hale getirildi. Bu derslerde birkaç kez “bellekte kalır” denildi ama neyin, neden ve ne kadar süreyle kaldığı açıklanmadı. Sonraki konu bunu ele alır: belleğin ayrılma ve serbest bırakılma döngüsü, erişilebilirlik temelli toplama, eşzamansız kodun bellekte tuttuğu şeyler ve sızıntının araçlarla tanısı.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.