HHangfire Handbook

İ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-Patterns Best Practices Entity nesnesi argüman olarak geç Lambda içinde HttpContext/scoped service capture Non-idempotent job (retry'da çift işlem) Recurring job'da inline CRON string typo Sadece ID geç, job içinde DB'den çek Interface-based DI, Hangfire resolve eder Idempotency key + duplicate check Cron.Daily(9) helper veya const kullan

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.