İLERİ
Best Practices & Anti-Patterns
Hangfire'da production sorunlarının çoğu yanlış job tasarımından kaynaklanır.
Karar Rehberi
| Durum | Öneri | Örnek veya gerekçe |
|---|---|---|
| Yeni Hangfire projesi | Uygun: Tüm pattern'ları baştan uygula | Teknik borç biriktirme |
| Mevcut projede job fail artışı | Uygun: Anti-pattern tarama | Root cause genelde burada |
| Code review checklist | Uygun: Referans olarak kullan | PR'larda quick-check |
| Hızlı PoC / spike | Uygun değil: Over-engineering | Önce çalışsın, sonra iyileştir |
| 3rd party library job'u | Uygun değil: Pattern zorlamaya gerek yok | Library kendi convention'ını takip eder |
Anti-Pattern vs Doğru Kullanım
// YANLIŞ: Büyük nesne serialize edilir, storage şişer
BackgroundJob.Enqueue<IReportService>(svc => svc.Generate(new ReportRequest
{
Data = hugeDataList, // MB'larca veri job storage'da!
Template = complexTemplate
}));
// DOĞRU: Sadece ID, job içinde veriyi çek
BackgroundJob.Enqueue<IReportService>(svc => svc.GenerateByRequestId(requestId));// YANLIŞ: HttpContext capture — job çalışırken context yok!
BackgroundJob.Enqueue(() => ProcessRequest(HttpContext.Request.Body));
// DOĞRU: Gerekli veriyi önce çıkar, sadece değeri geç
var userId = HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
BackgroundJob.Enqueue<IUserService>(svc => svc.ProcessAsync(userId));// YANLIŞ: Non-idempotent — retry'da müşteriye 2x para çekilir
public async Task ChargeCustomerAsync(int orderId)
{
var order = await _repo.GetAsync(orderId);
await _paymentGateway.Charge(order.Amount, order.CardToken);
}
// DOĞRU: Idempotency key ile korunmuş
public async Task ChargeCustomerAsync(int orderId)
{
var order = await _repo.GetAsync(orderId);
// Zaten charged ise skip
if (order.PaymentStatus == PaymentStatus.Charged)
return;
// Gateway idempotency key
var idempotencyKey = "order-charge-" + orderId;
await _paymentGateway.Charge(order.Amount, order.CardToken, idempotencyKey);
order.PaymentStatus = PaymentStatus.Charged;
await _repo.UpdateAsync(order);
}Queue İzolasyonu
// Queue tanımı (priority order)
builder.Services.AddHangfireServer(options =>
{
options.Queues = new[] { "critical", "default", "low" };
// Worker'lar önce "critical" queue'u işler
});
// Kullanım
[Queue("critical")]
public async Task ProcessPaymentAsync(int paymentId) { ... }
[Queue("low")]
public async Task SendAnalyticsAsync(int eventId) { ... }
// Veya enqueue sırasında
BackgroundJob.Enqueue<IPaymentService>(
svc => svc.ProcessAsync(paymentId), "critical");Job Timeout (CancellationToken)
// Hangfire, job method'una CancellationToken inject eder.
// Server ShutdownTimeout veya job iptal edildiğinde token cancel olur.
[AutomaticRetry(Attempts = 2)]
public async Task ImportLargeFileAsync(int fileId, CancellationToken cancellationToken)
{
// CancellationToken'ı her adımda kontrol et
foreach (var batch in GetBatches(fileId))
{
cancellationToken.ThrowIfCancellationRequested();
await ProcessBatchAsync(batch, cancellationToken);
}
}
// Custom timeout: belirli sürede bitmeyen job'ı iptal et
public async Task ProcessWithTimeoutAsync(int id, CancellationToken cancellationToken)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(TimeSpan.FromMinutes(5)); // 5 dk hard limit
await DoWorkAsync(id, cts.Token);
}Örnek: Bir SaaS'ta job argümanlarında entity nesnesi geçiliyordu. 6 ay sonra entity'ye yeni required property eklenince, queue'daki tüm eski job'lar deserialization hatası ile Failed oldu. ID-based yaklaşım bu sorunu tamamen önler.
Development'ta Shared Storage Sorunu
Bu, Hangfire kullanan hemen her ekipte yaşanan en sinir bozucu sorundur:
Klasik senaryo: Developer A bir job enqueue eder ve breakpoint koyarak debug etmeye çalışır. Ama job, aynı veritabanına bağlı Developer B'nin makinesindeki Hangfire Server tarafından alınır. A hiçbir şey göremez, B'nin konsolu beklenmedik loglarla dolar.
Neden olur? Tüm geliştiriciler aynı Hangfire storage'ını (shared dev DB) kullandığında, herhangi bir aktif Hangfire Server job'ı kapabilir. Hangfire "ilk gelen alır" prensibiyle çalışır.
Çözüm stratejileri (en iyiden en kolaya):
// ÇÖZÜM 1: Development'ta InMemory storage (EN İYİ)
if (builder.Environment.IsDevelopment())
{
builder.Services.AddHangfire(config => config.UseInMemoryStorage());
}
else
{
builder.Services.AddHangfire(config => config.UseSqlServerStorage(connectionString));
}// ÇÖZÜM 2: Developer başına schema/prefix izolasyonu
var devName = Environment.MachineName; // veya Environment.UserName
builder.Services.AddHangfire(config => config
.UseSqlServerStorage(connectionString, new SqlServerStorageOptions
{
SchemaName = "hangfire_" + devName.ToLower() // hangfire_erdem, hangfire_ali, ...
}));// ÇÖZÜM 3: Developer-specific queue + server
var devQueue = "dev-" + Environment.MachineName.ToLower();
builder.Services.AddHangfireServer(options =>
{
options.ServerName = Environment.MachineName;
options.Queues = new[] { devQueue }; // Sadece kendi queue'unu dinle
});
// Enqueue sırasında kendi queue'una gönder
BackgroundJob.Enqueue<IMyService>(svc => svc.DoWork(id), devQueue);| Çözüm | Avantaj | Dezavantaj |
|---|---|---|
| InMemory storage | Sıfır konfigürasyon, tam izolasyon | Dashboard yok, restart'ta job kaybolur |
| Schema per developer | Tam izolasyon, Dashboard çalışır | Her dev'e ayrı schema (auto-create) |
| Queue per developer | Tek DB, kolay geçiş | Queue discipline gerekir, hata riski |
Örnek: 8 kişilik bir backend ekibi shared dev DB kullanıyordu. Sprint'in ilk haftası 3 developer aynı bug'ı "job'ım çalışmıyor" diye raporladı. Çözüm: Development ortamında InMemory storage + environment check. 10 satır kod ile sorun tamamen ortadan kalktı. Integration test'ler için Docker'da izole SQL Server ayağa kaldırılıyor.