HHangfire Handbook

UZMAN

Production Checklist & Docker

Production'a deploy öncesi kontrol listesi ve container-ready konfigürasyon.

Karar Rehberi

Durum Öneri Örnek veya gerekçe
Production'a ilk deploy Uygun: Tüm checklist'i uğra Güvenlik + stability
API ve worker ayrımı Uygun: Container role split Scale bağımsız
Zero-downtime deploy Uygun: Rolling update K8s maxUnavailable: 0
Tek sunucu PoC Uygun değil: Overkill docker-compose gerek yok
Development ortamı Uygun değil: InMemory yeter Container gereksiz
Production Topology API Container Enqueue only (no server) Worker Container AddHangfireServer() SQL Server hangfire schema Dashboard Separate service (optional)

Production Checklist

# Kontrol Neden
1 SchemaName ayarla Uygulama tablolarından izole
2 DisableGlobalLocks = true Multi-server uyumluluğu
3 Dashboard auth filter Hassas veri koruması
4 DisplayStorageConnectionString = false CS sızmasını önle
5 Retry policy tanımla Infinite retry önle
6 Queue isolation (critical/default/low) Priority management
7 Health check entegrasyonu Monitoring altyapısına bağla
8 Graceful shutdown timeout In-flight job'ları koru
9 Worker count tuning Resource kullanımını optimize et
10 Log level: Hangfire = Information Sorun teşhisi için
11 Job retention ayarla Storage şişmesini önle
12 DisableConcurrentExecution (recurring) Çift çalışmayı engelle

Job Retention (Storage Temizliği)

// Tamamlanan job'ların ne kadar süre saklanacağını ayarlayın
// Varsayılan: Succeeded = 1 gün, Deleted = 1 gün
// Büyük hacimli sistemlerde storage şişmesini önler

builder.Services.AddHangfire(config => config
    .UseSqlServerStorage(connectionString, new SqlServerStorageOptions
    {
        SchemaName = "hangfire",
        JobExpirationCheckInterval = TimeSpan.FromMinutes(30),  // Temizleme kontrol aralığı
    }));

// Job bazında retention (Hangfire Pro — ücretli)
// Community sürümde bu attribute YOKTUR — derleme hatası alırsınız!
// Community'de sadece global expiration (1 gün) geçerlidir, değiştirilemez.
[JobExpirationTimeout(hours: 72)]  // Sadece Hangfire Pro!
public async Task GenerateReportAsync(int reportId) { ... }

Not: Default retention (1 gün) çoğu proje için yeterlidir. Ama compliance gerektiren sistemlerde (fintech, healthcare) audit trail için daha uzun tutabilirsiniz. Storage boyutunu haftalık izleyin.

Docker Compose (Production-Ready)

docker-compose.yml
services:
  hangfire-api:
    build: .
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
      - ConnectionStrings__HangfireConnection=Server=sqlserver;Database=HangfireDb;User=sa;Password=${SQL_SA_PASSWORD};TrustServerCertificate=true
      - Hangfire__RunServer=false
    ports:
      - "8080:8080"
    depends_on:
      sqlserver:
        condition: service_healthy
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '1.0'
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      retries: 3

  hangfire-worker:
    build: .
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
      - ConnectionStrings__HangfireConnection=Server=sqlserver;Database=HangfireDb;User=sa;Password=${SQL_SA_PASSWORD};TrustServerCertificate=true
      - Hangfire__RunServer=true
      - Hangfire__WorkerCount=20
      - Hangfire__Queues=critical,default,low
    depends_on:
      sqlserver:
        condition: service_healthy
    deploy:
      replicas: 2
      resources:
        limits:
          memory: 1G
          cpus: '2.0'
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      retries: 3

  hangfire-dashboard:
    build: .
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
      - ConnectionStrings__HangfireConnection=Server=sqlserver;Database=HangfireDb;User=sa;Password=${SQL_SA_PASSWORD};TrustServerCertificate=true
      - Hangfire__RunServer=false
      - Hangfire__EnableDashboard=true
    ports:
      - "8081:8080"
    depends_on:
      sqlserver:
        condition: service_healthy
    deploy:
      resources:
        limits:
          memory: 256M
          cpus: '0.5'

  sqlserver:
    image: mcr.microsoft.com/mssql/server:2022-latest
    environment:
      - ACCEPT_EULA=Y
      - MSSQL_SA_PASSWORD=${SQL_SA_PASSWORD}  # .env dosyasından veya secret manager'dan
    volumes:
      - sqldata:/var/opt/mssql
    ports:
      - "1433:1433"
    deploy:
      resources:
        limits:
          memory: 2G
          cpus: '2.0'
    healthcheck:
      test: /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "${SQL_SA_PASSWORD}" -C -Q "SELECT 1"
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  sqldata:
# .env dosyası (git'e EKLEME! .gitignore'a ekle)
SQL_SA_PASSWORD=YourStr0ngPwd!ChangeMeInProd

