UZMAN
Connection Resiliency & DbContext Pooling
Bulut ortamlarında geçici ağ hataları kaçınılmaz. Retry mekanizması yoksa uygulama ilk geçici hatada çöker. Connection Resiliency otomatik yeniden deneme sağlar; DbContext Pooling ise context nesne oluşturma maliyetini azaltarak throughput artırır.
Connection Resiliency (Bağlantı Dayanıklılığı)
// Program.cs — retry stratejisi
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString, sqlOptions =>
{
sqlOptions.EnableRetryOnFailure(
maxRetryCount: 5, // Maks deneme
maxRetryDelay: TimeSpan.FromSeconds(30), // Denemeler arası maks bekleme
errorNumbersToAdd: null); // Ek SQL error code'ları
}));
Özel Retry Stratejisi
// Kendi execution strategy'nizi yazabilirsiniz
public class CustomRetryStrategy : SqlServerRetryingExecutionStrategy
{
public CustomRetryStrategy(DbContext context)
: base(context, maxRetryCount: 3, maxRetryDelay: TimeSpan.FromSeconds(10),
errorNumbersToAdd: new[] { 4060, 4221 }) // Login failed, timeout
{ }
protected override bool ShouldRetryOn(Exception exception)
{
// Özel logic: sadece belirli hatalarda retry yap
if (exception is SqlException sqlEx)
{
// Deadlock (1205) ve Timeout (-2)
return sqlEx.Number is 1205 or -2;
}
return base.ShouldRetryOn(exception);
}
}
// Kullanım
options.UseSqlServer(connectionString, sql =>
sql.ExecutionStrategy(ctx => new CustomRetryStrategy(ctx)));
Manual Transaction ile Retry
// ⚠️ Retry etkin ise, transaction'lar özel handling gerektirir!
var strategy = context.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
await using var transaction = await context.Database.BeginTransactionAsync();
try
{
// İşlemler...
context.Orders.Add(order);
await context.SaveChangesAsync();
context.Inventory.Update(stock);
await context.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
});
İdempotency Kuralları
Retry mekanizması aynı işlemi tekrar çalıştırabileceği için işlemlerin idempotent olması gerekir:
| Kural | Neden |
|---|---|
| Unique constraint + upsert | Retry aynı INSERT'i tekrar çalıştırabilir |
| Idempotency key (request ID) | Aynı işlemin 2 kez uygulanmasını engeller |
| SELECT before INSERT | Retry'da "zaten var" kontrolü |
// İdempotent kayıt oluşturma
var strategy = context.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
var exists = await context.Payments
.AnyAsync(p => p.IdempotencyKey == request.RequestId);
if (!exists)
{
context.Payments.Add(new Payment
{
IdempotencyKey = request.RequestId,
Amount = request.Amount,
OrderId = request.OrderId
});
await context.SaveChangesAsync();
}
});
Retry Davranışı (Exponential Backoff)
Deneme 1 → Hata (transient) → Bekle ~1s
Deneme 2 → Hata (transient) → Bekle ~3s (exponential backoff + jitter)
Deneme 3 → Hata (transient) → Bekle ~7s
Deneme 4 → Başarılı ✓
EF retry sadece transient hatalar için çalışır. Constraint violation, syntax error gibi kalıcı hatalar anında fırlatılır.
Advanced: Built-in vs Polly Karşılaştırması
| Özellik | EF Core Built-in | Microsoft.Extensions.Resilience (Polly v8) |
|---|---|---|
| Retry | ||
| Circuit Breaker | ||
| Timeout | ||
| Fallback | ||
| Hedging (parallel request) | ||
| Bulkhead (concurrency limit) | ||
| Telemetry entegrasyonu | Temel | OpenTelemetry native |
Ne zaman Polly gerekir?
- Circuit breaker lazımsa (DB sürekli down → fast-fail, cascade engelle)
- Timeout kontrolü istiyorsan (EF retry sonsuza kadar bekleyebilir)
- Fallback stratejisi (read-replica'ya düş, cache'den oku)
- Multi-service senaryosu (HTTP + DB + Redis hepsi aynı pipeline'da)
// NuGet: Microsoft.Extensions.Resilience (>= 8.0.0)
// Gereksinim: .NET 8+ ve Polly v8
// EF Core execution strategy'yi KAPAT, Polly'ye devret:
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString, sql =>
sql.ExecutionStrategy(deps => new NonRetryingExecutionStrategy(deps))));
// Polly pipeline tanımla:
builder.Services.AddResiliencePipeline("db-pipeline", pipeline =>
{
pipeline
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 5,
BackoffType = DelayBackoffType.Exponential,
ShouldHandle = new PredicateBuilder().Handle<SqlException>(ex => ex.IsTransient)
})
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(10),
MinimumThroughput = 8,
BreakDuration = TimeSpan.FromSeconds(30)
})
.AddTimeout(TimeSpan.FromSeconds(15));
});
// Kullanım:
public class OrderService
{
private readonly ResiliencePipeline _pipeline;
private readonly AppDbContext _context;
public OrderService(
[FromKeyedServices("db-pipeline")] ResiliencePipeline pipeline,
AppDbContext context)
{
_pipeline = pipeline;
_context = context;
}
public async Task<Order?> GetOrderAsync(int id)
{
return await _pipeline.ExecuteAsync(async ct =>
await _context.Orders.FindAsync([id], ct),
CancellationToken.None);
}
}
Tavsiye: Çoğu proje için EF Core'un built-in retry'ı yeterlidir. Polly'ye geçiş sadece circuit breaker veya multi-strategy ihtiyacı olduğunda değer yaratır.
DbContext Pooling
// ❌ Normal — her request'te yeni DbContext oluşturulur
builder.Services.AddDbContext<AppDbContext>(options => ...);
// ✅ Pooling — DbContext instance'ları yeniden kullanılır
builder.Services.AddDbContextPool<AppDbContext>(options =>
options.UseSqlServer(connectionString),
poolSize: 1024); // Varsayılan: 1024
Pooling Farkı
| Yöntem | Davranış | Bellek |
|---|---|---|
AddDbContext |
Her request'te yeni instance oluşturur | Her seferinde allocation + GC baskısı |
AddDbContextPool |
Havuzdan hazır instance alır, işi bitince geri koyar | Sabit havuz, GC yükü minimal |
Pooling kısıtlamaları:
- Constructor'da DI kullanamazsın (singleton gibi davranır)
- DbContext'te state tutma (her kullanımda sıfırlanır)
OnConfiguringoverride etme
PostgreSQL Connection & PgBouncer
Connection & Pooling Limitleri:
Limit PostgreSQL SQL Server Max eş zamanlı bağlantı (varsayılan) 100 32.767 Max bağlantı (artırılabilir) ~500-1000 (RAM'e bağlı) 32.767 Connection string Max Pool Size(varsayılan)100 (Npgsql) 100 DbContext Pool Size (varsayılan) 1024 1024 PostgreSQL connection limiti neden düşük?
- PG her connection için ayrı bir process fork eder (~5-10 MB RAM/connection)
- 100 bağlantı = ~1 GB RAM sadece connection'lar için
- SQL Server thread-based → binlerce bağlantı daha ucuz
Çözüm: Connection Pooler (PgBouncer / Supavisor)
Uygulama (1000 request) → PgBouncer (50 connection) → PostgreSQL (50 backend)
// Connection string'de pool ayarları:
"Host=localhost;Database=mydb;Username=app;Password=***;"
+ "Maximum Pool Size=50;" // Npgsql iç pool: max 50 bağlantı
+ "Minimum Pool Size=5;" // Başlangıçta 5 bağlantı hazır tut
+ "Connection Idle Lifetime=60;" // 60 sn boş kalan bağlantıyı kapat
+ "Timeout=30;" // Havuzda boş connection yoksa 30 sn bekle
// PgBouncer kullanılıyorsa:
// - Npgsql pool size = PgBouncer'daki pool size ile eşleş
// - "No Reset On Close=true" ekle (PgBouncer session modda)
// - Prepared statements devre dışı: "Max Auto Prepare=0"
Yaygın hata: "Npgsql.NpgsqlException: The connection pool has been exhausted"
- Sebep: Bağlantılar geri verilmiyor (DbContext Dispose edilmemiş veya long-running transaction)
- Çözüm:
usingkullan, DI lifetime doğru ayarla (Scoped), pool size artır