---
title: 'Hata ve Yeniden Deneme'
source: 'https://academia.sh/tr/kurslar/ios-veri-ve-ag/hata-ve-yeniden-deneme'
course: 'Veri, Ağ ve Eşzamanlılık'
language: tr
updated: '2026-10-21T05:03:09+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Ağ hatalarını yeniden denenebilir ve denenemez olarak saf bir işlevle sınıflandırır'
    - 'Zaman aşımından sonra yeniden denenen anahtarsız bir isteğin sunucuda ikinci kaydı doğurduğunu ölçer'
    - 'Etkisizlik anahtarlı isteğin aynı yeniden denemede sağlam kaldığını gösterir'
    - 'Yanıt kaybı ile isteğin ulaşmamasının istemciden ayırt edilemediğini söyler'
---

# Hata ve Yeniden Deneme

Ağ hatalarının sınıflandırılması ve zaman aşımından sonra yeniden denenen bir siparişin sunucuda ikinci kaydı doğurup doğurmadığının ölçümü.

Ağ katmanında hata bir istisna değil, günlük bir olaydır. Bağlantı kopar, sunucu geç yanıtlar, yanıt yolda kaybolur. Ağ istemcisi bu hataları iki gruba ayırmak zorundadır: tekrar denemeye değenler ve değmeyenler. **Yeniden deneme** (retry) doğru yapılırsa uygulamayı kararlı kılar, yanlış yapılırsa aynı siparişi iki kez verir.

Bu derste hata sınıflandırması saf bir işlevdir ve ölçülür. Asıl soru ise sınıflandırmanın bıraktığı yerdedir: bir isteğin yeniden denenmesi sunucuda ne yapar? Çekirdek aynıdır.

```swift
// Replay.swift — bir islemin yinelemeye ve siraya duyarliligini siniflandirir
struct Trial<State: Equatable & CustomStringConvertible> {
    let label: String
    let once: State
    let twice: State
    let forward: State
    let backward: State

    var sensitivity: String {
        switch (once != twice, forward != backward) {
        case (false, false): return "saglam"
        case (true, false): return "yinelemeye-duyarli"
        case (false, true): return "siraya-duyarli"
        case (true, true): return "cift-duyarli"
        }
    }

    func report() -> String {
        print("\(label) | bir=\(once) iki=\(twice) ab=\(forward) ba=\(backward) -> \(sensitivity)")
        return sensitivity
    }
}

func replay<State>(_ label: String, from start: State, _ a: (State) -> State, then b: (State) -> State) -> Trial<State> {
    Trial(label: label, once: a(start), twice: a(a(start)), forward: b(a(start)), backward: a(b(start)))
}

func countSensitivities(_ results: [String]) {
    for name in ["saglam", "yinelemeye-duyarli", "siraya-duyarli", "cift-duyarli"] {
        print("\(name): \(results.filter { $0 == name }.count)")
    }
}
```

## Hata Sınıflandırması

Dört hata ayırt edilir: zaman aşımı, bağlantı yokluğu, sunucu hatası ve istemci hatası. İlk ikisi ve beşyüzlü sunucu hataları yeniden denenebilir; dörtyüzlü istemci hataları denenemez, çünkü aynı istek aynı hatayı alır. Deneme sayısı bir **deneme bütçesiyle** sınırlanır ve bütçe bir tamsayıdır. Bekleme ya da **üstel geri çekilme** süresi bu derste modellenmez ve ölçülmez.

```swift
// Orders.swift — hata sinifi, siparis defteri ve yeniden deneme dongusu
enum Failure: Equatable {
    case timeout
    case offline
    case server(Int)
    case client(Int)
}

func isRetryable(_ failure: Failure) -> Bool {
    switch failure {
    case .timeout, .offline: return true
    case .server(let code): return code >= 500
    case .client: return false
    }
}

struct Ledger: Equatable, CustomStringConvertible {
    var orders: [String] = []
    var description: String { "[" + orders.joined(separator: ",") + "]" }
}

func place(_ item: String, key: String?) -> (Ledger) -> Ledger {
    { ledger in
        let tag = "\(item)@\(key ?? "-")"
        if key != nil && ledger.orders.contains(tag) { return ledger }
        return Ledger(orders: ledger.orders + [tag])
    }
}

func simulate(budget: Int, key: String?, lostResponses: Int) -> (attempts: Int, ledger: Ledger) {
    var ledger = Ledger()
    var attempts = 0
    while attempts < budget {
        attempts += 1
        ledger = place("kitap", key: key)(ledger)
        if attempts > lostResponses { break }
    }
    return (attempts, ledger)
}
```