Secret yönetimi: Production'da Docker Secrets, HashiCorp Vault veya cloud provider secret manager kullanın. Environment variable'larda plaintext password SADECE local development için kabul edilebilir.

Conditional Server Registration

// Program.cs — aynı codebase, farklı roller
var runServer = builder.Configuration.GetValue<bool>("Hangfire:RunServer");
var enableDashboard = builder.Configuration.GetValue<bool>("Hangfire:EnableDashboard");

builder.Services.AddHangfire(config => config
    .UseSqlServerStorage(builder.Configuration.GetConnectionString("HangfireConnection")));

if (runServer)
{
    builder.Services.AddHangfireServer(options =>
    {
        options.WorkerCount = builder.Configuration.GetValue<int>("Hangfire:WorkerCount", 20);
        options.Queues = builder.Configuration.GetSection("Hangfire:Queues")
            .Get<string[]>() ?? new[] { "default" };
        options.ShutdownTimeout = TimeSpan.FromSeconds(30);
    });
}

var app = builder.Build();

if (enableDashboard)
{
    app.UseHangfireDashboard("/hangfire", new DashboardOptions
    {
        Authorization = new[] { new HangfireAuthorizationFilter() },
        DisplayStorageConnectionString = false
    });
}

Graceful Shutdown

// Worker container SIGTERM aldığında:
// 1. Yeni job kabul etmeyi durdurur
// 2. Mevcut job'ların tamamlanmasını bekler (ShutdownTimeout kadar)
// 3. Timeout dolarsa in-flight job'lar "Processing" state'te kalır
// 4. SlidingInvisibilityTimeout sonra başka server tarafından alınır

builder.Services.AddHangfireServer(options =>
{
    options.ShutdownTimeout = TimeSpan.FromSeconds(30);  // Max bekleme
    options.StopTimeout = TimeSpan.FromSeconds(15);       // Stop sinyali timeout
});

// Kubernetes: terminationGracePeriodSeconds > ShutdownTimeout olmalı
// terminationGracePeriodSeconds: 45 (ShutdownTimeout + buffer)

Rolling Update Strategy

# Kubernetes deployment
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0    # Hiçbir zaman 0 worker olmasın
  template:
    spec:
      terminationGracePeriodSeconds: 45
      containers:
      - name: hangfire-worker
        lifecycle:
          preStop:
            exec:
              command: ["sleep", "5"]  # Load balancer drain süresi

Örnek: Deployment sırasında in-flight job kaybı: Eski pod kill ediliyor ama 2 dakikalık job devam ediyor. Çözüm: ShutdownTimeout=120s + terminationGracePeriodSeconds=135s. Pod, işi bitirene kadar bekler. Bitmezse job "Processing" state'te kalır ve SlidingInvisibilityTimeout sonra başka worker tarafından tekrar alınır (idempotent olmalı!).

Versiyon Güncellik Stratejisi

Aksiyon Aralık Araç
NuGet patch update kontrolü Haftalık Dependabot / Renovate
Hangfire changelog takibi Her minor release GitHub Releases
Breaking change testi Major öncesi Staging ortamında full regression
CompatibilityLevel yükseltme Major geçişte bir kez Tüm node'lar güncellendikten sonra

NuGet pinleme: Production'da Hangfire.Core ve Hangfire.SqlServer versiyonlarını explicit pinleyin. Floating version (*) kullanmayın — beklenmeyen breaking change riski.