Ana içeriğe geç

Saga Pattern

Saga Pattern, microservice’ler arası dağıtık işlemlerde veri tutarlılığını sağlar; yanlış uygulamalar tutarsız veriye ve kurtarılamaz hatalara yol açar.


1. Dağıtık Transaction Kullanmak

Yanlış Kullanım: Microservice’ler arası two-phase commit ile transaction yönetmek.

public async Task CreateOrderAsync(OrderRequest request)
{
    using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);

    await _orderDb.CreateOrderAsync(request);     // Order DB
    await _paymentDb.ChargeAsync(request.Total);   // Payment DB - farklı servis!
    await _inventoryDb.ReserveAsync(request.Items); // Inventory DB - farklı servis!

    scope.Complete(); // Dağıtık transaction - tight coupling, performans sorunu
}

İdeal Kullanım: Saga pattern ile eventual consistency sağlayın.

public class CreateOrderSaga
{
    public async Task ExecuteAsync(OrderRequest request)
    {
        // Step 1: Sipariş oluştur
        var orderId = await _orderService.CreateAsync(request);

        try
        {
            // Step 2: Ödeme al
            await _paymentService.ChargeAsync(orderId, request.Total);

            // Step 3: Stok ayır
            await _inventoryService.ReserveAsync(orderId, request.Items);
        }
        catch (PaymentFailedException)
        {
            await _orderService.CancelAsync(orderId);
            throw;
        }
        catch (InsufficientStockException)
        {
            await _paymentService.RefundAsync(orderId);
            await _orderService.CancelAsync(orderId);
            throw;
        }
    }
}

2. Compensation Mantığını Yazmamak

Yanlış Kullanım: Hata durumunda geri alma işlemi yapmamak.

public async Task ProcessOrderAsync(Order order)
{
    await _paymentService.ChargeAsync(order);
    await _shippingService.CreateShipmentAsync(order); // Başarısız olursa ödeme geri alınmaz
    await _notificationService.SendConfirmationAsync(order);
}

İdeal Kullanım: Her adım için compensation aksiyonu tanımlayın.

public class OrderSagaStep
{
    public Func<Task> Execute { get; init; }
    public Func<Task> Compensate { get; init; }
}

public class OrderSagaOrchestrator
{
    public async Task ExecuteAsync(Order order)
    {
        var steps = new List<OrderSagaStep>
        {
            new()
            {
                Execute = () => _paymentService.ChargeAsync(order),
                Compensate = () => _paymentService.RefundAsync(order.Id)
            },
            new()
            {
                Execute = () => _inventoryService.ReserveAsync(order),
                Compensate = () => _inventoryService.ReleaseAsync(order.Id)
            },
            new()
            {
                Execute = () => _shippingService.CreateAsync(order),
                Compensate = () => _shippingService.CancelAsync(order.Id)
            }
        };

        var completedSteps = new Stack<OrderSagaStep>();

        foreach (var step in steps)
        {
            try
            {
                await step.Execute();
                completedSteps.Push(step);
            }
            catch
            {
                while (completedSteps.Count > 0)
                {
                    var compensate = completedSteps.Pop();
                    await compensate.Compensate();
                }
                throw;
            }
        }
    }
}

3. Choreography ve Orchestration Karışımı

Yanlış Kullanım: Event-driven ve command-driven yaklaşımları karıştırmak.

public class OrderService
{
    public async Task CreateAsync(Order order)
    {
        await _repository.AddAsync(order);
        await _messageBus.PublishAsync(new OrderCreated(order.Id)); // Event (choreography)
        await _paymentService.ChargeAsync(order.Total);             // Direct call (orchestration)
    }
}

İdeal Kullanım: Tutarlı bir yaklaşım seçin. Orchestration örneği:

public class OrderSagaOrchestrator : IRequestHandler<CreateOrderCommand, OrderResult>
{
    private readonly ISagaStateRepository _sagaRepo;
    private readonly IMessageBus _bus;

    public async Task<OrderResult> Handle(CreateOrderCommand request, CancellationToken ct)
    {
        var saga = new OrderSagaState(request.OrderId);
        await _sagaRepo.SaveAsync(saga);

        await _bus.SendAsync(new ChargePayment(saga.Id, request.OrderId, request.Total));
        return new OrderResult(request.OrderId, "Processing");
    }

    public async Task HandlePaymentCharged(PaymentCharged @event)
    {
        var saga = await _sagaRepo.GetAsync(@event.SagaId);
        saga.PaymentCompleted = true;
        await _bus.SendAsync(new ReserveInventory(saga.Id, saga.OrderId));
    }

    public async Task HandlePaymentFailed(PaymentFailed @event)
    {
        var saga = await _sagaRepo.GetAsync(@event.SagaId);
        saga.Status = SagaStatus.Failed;
        await _bus.SendAsync(new CancelOrder(saga.OrderId));
    }
}

4. Saga State Yönetimi Yapmamak

Yanlış Kullanım: Saga durumunu izlememek.

public async Task ProcessAsync(Order order)
{
    await _paymentService.ChargeAsync(order);
    // Uygulama çökerse saga'nın hangi adımda olduğu bilinmez
    await _inventoryService.ReserveAsync(order);
}

İdeal Kullanım: Saga state’ini persist ederek izlenebilirlik sağlayın.

public class OrderSagaState
{
    public Guid Id { get; set; }
    public int OrderId { get; set; }
    public SagaStatus Status { get; set; }
    public bool PaymentCompleted { get; set; }
    public bool InventoryReserved { get; set; }
    public bool ShipmentCreated { get; set; }
    public DateTime StartedAt { get; set; }
    public DateTime? CompletedAt { get; set; }
    public string FailureReason { get; set; }

    public bool IsComplete => PaymentCompleted && InventoryReserved && ShipmentCreated;
}

// Saga recovery worker
public class SagaRecoveryService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            var stuckSagas = await _sagaRepo.GetStuckAsync(TimeSpan.FromMinutes(5));
            foreach (var saga in stuckSagas)
            {
                await ResumeOrCompensateAsync(saga);
            }
            await Task.Delay(TimeSpan.FromMinutes(1), ct);
        }
    }
}

5. Timeout ve Deadletter Yönetimi

Yanlış Kullanım: Saga adımlarında timeout belirlememek.

public async Task HandleOrderCreated(OrderCreated @event)
{
    await _paymentService.ChargeAsync(@event.OrderId);
    // Payment servisi yanıt vermezse saga sonsuza kadar bekler
}

İdeal Kullanım: Timeout ve deadletter queue ile takılı saga’ları yönetin.

public class SagaTimeoutManager : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            var timedOutSagas = await _sagaRepo.GetTimedOutAsync(TimeSpan.FromMinutes(2));

            foreach (var saga in timedOutSagas)
            {
                _logger.LogWarning("Saga timeout: {SagaId}, adım: {Step}", saga.Id, saga.CurrentStep);
                saga.Status = SagaStatus.TimedOut;

                await CompensateAsync(saga);
                await _deadLetterQueue.PublishAsync(new SagaTimedOut(saga.Id, saga.CurrentStep));
            }

            await Task.Delay(TimeSpan.FromSeconds(30), ct);
        }
    }
}