`Failure` dört durumlu bir **numaralandırmadır**. `isRetryable` yalnız hatanın türüne bakar. `place` sunucunun siparişi kaydetmesini kurar: anahtar verilmişse ve aynı etiketli kayıt varsa ikinci kaydı eklemez. `simulate` istemcinin döngüsünü kurar. İstek her denemede sunucuya ulaşır ve kaydedilir; ilk `lostResponses` yanıt ise yolda kaybolur. İstemci kayıp yanıtı zaman aşımı olarak görür ve bütçe elverdiği sürece yeniden dener. **Model, işlemin duruma ne yaptığını kurar; ağın kendisi değildir.**

```swift
// main.swift — siniflandirma, yeniden deneme, iki yazma bicimi
print("zaman-asimi: \(isRetryable(.timeout))")
print("baglanti-yok: \(isRetryable(.offline))")
print("sunucu-503: \(isRetryable(.server(503)))")
print("istemci-404: \(isRetryable(.client(404)))")

let keyless = simulate(budget: 3, key: nil, lostResponses: 1)
let keyed = simulate(budget: 3, key: "k-9", lostResponses: 1)
print("anahtarsiz: deneme=\(keyless.attempts) defter=\(keyless.ledger)")
print("anahtarli: deneme=\(keyed.attempts) defter=\(keyed.ledger)")

let withoutKey = replay("anahtarsiz-siparis", from: Ledger(), place("kitap", key: nil), then: place("kitap", key: nil)).report()
let withKey = replay("anahtarli-siparis", from: Ledger(), place("kitap", key: "k-9"), then: place("kitap", key: "k-9")).report()
let spending = replay("butce-harca", from: 3, { max($0 - 1, 0) }, then: { max($0 - 1, 0) }).report()

countSensitivities([withoutKey, withKey, spending])
```

```
zaman-asimi: true
baglanti-yok: true
sunucu-503: true
istemci-404: false
anahtarsiz: deneme=2 defter=[kitap@-,kitap@-]
anahtarli: deneme=2 defter=[kitap@k-9]
anahtarsiz-siparis | bir=[kitap@-] iki=[kitap@-,kitap@-] ab=[kitap@-,kitap@-] ba=[kitap@-,kitap@-] -> yinelemeye-duyarli
anahtarli-siparis | bir=[kitap@k-9] iki=[kitap@k-9] ab=[kitap@k-9] ba=[kitap@k-9] -> saglam
butce-harca | bir=2 iki=1 ab=1 ba=1 -> yinelemeye-duyarli
saglam: 1
yinelemeye-duyarli: 2
siraya-duyarli: 0
cift-duyarli: 0
```

Dört hatanın üçü denenebilirdir, istemci hatası değildir. Ama dikkat çeken ikinci bölümdür. Aynı kayıp yanıt aynı döngüde iki farklı sonuç verdi: anahtarsız istek iki deneme sonunda iki kayıt bıraktı, anahtarlı istek aynı iki denemeden sonra tek kayıt bıraktı. İstemcinin gördüğü şey ikisinde de aynıdır: bir zaman aşımı ve ardından başarılı bir yanıt.

`anahtarsiz-siparis` yinelemeye duyarlıdır: ikinci uygulama ikinci kaydı ekler. `anahtarli-siparis` sağlamdır: aynı anahtar ikinci kaydı engeller. `butce-harca` yinelemeye duyarlıdır; ama bu hata değil, bütçenin işidir. Bütçe tam da yinelendikçe azalsın diye vardır. Hangi yinelemenin hata, hangisinin tasarım olduğunu kural söylemez.

**AG7.** Model sunucunun anahtarı tanıdığını varsayar. Gerçek bir sunucu anahtar başlığını yok sayabilir ve o zaman anahtarlı istek de ikinci kaydı doğurur.

**AG8.** Model kayıp yanıtı her zaman isteğin sunucuya ulaştığı biçimde kurar. Ulaşmayan istek ayrıca incelenir.

## İstemcide Hatanın Okunması

Gerçek bir istemcide hata, bir `URLError`'ın kodundan okunur.

