Repository navigation
Expand file tree
/
Copy pathTwoPhaseCommit.cs
More file actions
83 lines (70 loc) · 3.1 KB
/
Copy pathTwoPhaseCommit.cs
File metadata and controls
83 lines (70 loc) · 3.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
namespace CSharpExercises.Distributed;
public enum Vote { Yes, No }
public enum TransactionOutcome { Committed, Aborted }
public interface IParticipant
{
string Name { get; }
Task<Vote> PrepareAsync(Guid transaction, CancellationToken cancellationToken);
Task CommitAsync(Guid transaction);
Task RollbackAsync(Guid transaction);
}
/// <summary>
/// Two-phase commit makes several resources agree on one outcome. Phase 1: ask everyone to prepare (make the change
/// durable but not visible) and vote. Phase 2: commit only if every vote was yes; any no, error or timeout aborts
/// everyone. The coordinator writes its decision to a log first so it can finish the job after a crash. The weakness:
/// prepared participants are stuck waiting if the coordinator dies, which is why many systems prefer sagas.
/// </summary>
public sealed class TwoPhaseCommitCoordinator(TimeSpan prepareTimeout)
{
private readonly List<string> _log = [];
public IReadOnlyList<string> Log => _log;
public async Task<TransactionOutcome> ExecuteAsync(IReadOnlyList<IParticipant> participants)
{
var transaction = Guid.NewGuid();
_log.Add("begin");
using var timeout = new CancellationTokenSource(prepareTimeout);
var votes = await Task.WhenAll(participants.Select(async p =>
{
try
{
return await p.PrepareAsync(transaction, timeout.Token).WaitAsync(timeout.Token);
}
catch (Exception)
{
return Vote.No; // a crash or timeout counts as a no
}
}));
var outcome = votes.All(v => v == Vote.Yes) ? TransactionOutcome.Committed : TransactionOutcome.Aborted;
_log.Add(outcome == TransactionOutcome.Committed ? "commit" : "abort"); // the decision point
// Phase 2 must reach everyone; a real coordinator retries until it does.
await Task.WhenAll(participants.Select(p => outcome == TransactionOutcome.Committed ? p.CommitAsync(transaction) : p.RollbackAsync(transaction)));
_log.Add("end");
return outcome;
}
}
/// <summary>An in-memory participant for demonstrations: holds staged changes until told the outcome.</summary>
public sealed class AccountParticipant(string name, decimal balance, decimal change, TimeSpan? prepareDelay = null) : IParticipant
{
private decimal? _staged;
public string Name { get; } = name;
public decimal Balance { get; private set; } = balance;
public bool HasPendingChange => _staged is not null;
public async Task<Vote> PrepareAsync(Guid transaction, CancellationToken cancellationToken)
{
if (prepareDelay is { } delay) await Task.Delay(delay, cancellationToken);
if (Balance + change < 0) return Vote.No; // would overdraw
_staged = Balance + change;
return Vote.Yes;
}
public Task CommitAsync(Guid transaction)
{
if (_staged is { } value) Balance = value;
_staged = null;
return Task.CompletedTask;
}
public Task RollbackAsync(Guid transaction)
{
_staged = null;
return Task.CompletedTask;
}
}