Resilience'ı Test Etmek ve Gözlemlemek: WireMock.Net, Polly Chaos Strategy'leri, FakeTimeProvider ve Metric'ler
Resilience davranışı WireMock.Net ile gerçek API'ye karşı integration testlerle doğrulanır, backoff FakeTimeProvider ile beklemeden test edilir, Polly'nin chaos strategy'leri staging'de hata enjekte eder ve resilience.polly.strategy.events metric'i retry ile breaker'ı görünür kılar. Bookstore'da POST'un hiç tekrar denenmediğini, %20 hatada checkout'u ve metric'leri test ediyoruz.
Sürüm notu: Bu yazıdaki kodlar .NET 10, Polly 8.9, Microsoft.Extensions.Http.Resilience 10.11, WireMock.Net 2.31, xUnit v3 4.0, Microsoft.Extensions.TimeProvider.Testing 10.11 ve OpenTelemetry .NET 1.21 ile derlenip denenmiştir.
Önce Tahmin Et
Kargo client'ı 429 aldığında Retry-After: 2'ye uyup iki kez tekrar deniyor. Bu davranışı test eden bir integration test en az kaç saniye sürer?
İkinci soru: checkout'un ödeme sağlayıcısı %20 oranında 500 dönerken nasıl davrandığını test etmek istiyorsun. Bunun için Polly'nin chaos strategy'lerine mi, fake'e mi ihtiyacın var?
Üçüncü soru: production'da kargo firmasının circuit breaker'ı açıldı ve fallback devreye girdi. Kullanıcı hiçbir hata görmüyor. Bunu kim, nasıl fark eder?
Birinci sorunun cevabı 4 saniye değil, 264 milisaniye. İkincisinin cevabı "ikisi de olur ama farklı şeyleri test eder". Üçüncüsü bu yazının ikinci yarısı. Önceki iki yazıda resilience davranışlarını fake'leri farklı modlarda başlatıp elle gözledik. Şimdi aynı senaryoları otomatik testlere çeviriyor, production'da da görünür kılıyoruz.
Test Altyapısı: WebApplicationFactory ve In-Process WireMock
Testler gerçek Bookstore.Api'yi WebApplicationFactory ile ayağa kaldırıyor ve dış servis adreslerini test içinde başlatılan WireMock.Net sunucularına çeviriyor. Integration test altyapısının ayrıntısını Integration ve E2E (End-to-End; uçtan uca) Test serisinde anlattık; burada önemli olan, HTTP pipeline'ının, resilience handler'ların ve adapter'ların hiçbirinin mock'lanmaması. Yalnız dış servis sahte:
// Gerçek API; dış servisler yerine process içindeki WireMock fake'lerine bağlı
public sealed class BookstoreApiFactory(
WireMockServer payments,
WireMockServer carrier,
TimeProvider? timeProvider = null,
IDictionary<string, string?>? settings = null) : WebApplicationFactory<CheckoutService> // Bookstore.Api'den herhangi bir tip
{
public FakeEmailSender Emails { get; } = new();
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Development");
builder.UseSetting("Payments:Provider:BaseUrl", payments.Url + "/");
builder.UseSetting("Shipping:LegacyCarrier:BaseUrl", carrier.Url + "/");
foreach (var (key, value) in settings ?? new Dictionary<string, string?>())
{
builder.UseSetting(key, value);
}
builder.ConfigureTestServices(services =>
{
services.RemoveAll<IEmailSender>();
services.AddSingleton<IEmailSender>(Emails);
if (timeProvider is not null)
{
services.RemoveAll<TimeProvider>();
services.AddSingleton(timeProvider);
}
});
}
}
İki küçük ayrıntı. WebApplicationFactory<Program> yazamadık, çünkü test projesi hem API'yi hem Bookstore.FakeProviders'ı referanslıyor ve ikisinde de top-level statement'lardan üretilmiş bir Program var; derleyici CS0433 ile hangisini kastettiğimizi soruyor. Factory'nin istediği tek şey entry assembly'den bir tip, CheckoutService de iş görüyor. İkincisi, e-posta gönderimini fake bir IEmailSender ile değiştirdik; yoksa her checkout bir önceki yazıdaki SMTP (Simple Mail Transfer Protocol; e-posta gönderme protokolü) retry'larını bekliyor.
Fake'ler ilk yazıdan beri kullandığımız FakePaymentProvider ve FakeLegacyCarrier sınıfları; port vermeden Start() çağrılınca boş bir port seçiyorlar. WireMock'un LogEntries listesi sunucuya gelen her isteği tutuyor ve retry sayısını doğrulamanın en dürüst yolu bu: client'ın ne yaptığını değil, karşı tarafın ne gördüğünü sayıyorsun.
private int QuoteRequests => _carrier.LogEntries.Count(e => e.RequestMessage?.Path == "/api/v2/quotes");
private int ChargeRequests => _payments.LogEntries.Count(e => e.RequestMessage?.Path == "/v1/charges");
Retry ve Circuit'i Doğrulamak
Üç test, önceki yazılarda elle gördüğümüz üç davranışı sabitliyor:
[Fact]
public async Task Carrier_errors_are_retried_then_checkout_uses_the_flat_tariff()
{
FakeLegacyCarrier.Configure(_carrier, FakeMode.Error);
await using var api = new BookstoreApiFactory(_payments, _carrier);
using var response = await CheckoutAsync(api.CreateClient());
var result = await response.Content.ReadFromJsonAsync<CheckoutResponse>(TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.True(result!.ShippingIsFlatRate);
Assert.Equal(3, QuoteRequests); // 1 deneme + 2 retry
}
[Fact]
public async Task Payment_post_is_never_retried()
{
FakePaymentProvider.Configure(_payments, FakeMode.Error);
await using var api = new BookstoreApiFactory(_payments, _carrier);
using var response = await CheckoutAsync(api.CreateClient());
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
Assert.Equal(1, ChargeRequests);
}
[Fact]
public async Task Circuit_opens_and_stops_calling_the_carrier()
{
FakeLegacyCarrier.Configure(_carrier, FakeMode.Error);
await using var api = new BookstoreApiFactory(_payments, _carrier);
var client = api.CreateClient();
for (var i = 0; i < 4; i++)
{
using var _ = await CheckoutAsync(client);
}
var before = QuoteRequests;
using var next = await CheckoutAsync(client);
var health = await client.GetStringAsync("/health", TestContext.Current.CancellationToken);
Assert.Equal(before, QuoteRequests); // açık circuit çağrıyı ağa çıkmadan kesiyor
Assert.Equal("Degraded", health);
}
Payment_post_is_never_retried bu serideki en değerli test olabilir. Bir gün biri ödeme client'ına "ne olur ne olmaz" diye bir retry ekler ya da DisableForUnsafeHttpMethods satırını siler; bu test o pull request'te kırmızıya döner. Resilience ayarları kod gibi görünmediği için code review'da kolay kaçar, testle sabitlemek gerekiyor.
Circuit testinde her factory yeni bir host, dolayısıyla yeni bir breaker demek. Breaker'ın durumu testler arasında taşınmıyor; ama aynı test içinde açtığın breaker sonraki isteklerin hepsini etkiliyor. Testleri bu yüzden bağımsız factory'lerle yazdık.
Backoff'u Beklemeden Test Etmek: FakeTimeProvider
Kargo firması iki kez 429 ve Retry-After: 2 dönüp üçüncü istekte cevap verirse checkout gerçek zamanda en az 4 saniye sürer. Polly zamanı TimeProvider üzerinden okuyor ve Microsoft.Extensions.Resilience pipeline'ları kurarken DI (Dependency Injection; bağımlılıkların dışarıdan verilmesi) container'ındaki TimeProvider'ı kullanıyor. Test, container'a FakeTimeProvider koyup saati kendisi ilerletiyor:
[Fact]
public async Task Retry_After_backoff_runs_on_fake_time()
{
// İki kez 429 + Retry-After: 2, sonra normal fiyat
_carrier.Given(Request.Create().WithPath("/api/v2/quotes").UsingGet())
.InScenario("throttle").WillSetStateTo("once")
.AtPriority(-1) // fake'in kendi fiyat kuralından önce
.RespondWith(Response.Create().WithStatusCode(429).WithHeader("Retry-After", "2"));
_carrier.Given(Request.Create().WithPath("/api/v2/quotes").UsingGet())
.InScenario("throttle").WhenStateIs("once").WillSetStateTo("twice")
.AtPriority(-1) // fake'in kendi fiyat kuralından önce
.RespondWith(Response.Create().WithStatusCode(429).WithHeader("Retry-After", "2"));
var time = new FakeTimeProvider(DateTimeOffset.UtcNow);
await using var api = new BookstoreApiFactory(_payments, _carrier, time);
var client = api.CreateClient();
var started = TimeProvider.System.GetTimestamp();
var checkout = CheckoutAsync(client);
while (!checkout.IsCompleted)
{
await Task.Delay(TimeSpan.FromMilliseconds(20), TestContext.Current.CancellationToken);
time.Advance(TimeSpan.FromMilliseconds(500));
}
using var response = await checkout;
var result = await response.Content.ReadFromJsonAsync<CheckoutResponse>(TestContext.Current.CancellationToken);
var realTime = TimeProvider.System.GetElapsedTime(started);
TestContext.Current.TestOutputHelper?.WriteLine($"quote requests={QuoteRequests}, flat rate={result?.ShippingIsFlatRate}, real time={realTime.TotalMilliseconds:0} ms");
Assert.False(result!.ShippingIsFlatRate);
Assert.Equal(3, QuoteRequests);
Assert.True(realTime < TimeSpan.FromSeconds(2), $"took {realTime}");
}
passed Bookstore.Api.Tests.CheckoutResilienceTests.Retry_After_backoff_runs_on_fake_time (393ms)
quote requests=3, flat rate=False, real time=264 ms
İki tane 2 saniyelik bekleme 264 milisaniyede bitti. Döngünün saati küçük adımlarla ilerletmesinin bir nedeni var: aynı TimeProvider 2 saniyelik attempt timeout'u ve 6 saniyelik total timeout'u da yönetiyor. Saati tek seferde 10 saniye ileri sararsan istek daha cevap gelmeden timeout'a düşer ve test yanlış bir şeyi doğrular. Zamanı test etmenin genel tekniklerini Unit Test serisinin zor durumlar yazısında anlattık.
WireMock'ta bir tuzağa düştüm: kuralları ilk denemede AtPriority(1) ile ekledim ve test ilk istekte 200 aldı. WireMock.Net'te küçük sayı yüksek öncelik demek ve fake'in kendi kuralı varsayılan öncelikte olduğu için önce o eşleşiyordu. İlk yazıda bulduğumuz "request builder'lar mutable" tuzağıyla birlikte, fake'lerle çalışırken en çok zaman kaybettiren iki ayrıntı bunlar.
Ödeme Sağlayıcısı %20 Hata Verirken
Plan sorusu şuydu: sağlayıcı isteklerin beşte birine 500 dönerken checkout ne yapıyor? Fake'i sabit bir seed ile rastgele hata verecek şekilde kurup 100 checkout gönderdik:
var random = new Random(42);
var failures = 0;
_payments.Reset();
_payments.Given(Request.Create().WithPath("/v1/charges").UsingPost())
.RespondWith(Response.Create().WithCallback(_ =>
{
lock (random)
{
if (random.NextDouble() < 0.2)
{
failures++;
return new ResponseMessage { StatusCode = 500 };
}
}
var ok = new ResponseMessage { StatusCode = 201 };
// ... başarılı charge gövdesi
return ok;
}));
passed Bookstore.Api.Tests.CheckoutResilienceTests.Checkout_with_twenty_percent_payment_failures (233ms)
responses: 200 x 69, 503 x 31; provider 500s: 31; charge requests: 100
Test üç şeyi doğruluyor: hiçbir yanıt 500 değil (hatalar kendi hata modelimizden geçip 503 oluyor), 503 sayısı sağlayıcının 500 sayısına eşit ve sağlayıcı tam 100 istek görmüş, yani hata altında bile POST tekrar denenmemiş. Seed 42 ile 100 denemede 31 hata çıktı; oran %20 civarında ama tam değil. Sabit seed testi tekrarlanabilir kılıyor, sayıyı değil davranışı doğruladığımız için de seed değişse test kırılmıyor.
Polly Chaos Strategy'leri
Fake'le test etmek, dış servisin davranışını kontrol edebildiğin bir ortam ister. Staging'de ya da gerçek bir entegrasyon ortamında bu kontrol yok; orada hatayı client tarafında, pipeline'ın içinde üretmek gerekiyor. Polly 8.3 ile eski Simmy kütüphanesi Polly'ye katıldı ve chaos strategy'leri Polly.Core'un parçası oldu: AddChaosFault exception, AddChaosOutcome sahte sonuç, AddChaosLatency gecikme, AddChaosBehavior istediğin bir davranış enjekte ediyor. Kargo pipeline'ının en içine iki tanesini ekledik:
// Chaos strategy'leri en sonda (en içte). Config ne derse desin Production'da asla aktif değil
var chaos = context.ServiceProvider.GetRequiredService<IOptionsMonitor<ChaosOptions>>();
var environment = context.ServiceProvider.GetRequiredService<IHostEnvironment>();
ValueTask<bool> ChaosEnabled() =>
ValueTask.FromResult(!environment.IsProduction() && chaos.Get(LegacyCarrierClient.ClientName).Enabled);
pipeline
.AddChaosLatency(new ChaosLatencyStrategyOptions
{
EnabledGenerator = _ => ChaosEnabled(),
InjectionRateGenerator = _ => ValueTask.FromResult(chaos.Get(LegacyCarrierClient.ClientName).LatencyRate),
LatencyGenerator = _ => ValueTask.FromResult(chaos.Get(LegacyCarrierClient.ClientName).Latency)
})
.AddChaosOutcome(new ChaosOutcomeStrategyOptions<HttpResponseMessage>
{
EnabledGenerator = _ => ChaosEnabled(),
InjectionRateGenerator = _ => ValueTask.FromResult(chaos.Get(LegacyCarrierClient.ClientName).FaultRate),
OutcomeGenerator = _ => ValueTask.FromResult<Outcome<HttpResponseMessage>?>(
Outcome.FromResult(new HttpResponseMessage(HttpStatusCode.ServiceUnavailable)))
});
Burada üç karar var. Chaos strategy'leri pipeline'ın en içinde, çünkü dışındaki retry, breaker ve timeout'un enjekte edilen hataya gerçek bir hataymış gibi tepki vermesini istiyoruz; Polly'nin dokümanı da bu sırayı öneriyor. İkincisi, chaos strategy'leri eklendikleri an varsayılan olarak aktif ve varsayılan InjectionRate 0,001; yani "ekledim ama rate vermedim" dediğin kod binde bir hata üretiyor. Biz EnabledGenerator ile açıkça kontrol ediyoruz. Üçüncüsü, ayarlar IOptionsMonitor ile her çağrıda okunuyor; staging'de bir config değişikliğiyle chaos'u restart etmeden açıp kapatabiliyorsun.
API'yi --Chaos:LegacyCarrier:Enabled=true --Chaos:LegacyCarrier:FaultRate=0.2 ile başlatıp 50 checkout gönderdim:
50 "shippingIsFlatRate":false
14 EventName: 'Chaos.OnOutcome'
14 EventName: 'OnRetry'
On dört sahte 503 üretildi, her biri bir retry tetikledi ve hiçbir checkout sabit tarifeye düşmedi: bir isteğin üç denemesinin üçünün de %20 ile hata alma ihtimali yaklaşık binde 8. Bu, "retry ayarlarımız %20'lik bir hata oranını kullanıcıya göstermeden karşılıyor" cümlesinin ölçülmüş hâli. Aynı ayarlarla --environment Production verince Chaos.OnOutcome sayısı 0 kaldı.
Chaos outcome'ın bir özelliğini de bir testle sabitledik: sahte sonuç ağa hiç çıkmıyor.
[Fact]
public async Task Chaos_faults_on_the_carrier_never_reach_the_network()
{
await using var api = new BookstoreApiFactory(_payments, _carrier, settings: new Dictionary<string, string?>
{
["Chaos:LegacyCarrier:Enabled"] = "true",
["Chaos:LegacyCarrier:FaultRate"] = "1.0"
});
using var response = await CheckoutAsync(api.CreateClient());
var result = await response.Content.ReadFromJsonAsync<CheckoutResponse>(TestContext.Current.CancellationToken);
Assert.True(result!.ShippingIsFlatRate);
Assert.Equal(0, QuoteRequests);
}
Bu, chaos'u gerçek bir sağlayıcıya karşı kullanırken önemli: sağlayıcıya ek yük bindirmeden kendi tarafının davranışını ölçüyorsun. Tersi de geçerli: chaos ağ katmanını, DNS'i ya da TLS'i (Transport Layer Security; HTTPS bağlantısını şifreleyen protokol) test etmez; onlar için fake ya da gerçek bir kesinti senaryosu gerekir.
Gözlem: Polly'nin Log'ları ve Metric'leri
Polly her strategy olayını hem log'a hem Polly adlı meter'a yazıyor. API'ye OpenTelemetry ekledik ve geliştirme ortamında console exporter'a bağladık:
// Polly ve HttpClient metric ve span yayınlıyor; burada console'a, production'da bir OTLP collector'a gidiyor
if (builder.Configuration.GetValue<bool>("Telemetry:ConsoleExporter"))
{
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics => metrics
.AddMeter("Polly", "System.Net.Http")
.AddConsoleExporter((_, reader) => reader.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 10_000))
.WithTracing(tracing => tracing
.AddSource("Microsoft.AspNetCore", "System.Net.Http")
.AddConsoleExporter());
}
OTLP (OpenTelemetry Protocol; telemetri verisini collector'a göndermek için kullanılan protokol) yerine console seçmemiz yalnız örnek için. Chaos çalıştırmasında exporter'ın yazdığı metric'ler:
Metric Name: http.client.active_requests
Metric Name: http.client.open_connections
Metric Name: http.client.request.duration
Metric Name: http.client.request.time_in_queue
Metric Name: resilience.polly.pipeline.duration
Metric Name: resilience.polly.strategy.attempt.duration
Metric Name: resilience.polly.strategy.events
Metric Name: resilience.polly.strategy.events, Description: Tracks the number of resilience events that occurred in resilience strategies., Metric Type: LongSum
Instrumentation scope (Meter):
Name: Polly
Version: 8.9.1
(2026-11-14T09:24:16.8300100Z, 2026-11-14T09:24:21.9080350Z] event.name: Chaos.OnOutcome event.severity: Information pipeline.instance: pipeline.name: legacy-carrier-carrier strategy.name: Chaos.Outcome
Value: 14
(2026-11-14T09:24:16.8300100Z, 2026-11-14T09:24:21.9080350Z] error.type: 503 event.name: OnRetry event.severity: Warning pipeline.instance: pipeline.name: legacy-carrier-carrier strategy.name: Retry
Value: 14
resilience.polly.strategy.events bu yazının en önemli metric'i. event.name tag'i OnRetry, OnCircuitOpened, OnTimeout, OnFallback, OnHedging gibi değerler alıyor, pipeline.name hangi client'ın olduğunu söylüyor. System.Net.Http meter'ı ise .NET 8'den beri her dış çağrının süresini server.address ve http.response.status_code tag'leriyle veriyor. Span tarafında her denemenin ayrı bir client span'ı var; aynı kargo isteğinin üç denemesi trace'te üç ayrı span olarak görünüyor ve URL'nin query kısmı span'da da maskeli:
Activity.DisplayName: GET
Activity.Kind: Client
Activity.Tags:
http.request.method: GET
server.address: localhost
server.port: 9092
url.full: http://localhost:9092/api/v2/quotes?*
http.response.status_code: 200
Metric'leri test etmek de mümkün. Microsoft.Extensions.Diagnostics.Testing paketindeki MetricCollector, adı verilen meter'ı dinliyor:
// Polly resilience event'lerini "Polly" meter'ında yayınlar
using var events = new MetricCollector<int>(null, "Polly", "resilience.polly.strategy.events");
await using var api = new BookstoreApiFactory(_payments, _carrier);
using var _ = await api.CreateClient().PostAsJsonAsync("/api/orders/1001/checkout",
new { cardToken = "tok_visa" }, TestContext.Current.CancellationToken);
var snapshot = events.GetMeasurementSnapshot();
Assert.Equal(2, snapshot.Count(m => m.Tags["event.name"] is "OnRetry"));
Assert.Equal(1, snapshot.Count(m => m.Tags["event.name"] is "OnFallback"));
passed Bookstore.Api.Tests.ResilienceTelemetryTests.Retries_and_fallbacks_show_up_in_polly_metrics (624ms)
OnRetry: event.severity=Warning, pipeline.name=legacy-carrier-carrier, strategy.name=Retry
OnRetry: event.severity=Warning, pipeline.name=legacy-carrier-carrier, strategy.name=Retry
OnFallback: event.severity=Warning, pipeline.name=shipping-quote, strategy.name=Fallback
Meter process genelinde paylaşıldığı için iki test class'ı paralel çalışırsa sayılar karışıyor. Test projesine parallelizeTestCollections: false içeren bir xunit.runner.json ekledik. Bütün testlerin sonucu:
dotnet test --project tests/Bookstore.Api.Tests --output detailed
total: 7
failed: 0
succeeded: 7
duration: 5s 142ms
Dış Servis İçin Dashboard ve Alarm
Tahmin sorusundaki sessiz arıza, fallback'in iyi çalışmasının bedeli: kullanıcı bir şey görmüyor, sabit tarife sessizce para kaybettiriyor. Bunu yakalamak için her dış servis için birkaç metric ve iki alarm yeterli. Dashboard'da servis başına, pipeline.name ve server.address ile ayrılmış şu paneller olsun: http.client.request.duration'dan p50 ve p99, status code'a göre istek sayısı, OnRetry sayısının toplam isteğe oranı ve OnFallback sayısı. Sağlayıcının SLA'sında (Service Level Agreement; sağlayıcının taahhüt ettiği hizmet seviyesi) "aylık %99,9 erişilebilirlik" yazıyorsa, bunu kendi tarafında ölçtüğün tek yer bu panel; sözleşme görüşmesinde de elindeki tek veri.
Alarmlar için iki kural öneriyorum. Birincisi, OnCircuitOpened event'i 0'dan büyükse alarm: breaker'ın açılması tanım gereği sağlayıcının ciddi bir sorun yaşadığı anlamına geliyor. İkincisi, OnFallback oranı belirli bir eşiği birkaç dakika boyunca aşarsa alarm; bu, breaker açılmadan da sabit tarifeye düşülen durumları yakalıyor. Retry oranını alarm yerine dashboard'da izlemek daha doğru, çünkü retry'ların işi zaten kısa süreli hataları kullanıcıdan saklamak. Metric'lerin bir backend'e nasıl gönderileceğini ve alarm kurallarının nasıl yazılacağını Observability serisinde anlatıyoruz.
Log tarafında Polly'nin Polly kategorisini Warning'de tutmak yeterli: retry, timeout ve breaker olayları Warning ya da Error seviyesinde yazılıyor, her başarılı denemenin Execution attempt satırı ise Information seviyesinde ve production'da gereksiz hacim üretir.
Üretimde Dikkat
Chaos'u production'da kontrolsüz açma. Bizim pipeline'ımız IsProduction() kontrolüyle production'da chaos'u tamamen kapatıyor. Production'da chaos deneyi yapacaksan bunu planlı, kısa süreli, düşük oranda ve Polly dokümanının önerdiği gibi belirli kullanıcı ya da tenant'larla sınırlı yap; ve deneyin sonunda kapatmayı unutma diye EnabledGenerator'ı bir süre sınırına bağla.
Testlerdeki fake'ler gerçek sağlayıcı kadar iyi. Sağlayıcının sandbox ortamı varsa, fake'in yanıtlarını oradan alınmış gerçek örneklerle güncel tut; ilk yazıdaki Content-Type eksikliği gibi farklar testte değil production'da ortaya çıkar.
Metric tag'lerinin kardinalitesine dikkat. pipeline.name ve server.address sabit kümeler; tag'e sipariş numarası ya da müşteri kimliği gibi değerler koyarsan metric backend'in maliyeti hızla büyür. Bu tür bilgiler log'a ve trace'e aittir.
Sık Düşülen Tuzaklar
Backoff'u gerçek zamanla test etmek. Retry süreleri büyüdükçe test süresi de büyür ve bir noktada biri testi "yavaş" diye kapatır. FakeTimeProvider ile testler milisaniyeler içinde biter.
Retry sayısını client tarafında saymak. Mock'lanmış bir handler'ın çağrı sayısı, pipeline'ın gerçekte kaç istek gönderdiğini söylemeyebilir; fake sunucunun gördüğü istek sayısını say.
Retry'ları loglamamak. "Checkout neden 4 saniye sürdü" sorusunun cevabı genelde iki retry ve bir Retry-After'dır; Polly'nin log'ları kapalıysa bu cevabı bulamazsın.
Chaos strategy'sini rate vermeden eklemek. Varsayılan rate 0,001 ve strategy varsayılan olarak aktif; production'da binde bir sahte hata üreten bir kod satırı unutulmuş olur.
Küçük Bir Deneme
★☆☆ Checkout_with_twenty_percent_payment_failures testinde seed'i değiştir (ör. 7) ve çıktıdaki 503 sayısına bak. Test neden hâlâ geçiyor?
★★☆ Kargo pipeline'ındaki chaos latency'yi LatencyRate = 0.3 ve Latency = 3 sn ile aç. 2 saniyelik attempt timeout bu gecikmeyi nasıl karşılıyor? 50 checkout içinde kaç tanesi sabit tarifeye düşüyor ve OnTimeout sayısı ne?
★★★ OnCircuitOpened event'ini dinleyen bir MetricCollector testi yaz: kargo fake'i 500 dönerken kaçıncı checkout'ta breaker'ın açıldığını assert et. Ardından ChaosOutcome ile aynı sonucu fake'i hiç değiştirmeden elde et.
Bu yazıyla Bookstore'un dış çağrıları zaman sınırlı, tekrar denenebilir, kesintide degrade olabilir ve gözlemlenebilir hâle geldi. Ama bir soru açık kaldı: ödeme POST'u timeout'a düştüğünde, para çekildi mi çekilmedi mi? Bir sonraki yazıda bu "bilinmeyen sonucu" ele alıyoruz: Idempotency-Key ile POST'u güvenle tekrar denemek, ödemenin durumunu sağlayıcıdan sorgulamak ve reconciliation.