```swift
import Foundation

func classify(_ error: Error) -> Failure {
    guard let urlError = error as? URLError else { return .client(0) }
    switch urlError.code {
    case .timedOut: return .timeout
    case .notConnectedToInternet: return .offline
    default: return .client(0)
    }
}
```

Bu parça KAYNAK kalır. `URLError` ağ hatalarını bir kodla taşır; `.timedOut` ve `.notConnectedToInternet` bunlardan ikisidir. Parça yalnız iki kodu eşler; gerçek bir sınıflandırma daha fazlasını kapsar. Anahtar başlığının adı sunucuya bağlıdır; genelde bir istek başlığıyla gönderilir.

## Yeniden Denemenin Bedeli

Yeniden deneme bir sigorta gibi görünür, ama ucuz değildir. Her deneme sunucuya bir istek daha yükler, kullanıcının bekleme süresini uzatır ve pil harcar. Sunucu zaten yük altındaysa, binlerce istemcinin aynı anda yeniden denemesi yükü azaltmaz, artırır. Bu yüzden iki sınırlama birlikte kullanılır: deneme bütçesi toplam deneme sayısını kısar, **üstel geri çekilme** ise denemeler arasındaki beklemeyi her seferinde katlar. Bütçe kaç kez denendiğini, geri çekilme ne sıklıkta denendiğini sınırlar.

Geri çekilmeye genellikle küçük bir rastgele pay eklenir. Aksi hâlde aynı anda başarısız olan bin istemci, aynı anda ve aynı aralıklarla yeniden dener ve sunucuya dalgalar hâlinde çarpar. Rastgele pay bu dalgaları dağıtır. Bu ders bekleme süresini modellemediği için bunu ölçmez; ama gerçek bir istemcide bütçe ile geri çekilmeyi ayrı iki karar olarak ele almak gerekir.

Bütçenin ölçümde yinelemeye duyarlı çıkması bir kusur değildir. `butce-harca` satırında `bir=2` ile `iki=1` farklıdır, çünkü bütçe tam da her harcamada azalsın diye vardır. Kural yinelemeye duyarlılığı yalnız bildirir; bunun istenen mi istenmeyen mi olduğunu ayırt etmez. Bu ayrımı yapan, ölçümü okuyan kişidir.

## Gerçek Uygulamada Nerede Görülür

Bir alışveriş uygulamasında kullanıcı "Siparişi Onayla" düğmesine basar. Ağ yavaştır, istek sunucuya ulaşır ve sipariş kaydedilir, ama yanıt yolda kaybolur. İstemci zaman aşımı görür ve isteği yeniden gönderir. Anahtar yoksa sunucu ikinci isteği yepyeni bir sipariş sayar ve kullanıcı iki kez ücretlendirilir. `anahtarsiz` satırındaki `defter=[kitap@-,kitap@-]` tam olarak bu durumdur.

Kullanıcı bunu çoğunlukla ayrı bir hata olarak yaşar: "Siparişim neden iki kez geldi?" Destek ekibi iki kaydı görür, ikisi de geçerli görünür ve hangisinin silineceği belirsizdir. Sorunu yeniden üretmek zordur, çünkü yalnız yanıtın kaybolduğu nadir bir durumda ortaya çıkar. Geliştirici testte ağ hep çalıştığı için sorunu hiç görmez. Üretimde ise milyonlarca istek arasında her gün birkaç kez gerçekleşir.

Aynı durum yalnız ödemeyle sınırlı değildir. Bir mesajı göndermek, bir yorum eklemek, bir sepete ürün koymak gibi sunucuda yeni bir kayıt doğuran her işlem aynı riski taşır. Okuma işlemleri ise genellikle güvenle yeniden denenir, çünkü sunucudaki durumu değiştirmezler.

## Yanlış Kurulum ve Düzeltmesi

Yaygın yanlış kurulum, her hatada ve sınırsız biçimde yeniden denemektir.

```swift
import Foundation

// Yanlış: her hata yeniden denenir, bütçe yok, bekleme yok
func sendWithRetry(_ request: URLRequest, using session: URLSession) async throws -> Data {
    while true {
        do {
            let (data, _) = try await session.data(for: request)
            return data
        } catch {
            continue
        }
    }
}
```

Bu parça KAYNAK kalır. Üç sorunu vardır: bir `404` bile sonsuza dek denenir, sunucu çöktüğünde istemci saniyede yüzlerce istek gönderir ve kullanıcı ekranın neden hiç ilerlemediğini anlayamaz. Düzeltme sınıflandırmayı, bütçeyi ve beklemeyi bir araya getirir.

