-
Notifications
You must be signed in to change notification settings - Fork 504
Expand file tree
/
Copy pathOperationIdGenerator.cs
More file actions
93 lines (84 loc) · 3.27 KB
/
Copy pathOperationIdGenerator.cs
File metadata and controls
93 lines (84 loc) · 3.27 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
84
85
86
87
88
89
90
91
92
93
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using Amazon.Util;
namespace Amazon.Lambda.DurableExecution.Internal;
/// <summary>
/// Generates deterministic operation IDs for durable operations. Each call
/// increments an internal counter and SHA-256 hashes <c>"<parentId>-<counter>"</c>
/// (or just <c>"<counter>"</c> at the root). The same workflow position
/// produces a stable, opaque ID across replays — and the human-readable step
/// name is carried separately on <c>OperationUpdate.Name</c>, so renaming a
/// step does not break replay correlation.
/// </summary>
internal sealed class OperationIdGenerator
{
private int _counter;
private readonly string _prefix;
/// <summary>
/// Creates a root-level generator.
/// </summary>
public OperationIdGenerator()
: this(parentId: null)
{
}
/// <summary>
/// Creates a child generator scoped under a parent operation. The parent
/// ID (already hashed) becomes part of the prefix, so child IDs are
/// <c>hash("<parentHash>-1")</c>, <c>hash("<parentHash>-2")</c>, etc.
/// </summary>
public OperationIdGenerator(string? parentId)
{
_counter = 0;
ParentId = parentId;
_prefix = parentId != null ? parentId + "-" : string.Empty;
}
/// <summary>
/// Gets the parent operation ID, if any.
/// </summary>
public string? ParentId { get; }
/// <summary>
/// Generates the next operation ID. The counter is pre-incremented so the
/// first ID is <c>hash("1")</c>.
/// </summary>
/// <remarks>
/// Uses <see cref="Interlocked.Increment(ref int)"/> so concurrent callers
/// (e.g. user code that wraps multiple <c>StepAsync</c> calls in
/// <c>Task.WhenAll</c> with <c>Task.Run</c>, or future <c>ParallelAsync</c>/
/// <c>MapAsync</c> branches that fan out before awaiting) cannot collide
/// on the same ID. Determinism still requires that calls happen in a
/// deterministic order — atomicity prevents duplicate IDs but not
/// reordering between replays.
/// </remarks>
public string NextId()
{
var counter = Interlocked.Increment(ref _counter);
return HashOperationId(_prefix + counter.ToString(System.Globalization.CultureInfo.InvariantCulture));
}
/// <summary>
/// SHA-256 hashes <paramref name="rawId"/> and returns a 64-char lowercase
/// hex digest. Public so tests and child-context construction can reproduce
/// the same hashing logic.
/// </summary>
public static string HashOperationId(string rawId)
{
var bytes = Encoding.UTF8.GetBytes(rawId);
var hash = SHA256.HashData(bytes);
return AWSSDKUtils.ToHex(hash, lowercase: true);
}
/// <summary>
/// Creates a child generator scoped under an operation ID from this generator.
/// </summary>
public OperationIdGenerator CreateChild(string operationId)
{
return new OperationIdGenerator(operationId);
}
/// <summary>
/// Resets the counter (used for testing only). Not safe to call concurrently
/// with <see cref="NextId"/>; tests must quiesce before resetting.
/// </summary>
internal void Reset()
{
Interlocked.Exchange(ref _counter, 0);
}
}