Created
December 30, 2025 13:53
-
-
Save moostafaa/c7b6758e49295826360c06492175bb4c to your computer and use it in GitHub Desktop.
Two phase sample implementation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| public interface ITransactionParticipant | |
| { | |
| Task<PrepareResult> PrepareAsync(Guid transactionId, TransactionContext context); | |
| Task CommitAsync(Guid transactionId); | |
| Task RollbackAsync(Guid transactionId); | |
| } | |
| public enum PrepareResult | |
| { | |
| Prepared, | |
| Aborted | |
| } | |
| public class TransactionContext | |
| { | |
| public Guid TransactionId { get; set; } | |
| public DateTime CreatedAt { get; set; } | |
| public Dictionary<string, object> Data { get; set; } = new(); | |
| } | |
| public class Order | |
| { | |
| public Guid Id { get; set; } | |
| public Guid TransactionId { get; set; } | |
| public string CustomerId { get; set; } | |
| public string Status { get; set; } | |
| public DateTime CreatedAt { get; set; } | |
| } | |
| public class ProductStock | |
| { | |
| public int Id { get; set; } | |
| public string ProductId { get; set; } | |
| public int AvailableQuantity { get; set; } | |
| public int ReservedQuantity { get; set; } | |
| } | |
| public class OrderDbContext : DbContext | |
| { | |
| public DbSet<Order> Orders { get; set; } | |
| protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseInMemoryDatabase("OrderDb"); | |
| } | |
| public class InventoryDbContext : DbContext | |
| { | |
| public DbSet<ProductStock> ProductStocks { get; set; } | |
| protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseInMemoryDatabase("InventoryDb"); | |
| } | |
| public class OrderParticipant : ITransactionParticipant | |
| { | |
| public async Task<PrepareResult> PrepareAsync(Guid transactionId, TransactionContext context) | |
| { | |
| try | |
| { | |
| using var db = new OrderDbContext(); | |
| using var transaction = await db.Database.BeginTransactionAsync(); | |
| var customerId = context.Data["CustomerId"].ToString(); | |
| var order = new Order | |
| { | |
| Id = Guid.NewGuid(), | |
| TransactionId = transactionId, | |
| CustomerId = customerId, | |
| Status = "PREPARED", | |
| CreatedAt = DateTime.UtcNow | |
| }; | |
| db.Orders.Add(order); | |
| await db.SaveChangesAsync(); | |
| await transaction.CommitAsync(); | |
| Console.WriteLine($"[Order] Transaction {transactionId} PREPARED (Order Created)"); | |
| return PrepareResult.Prepared; | |
| } | |
| catch (Exception ex) | |
| { | |
| Console.WriteLine($"[Order] Transaction {transactionId} ABORTED: {ex.Message}"); | |
| return PrepareResult.Aborted; | |
| } | |
| } | |
| public async Task CommitAsync(Guid transactionId) | |
| { | |
| using var db = new OrderDbContext(); | |
| var order = await db.Orders.FirstOrDefaultAsync(o => o.TransactionId == transactionId); | |
| if (order != null && order.Status == "PREPARED") | |
| { | |
| order.Status = "COMMITTED"; | |
| await db.SaveChangesAsync(); | |
| Console.WriteLine($"[Order] Transaction {transactionId} COMMITTED"); | |
| } | |
| else if (order == null) | |
| { | |
| Console.WriteLine($"[Order] Transaction {transactionId} not found for commit."); | |
| } | |
| } | |
| public async Task RollbackAsync(Guid transactionId) | |
| { | |
| using var db = new OrderDbContext(); | |
| var order = await db.Orders.FirstOrDefaultAsync(o => o.TransactionId == transactionId); | |
| if (order != null && order.Status == "PREPARED") | |
| { | |
| db.Orders.Remove(order); | |
| await db.SaveChangesAsync(); | |
| Console.WriteLine($"[Order] Transaction {transactionId} ROLLBACKED (Order Deleted)"); | |
| } | |
| } | |
| } | |
| public class InventoryParticipant : ITransactionParticipant | |
| { | |
| public async Task<PrepareResult> PrepareAsync(Guid transactionId, TransactionContext context) | |
| { | |
| try | |
| { | |
| using var db = new InventoryDbContext(); | |
| using var transaction = await db.Database.BeginTransactionAsync(); | |
| var productId = context.Data["ProductId"].ToString(); | |
| var quantity = (int)context.Data["Quantity"]; | |
| // Note: In a real SQL DB, use UPDLOCK hints to prevent race conditions here. | |
| var stock = await db.ProductStocks.FirstOrDefaultAsync(p => p.ProductId == productId); | |
| if (stock == null || stock.AvailableQuantity < quantity) | |
| { | |
| Console.WriteLine($"[Inventory] Insufficient stock for {productId}. Aborting."); | |
| return PrepareResult.Aborted; | |
| } | |
| stock.AvailableQuantity -= quantity; | |
| stock.ReservedQuantity += quantity; | |
| await db.SaveChangesAsync(); | |
| await transaction.CommitAsync(); | |
| Console.WriteLine($"[Inventory] Transaction {transactionId} PREPARED (Stock Reserved)"); | |
| return PrepareResult.Prepared; | |
| } | |
| catch (Exception ex) | |
| { | |
| Console.WriteLine($"[Inventory] Transaction {transactionId} ABORTED: {ex.Message}"); | |
| return PrepareResult.Aborted; | |
| } | |
| } | |
| public async Task CommitAsync(Guid transactionId) | |
| { | |
| Console.WriteLine($"[Inventory] Transaction {transactionId} COMMITTED (Reservation Finalized)"); | |
| await Task.CompletedTask; | |
| } | |
| public async Task RollbackAsync(Guid transactionId) | |
| { | |
| Console.WriteLine($"[Inventory] Transaction {transactionId} ROLLBACKED (Stock Restored - Logic requires persistence to be perfect)"); | |
| await Task.CompletedTask; | |
| } | |
| } | |
| public class TwoPhaseCommitCoordinator | |
| { | |
| private readonly List<ITransactionParticipant> _participants; | |
| public TwoPhaseCommitCoordinator(List<ITransactionParticipant> participants) | |
| { | |
| _participants = participants; | |
| } | |
| public async Task<bool> ExecuteTransactionAsync(TransactionContext context) | |
| { | |
| var transactionId = context.TransactionId; | |
| var preparedParticipants = new List<ITransactionParticipant>(); | |
| Console.WriteLine($"========================================"); | |
| Console.WriteLine($"[Coordinator] Starting PREPARE phase for {transactionId}"); | |
| foreach (var participant in _participants) | |
| { | |
| var result = await participant.PrepareAsync(transactionId, context); | |
| if (result == PrepareResult.Aborted) | |
| { | |
| Console.WriteLine($"[Coordinator] PREPARE FAILED for {transactionId}. Initiating Rollback..."); | |
| await AbortAsync(transactionId, preparedParticipants); | |
| return false; | |
| } | |
| preparedParticipants.Add(participant); | |
| } | |
| Console.WriteLine($"[Coordinator] All participants PREPARED for {transactionId}"); | |
| Console.WriteLine($"[Coordinator] Starting COMMIT phase for {transactionId}"); | |
| try | |
| { | |
| foreach (var participant in _participants) | |
| await participant.CommitAsync(transactionId); | |
| Console.WriteLine($"[Coordinator] Transaction {transactionId} COMMITTED successfully"); | |
| Console.WriteLine($"========================================"); | |
| return true; | |
| } | |
| catch (Exception ex) | |
| { | |
| Console.WriteLine($"[Coordinator] COMMIT FAILED: {ex.Message}"); | |
| Console.WriteLine($"[Coordinator] CRITICAL ERROR: Transaction is in doubt. Manual intervention may be required."); | |
| throw; | |
| } | |
| } | |
| private async Task AbortAsync(Guid transactionId, List<ITransactionParticipant> preparedParticipants) | |
| { | |
| foreach (var participant in preparedParticipants) | |
| { | |
| try | |
| { | |
| await participant.RollbackAsync(transactionId); | |
| } | |
| catch (Exception ex) | |
| { | |
| Console.WriteLine($"[Coordinator] Rollback failed for participant: {ex.Message}"); | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment