Skip to content

Commit c63f7c4

Browse files
KSemenenkoCopilot
andauthored
Add grain-side status compare-and-set helper (#70)
* Add grain-side status compare-and-set helper * Update ManagedCode.Communication.Orleans/Grains/CommandIdempotencyGrain.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 9eaa9c9 commit c63f7c4

File tree

4 files changed

+74
-28
lines changed

4 files changed

+74
-28
lines changed

ManagedCode.Communication.Orleans/Grains/CommandIdempotencyGrain.cs

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,27 +27,26 @@ public Task<CommandExecutionStatus> GetStatusAsync()
2727
public async Task<bool> TryStartProcessingAsync()
2828
{
2929
// Reject concurrent executions
30-
if (state.State.Status is CommandExecutionStatus.InProgress or CommandExecutionStatus.Processing)
30+
switch (state.State.Status)
3131
{
32-
return false;
33-
}
34-
35-
if (state.State.Status is CommandExecutionStatus.Completed)
36-
{
37-
return false;
38-
}
39-
40-
// Allow retries after failures by clearing the previous outcome
41-
if (state.State.Status is CommandExecutionStatus.Failed)
42-
{
43-
state.State.Result = null;
44-
state.State.ErrorMessage = null;
45-
state.State.CompletedAt = null;
46-
state.State.FailedAt = null;
47-
}
48-
else if (state.State.Status is not CommandExecutionStatus.NotFound and not CommandExecutionStatus.NotStarted)
49-
{
50-
return false;
32+
case CommandExecutionStatus.InProgress:
33+
case CommandExecutionStatus.Processing:
34+
case CommandExecutionStatus.Completed:
35+
return false;
36+
37+
case CommandExecutionStatus.Failed:
38+
state.State.Result = null;
39+
state.State.ErrorMessage = null;
40+
state.State.CompletedAt = null;
41+
state.State.FailedAt = null;
42+
break;
43+
44+
case CommandExecutionStatus.NotFound:
45+
case CommandExecutionStatus.NotStarted:
46+
break;
47+
48+
default:
49+
return false;
5150
}
5251

5352
state.State.Status = CommandExecutionStatus.Processing;
@@ -58,6 +57,47 @@ public async Task<bool> TryStartProcessingAsync()
5857
return true;
5958
}
6059

60+
public async Task<bool> TrySetStatusAsync(CommandExecutionStatus expectedStatus, CommandExecutionStatus newStatus)
61+
{
62+
if (state.State.Status != expectedStatus)
63+
{
64+
return false;
65+
}
66+
67+
switch (newStatus)
68+
{
69+
case CommandExecutionStatus.InProgress:
70+
case CommandExecutionStatus.Processing:
71+
return await TryStartProcessingAsync();
72+
73+
case CommandExecutionStatus.Completed:
74+
await MarkCompletedAsync(state.State.Result);
75+
return true;
76+
77+
case CommandExecutionStatus.Failed:
78+
await MarkFailedAsync(state.State.ErrorMessage ?? "Status set to failed");
79+
return true;
80+
81+
case CommandExecutionStatus.NotFound:
82+
await ClearAsync();
83+
return true;
84+
85+
case CommandExecutionStatus.NotStarted:
86+
state.State.Status = CommandExecutionStatus.NotStarted;
87+
state.State.Result = null;
88+
state.State.ErrorMessage = null;
89+
state.State.StartedAt = null;
90+
state.State.CompletedAt = null;
91+
state.State.FailedAt = null;
92+
state.State.ExpiresAt = null;
93+
await state.WriteStateAsync();
94+
return true;
95+
96+
default:
97+
return false;
98+
}
99+
}
100+
61101
public async Task MarkCompletedAsync<TResult>(TResult result)
62102
{
63103
state.State.Status = CommandExecutionStatus.Completed;

ManagedCode.Communication.Orleans/Grains/ICommandIdempotencyGrain.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,12 @@ public interface ICommandIdempotencyGrain : IGrainWithStringKey
4242
/// Clears the command state from the grain.
4343
/// </summary>
4444
Task ClearAsync();
45-
}
45+
46+
/// <summary>
47+
/// Attempts to transition the command to a new status when the current status matches the expected value.
48+
/// </summary>
49+
/// <param name="expectedStatus">The status the caller believes the command currently has.</param>
50+
/// <param name="newStatus">The desired status to transition to.</param>
51+
/// <returns><c>true</c> when the transition succeeds, otherwise <c>false</c>.</returns>
52+
Task<bool> TrySetStatusAsync(CommandExecutionStatus expectedStatus, CommandExecutionStatus newStatus);
53+
}

ManagedCode.Communication.Orleans/Stores/OrleansCommandIdempotencyStore.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,12 @@ public async Task RemoveCommandAsync(string commandId, CancellationToken cancell
8888
public async Task<bool> TrySetCommandStatusAsync(string commandId, CommandExecutionStatus expectedStatus, CommandExecutionStatus newStatus, CancellationToken cancellationToken = default)
8989
{
9090
var grain = _grainFactory.GetGrain<ICommandIdempotencyGrain>(commandId);
91-
var currentStatus = await grain.GetStatusAsync();
92-
93-
if (currentStatus == expectedStatus)
91+
92+
if (await grain.TrySetStatusAsync(expectedStatus, newStatus))
9493
{
95-
await SetCommandStatusAsync(commandId, newStatus, cancellationToken);
9694
return true;
9795
}
98-
96+
9997
return false;
10098
}
10199

ManagedCode.Communication/Commands/Stores/MemoryCacheCommandIdempotencyStore.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,9 @@ private void ReleaseLockReference(string commandId, CommandLock commandLock)
107107
{
108108
if (Interlocked.Decrement(ref commandLock.RefCount) == 0)
109109
{
110-
if (_commandLocks.TryRemove(commandId, out var existingLock) && !ReferenceEquals(existingLock, commandLock))
110+
if (_commandLocks.TryGetValue(commandId, out var existingLock) && ReferenceEquals(existingLock, commandLock))
111111
{
112-
_commandLocks.TryAdd(commandId, existingLock);
112+
_commandLocks.TryRemove(new KeyValuePair<string, CommandLock>(commandId, commandLock));
113113
}
114114

115115
commandLock.Semaphore.Dispose();

0 commit comments

Comments
 (0)