```swift
import Foundation

// Düzeltme: yalnız denenebilir hata, sınırlı bütçe, katlanan bekleme
func sendWithRetry(_ request: URLRequest, using session: URLSession, budget: Int = 3) async throws -> Data {
    var delay: UInt64 = 500_000_000
    for attempt in 1...budget {
        do {
            let (data, response) = try await session.data(for: request)
            if let http = response as? HTTPURLResponse, http.statusCode >= 500, attempt < budget {
                try await Task.sleep(nanoseconds: delay)
                delay *= 2
                continue
            }
            return data
        } catch {
            guard isRetryable(classify(error)), attempt < budget else { throw error }
            try await Task.sleep(nanoseconds: delay)
            delay *= 2
        }
    }
    throw URLError(.unknown)
}
```

Bu da KAYNAK kalır. Dönüş yalnız beşyüzlü yanıtlarda ve denenebilir hatalarda bekler. Dörtyüzlü yanıt beklemeden döner; çağıran onu kendi hatası olarak okur. Bekleme her denemede katlanır. Bütçe bittiğinde son hata çağırana atılır, böylece kimse hatanın yutulduğunu fark etmeden bekleyip durmaz. Düzeltilmiş işlev yeniden denemeyi güvenli yapmaz; yalnız sınırlar. İsteğin yinelemeye karşı sağlam olması için ayrıca etkisizlik anahtarı gerekir, ve bu `anahtarli-siparis` satırının gösterdiği şeydir.

## Hatayı Kullanıcıya Taşımak

Bütçe tükendiğinde hata sessizce kaybolmamalıdır. Kullanıcıya iki şey gösterilir: ne olduğu ve ne yapabileceği. Bağlantı yoksa "İnternet bağlantınız yok" ve bağlantı gelince otomatik yeniden deneme önerisi anlamlıdır. Sunucu hatasında "Şu anda ulaşamıyoruz, birazdan yeniden deneyin" yeterlidir. İstemci hatasında ise yeniden denemenin bir yararı yoktur; kullanıcıya girdiğini düzeltmesi söylenir.

Anahtarsız bir yazma isteği zaman aşımıyla bittiğinde durum daha da inceliklidir. İstemci isteğin sunucuya ulaşıp ulaşmadığını bilmez; bu, sınırlayıcı ölçümün gösterdiği belirsizliktir. Doğru davranış, kullanıcıya "Sonuç belirsiz" demek ve sunucudaki durumu sorgulamadan ikinci bir yazma göndermemektir. Anahtar varsa belirsizlik ortadan kalkar, çünkü ikinci istek aynı anahtarla zararsızca tekrarlanabilir.

## Okuma ve Yazma İsteklerinin Ayrımı

Yeniden deneme kararının ilk girdisi hata değil, isteğin türüdür. Bir okuma isteği sunucudaki durumu değiştirmez; yanıtı kaybolsa da ikinci kez göndermek güvenlidir. Bir yazma isteği ise durumu değiştirir ve ikinci gönderim, durumun iki kez değişmesi demektir. Protokol bu ayrımı belli ölçüde kendisi taşır: okuma istekleri tanım gereği tekrarlanabilir kabul edilir, yeni bir kayıt doğuran istekler kabul edilmez. Güncelleme ve silme istekleri de çoğunlukla tekrarlanabilir olarak tasarlanır, çünkü aynı güncellemeyi ikinci kez uygulamak sonucu değiştirmez.

Pratikte bu, istemcide iki ayrı yol anlamına gelir. Okuma yolu hatayı sınıflandırır, bütçe elverdiği sürece bekleyip yeniden dener. Yazma yolu ise önce isteğin bir etkisizlik anahtarı taşıyıp taşımadığına bakar. Anahtar varsa okuma yolu gibi davranır. Anahtar yoksa yeniden deneme kararı kullanıcıya bırakılır ya da sunucudaki durum sorgulanarak verilir. Ölçümdeki `anahtarsiz-siparis` ile `anahtarli-siparis` satırları, bu iki yolun farkını sonuçlarıyla gösterir.

## Bütçeyi Ne Harcar

Her hata bütçeden aynı payı almaz. Bağlantı yokluğunda istek hiç yola çıkmaz; bunu bir deneme saymak bütçeyi boşa harcar. Daha iyi bir kurgu, bağlantı gelene kadar beklemek ve bütçeyi yalnız gerçekten gönderilen istekler için tüketmektir. Sunucu hatalarında ise her gönderim sunucuya yük bindirir ve bütçe doğrudan bu yükü sınırlar. Zaman aşımı ikisinin arasındadır: istek yola çıkmıştır, ama sonucu bilinmez.

Bütçenin büyüklüğü de hatanın doğasına bağlıdır. Kısa süreli bir ağ kesintisi için üç deneme genellikle yeterlidir. Sunucunun uzun süre erişilemez olduğu bir durumda onlarca deneme yalnız yük yaratır; o zaman istemcinin vazgeçmesi ve kullanıcıyı bilgilendirmesi daha doğrudur.

## Sık Yapılan Hatalar

- Her hatayı yeniden denemek. Bir `404` yanıtı kaç kez tekrarlanırsa tekrarlansın `404` olarak kalır.
- Yeniden denemeye üst sınır koymamak. Bütçesiz döngü sunucuyu daha da yorar.
- Anahtarsız bir `POST`'u zaman aşımından sonra yeniden denemek.
- Zaman aşımını "istek başarısız oldu" diye okumak. Zaman aşımı yalnız yanıtın gelmediğini söyler.
- Bekleme süresi olmadan art arda denemek. Bu derste süre ölçülmez ama gerçek istemcide gerekir.

## Ölçülen Sonucun Yorumu

Yeniden deneme bir sınıflandırma sorusu gibi görünür ama bir duyarlılık sorusudur. Sınıflandırma iki sonuçtan birini verir: dene ya da deneme. Duyarlılık ise denemenin sonucunun ne olacağını sorar. Anahtarsız sipariş ikinci kaydı doğurdu; anahtarlı sipariş doğurmadı. Hata sınıflandırması ikisinde de aynıydı.

## Sınırlayıcı Ölçüm

İstemci hatayı gördüğünde iki dünyadan hangisinde olduğunu bilemez: istek sunucuya ulaştı ama yanıt kayboldu, ya da istek hiç ulaşmadı. Aşağıdaki ölçüm iki dünyayı kurar ve istemcinin gördüğünü ile sunucudaki defteri yan yana basar.

```swift
// main.swift — iki dunya, ayni gozlem
func world(requestArrived: Bool) -> (observed: Failure, ledger: Ledger) {
    let ledger = requestArrived ? place("kitap", key: nil)(Ledger()) : Ledger()
    return (.timeout, ledger)
}

let lost = world(requestArrived: true)
let never = world(requestArrived: false)
print("istemcinin gordugu esit mi: \(lost.observed == never.observed)")
print("sunucudaki defter esit mi: \(lost.ledger == never.ledger)")
```

```
istemcinin gordugu esit mi: true
sunucudaki defter esit mi: false
```

İstemcinin gördüğü iki dünyada aynıdır, sunucudaki defter farklıdır. Bu yüzden istemci yeniden deneyip denememeye gözleminden karar veremez; kararı isteğin kendisi, yani anahtar verir. Kural bu belirsizliği çözmez, ölçmez de.

**AG9.** Ölçüm iki dünyayı istemci gözünden ayırt edilemez olarak kurar. Gerçek bir istemci bazı hatalarda (bağlantı hiç kurulamadı gibi) isteğin ulaşmadığını bilebilir.

## Özet

- Ağ hataları yeniden denenebilir ve denenemez olarak saf bir işlevle sınıflandırılır; deneme sayısı bir bütçeyle sınırlanır.
- Anahtarsız bir isteği zaman aşımından sonra yeniden denemek sunucuda ikinci kaydı doğurdu.
- Aynı yeniden denemede etkisizlik anahtarlı istek tek kayıt bıraktı.
- Bütçeyi harcamak yinelemeye duyarlıdır, ama bu tasarımdır, hata değil.
- İstemci, yanıt kaybı ile isteğin ulaşmamasını gözleminden ayıramaz.
- Bu dersin duyarlılığı: **yinelemeye duyarlı**.

## Sonraki Adım

Yeniden denenen bir istek bazen bir kimlik de taşır. Sıradaki derste Kimlik Doğrulama, iki eşzamanlı belirteç yenilemesinin yanıtlarının farklı sırayla saklanmasının eski belirteci nasıl geri getirebildiğini ölçecek.
