Skip to content

Commit 64a0e05

Browse files
committed
fixes for remainders and timers
1 parent 066cf97 commit 64a0e05

16 files changed

Lines changed: 598 additions & 10 deletions

AGENTS.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@ Update guidelines:
3232
- **Call Tracking**: Resolve outgoing call identities from the Orleans context (`SourceId`/interface metadata) instead of guessing via reflection order to avoid mislabelling callers.
3333
- **Configuration Flags**: Treat `AllowAll`/`DisallowAll` on `GrainCallsBuilder` as authoritative defaults; ensure runtime checks respect the builder’s chosen baseline before reporting violations.
3434
- **Runtime Graph Telemetry**: For live graph features, have filters report observed calls to stateless worker aggregators that periodically flush to an in-memory grain; do not rely on per-request `CallHistory` alone for a global runtime graph.
35-
- **Runtime Graph Identity**: Live graph nodes must never silently fall back to the Orleans base `Grain` type or any guessed identity; use a concrete grain implementation class or a real grain interface, and fail fast if neither can be resolved.
35+
- **Runtime Graph Identity**: Live graph nodes must never silently fall back to the Orleans base `Grain` type or any guessed identity; use a concrete grain implementation class or a real grain interface, and use the explicit `UNKNOWN_CALLER` vertex when Orleans exposes neither.
36+
- **Type Identity Literals**: Do not hardcode framework type identity strings such as Orleans interface full names; resolve them from `typeof(...).FullName` or an equivalent type-safe API so renames stay correct.
3637
- **Runtime Graph API Shape**: Expose live telemetry as a graph model with explicit `Vertices` and `Edges`; Mermaid output is only a renderer for that same graph, while methods/counts/timestamps belong on edges.
37-
- **No Silent Fallbacks**: In call tracking and runtime graph code, never substitute wildcard methods, base `Grain`, reflection guesses, or history scans when exact caller identity is required; fail fast so identity bugs are visible immediately.
38+
- **No Silent Fallbacks**: In call tracking and runtime graph code, never substitute wildcard methods, base `Grain`, reflection guesses, or history scans when exact caller identity is required; `UNKNOWN_CALLER` is the only allowed unresolved caller marker.
3839
- **Filter Hot Path**: Keep Orleans call filters O(1) on request context data; avoid reflection, interface scanning, or call-history searching in the hot path so graph tracking stays cheap.
3940
- **Telemetry Filtering**: Do not track Orleans.Graph internal telemetry calls by default; expose a configuration switch to include them, and test both filtered and full-tracking modes.
4041
- **Testing Scope**: Exercise new behavior through the Orleans-hosted integration tests in `ManagedCode.Orleans.Graph.Tests`, covering both positive and negative paths to mirror real cluster flows.
4142
- **Cluster-to-Cluster Tests**: For grain-only runtime graph scenarios, add coverage that starts from a silo-side `IGrainFactory` instead of `IClusterClient`, so client-origin edges cannot hide cluster-to-cluster behavior.
43+
- **Activation-Origin Call Tests**: When fixing caller resolution for grain work that can run outside an incoming request, cover `RegisterGrainTimer`, reminder callbacks, and stateless worker activations because they exercise distinct Orleans entry points.
4244
- **Test Framework**: Use TUnit for tests and Shouldly for assertions; do not introduce FluentAssertions, so the test style stays aligned with the newer ManagedCode Orleans projects.
4345
- **Migration Releases**: For major framework/package migrations, complete three strict code-review-and-fix iterations before README polish, feature additions, commit, push, and CI verification, so release branches are hardened before publication.
4446
- **Code Style**: Use enums or constants over magic literals, keep documentation and comments in English, and avoid template placeholders—name files and types for their real domain roles.

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<AnalysisLevel>latest-recommended</AnalysisLevel>
1212
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
1313
<NoWarn>$(NoWarn);CS1591;CA1707;CA1848;CA1859;CA1873</NoWarn>
14-
<Version>10.0.2</Version>
14+
<Version>10.0.3</Version>
1515
<PackageVersion>$(Version)</PackageVersion>
1616
</PropertyGroup>
1717

Directory.Packages.props

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
1414
<PackageVersion Include="Microsoft.Orleans.Analyzers" Version="10.1.0" />
1515
<PackageVersion Include="Microsoft.Orleans.Client" Version="10.1.0" />
16+
<PackageVersion Include="Microsoft.Orleans.Reminders" Version="10.1.0" />
1617
<PackageVersion Include="Microsoft.Orleans.Runtime" Version="10.1.0" />
1718
<PackageVersion Include="Microsoft.Orleans.Sdk" Version="10.1.0" />
1819
<PackageVersion Include="Microsoft.Orleans.Serialization" Version="10.1.0" />
@@ -23,4 +24,4 @@
2324
<PackageVersion Include="Shouldly" Version="4.3.0" />
2425
<PackageVersion Include="TUnit" Version="1.47.0" />
2526
</ItemGroup>
26-
</Project>
27+
</Project>

ManagedCode.Orleans.Graph.Tests/Cluster/Grains/GrainA.cs

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,21 @@
33

44
namespace ManagedCode.Orleans.Graph.Tests.Cluster.Grains;
55

6-
public class GrainA : Grain, IGrainA
6+
public class GrainA : Grain, IGrainA, IRemindable
77
{
8+
private const int TimerOriginatedCallInput = 41;
9+
private const int ReminderOriginatedCallInput = 41;
10+
private const string ReminderOriginatedCallName = "reminder-originated-call";
11+
private static readonly TimeSpan TimerOriginatedCallDueTime = TimeSpan.FromMilliseconds(25);
12+
private static readonly TimeSpan TimerOriginatedCallPeriod = Timeout.InfiniteTimeSpan;
13+
private static readonly TimeSpan ReminderOriginatedCallDueTime = TimeSpan.FromMilliseconds(100);
14+
private static readonly TimeSpan ReminderOriginatedCallPeriod = TimeSpan.FromSeconds(1);
15+
private IGrainTimer? _timerOriginatedCall;
16+
private int? _timerOriginatedCallResult;
17+
private string? _timerOriginatedCallFailure;
18+
private int? _reminderOriginatedCallResult;
19+
private string? _reminderOriginatedCallFailure;
20+
821
public async Task<int> MethodA1(int input)
922
{
1023
return await Task.FromResult(input + 1);
@@ -40,6 +53,80 @@ public async Task<int> MethodGrainOnlyComplexFlow(int input)
4053
return await RunBranchingFlowAsync(input);
4154
}
4255

56+
public Task StartTimerOriginatedCallAsync()
57+
{
58+
_timerOriginatedCall?.Dispose();
59+
_timerOriginatedCallResult = null;
60+
_timerOriginatedCallFailure = null;
61+
62+
_timerOriginatedCall = this.RegisterGrainTimer(
63+
RunTimerOriginatedCallAsync,
64+
new GrainTimerCreationOptions
65+
{
66+
DueTime = TimerOriginatedCallDueTime,
67+
Period = TimerOriginatedCallPeriod,
68+
Interleave = true,
69+
KeepAlive = false
70+
});
71+
72+
return Task.CompletedTask;
73+
}
74+
75+
public Task<int?> GetTimerOriginatedCallResultAsync()
76+
{
77+
return Task.FromResult(_timerOriginatedCallResult);
78+
}
79+
80+
public Task<string?> GetTimerOriginatedCallFailureAsync()
81+
{
82+
return Task.FromResult(_timerOriginatedCallFailure);
83+
}
84+
85+
public async Task StartReminderOriginatedCallAsync()
86+
{
87+
await UnregisterReminderOriginatedCallAsync();
88+
89+
_reminderOriginatedCallResult = null;
90+
_reminderOriginatedCallFailure = null;
91+
92+
await this.RegisterOrUpdateReminder(
93+
ReminderOriginatedCallName,
94+
ReminderOriginatedCallDueTime,
95+
ReminderOriginatedCallPeriod);
96+
}
97+
98+
public Task<int?> GetReminderOriginatedCallResultAsync()
99+
{
100+
return Task.FromResult(_reminderOriginatedCallResult);
101+
}
102+
103+
public Task<string?> GetReminderOriginatedCallFailureAsync()
104+
{
105+
return Task.FromResult(_reminderOriginatedCallFailure);
106+
}
107+
108+
public async Task ReceiveReminder(string reminderName, TickStatus status)
109+
{
110+
if (!string.Equals(reminderName, ReminderOriginatedCallName, StringComparison.Ordinal))
111+
{
112+
return;
113+
}
114+
115+
try
116+
{
117+
_reminderOriginatedCallResult = await GrainFactory.GetGrain<IGrainB>(this.GetPrimaryKeyString())
118+
.MethodB1(ReminderOriginatedCallInput);
119+
}
120+
catch (Exception exception)
121+
{
122+
_reminderOriginatedCallFailure = $"{exception.GetType().FullName}: {exception.Message}";
123+
}
124+
finally
125+
{
126+
await UnregisterReminderOriginatedCallAsync();
127+
}
128+
}
129+
43130
private async Task<int> RunBranchingFlowAsync(int input)
44131
{
45132
var grainKey = this.GetPrimaryKeyString();
@@ -52,6 +139,38 @@ private async Task<int> RunBranchingFlowAsync(int input)
52139
.MethodE2(fromC);
53140
}
54141

142+
private async Task RunTimerOriginatedCallAsync(CancellationToken cancellationToken)
143+
{
144+
if (cancellationToken.IsCancellationRequested)
145+
{
146+
return;
147+
}
148+
149+
try
150+
{
151+
_timerOriginatedCallResult = await GrainFactory.GetGrain<IGrainB>(this.GetPrimaryKeyString())
152+
.MethodB1(TimerOriginatedCallInput);
153+
}
154+
catch (Exception exception)
155+
{
156+
_timerOriginatedCallFailure = $"{exception.GetType().FullName}: {exception.Message}";
157+
}
158+
finally
159+
{
160+
_timerOriginatedCall?.Dispose();
161+
_timerOriginatedCall = null;
162+
}
163+
}
164+
165+
private async Task UnregisterReminderOriginatedCallAsync()
166+
{
167+
var reminder = await this.GetReminder(ReminderOriginatedCallName);
168+
if (reminder is not null)
169+
{
170+
await this.UnregisterReminder(reminder);
171+
}
172+
}
173+
55174
private async Task ClearRuntimeGraphTelemetryAsync()
56175
{
57176
await Task.Delay(100);

ManagedCode.Orleans.Graph.Tests/Cluster/Grains/Interfaces/IGrainA.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,16 @@ public interface IGrainA : IGrainWithStringKey
1212
Task<int> MethodComplexFlow(int input);
1313

1414
Task<int> MethodGrainOnlyComplexFlow(int input);
15+
16+
Task StartTimerOriginatedCallAsync();
17+
18+
Task<int?> GetTimerOriginatedCallResultAsync();
19+
20+
Task<string?> GetTimerOriginatedCallFailureAsync();
21+
22+
Task StartReminderOriginatedCallAsync();
23+
24+
Task<int?> GetReminderOriginatedCallResultAsync();
25+
26+
Task<string?> GetReminderOriginatedCallFailureAsync();
1527
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace ManagedCode.Orleans.Graph.Tests.Cluster.Grains.Interfaces;
2+
3+
public interface IStatelessWorkerCallStateGrain : IGrainWithStringKey
4+
{
5+
Task ResetTimerOriginatedCallAsync();
6+
7+
Task RecordTimerOriginatedCallResultAsync(int result);
8+
9+
Task RecordTimerOriginatedCallFailureAsync(string failure);
10+
11+
Task<int?> GetTimerOriginatedCallResultAsync();
12+
13+
Task<string?> GetTimerOriginatedCallFailureAsync();
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace ManagedCode.Orleans.Graph.Tests.Cluster.Grains.Interfaces;
2+
3+
public interface IStatelessWorkerCallerGrain : IGrainWithStringKey
4+
{
5+
Task<int> CallGrainBAsync(int input);
6+
7+
Task<StatelessWorkerCallResult> CallGrainBWithActivationAsync(int input, int delayMilliseconds);
8+
9+
Task StartTimerOriginatedCallAsync();
10+
11+
Task<int?> GetTimerOriginatedCallResultAsync();
12+
13+
Task<string?> GetTimerOriginatedCallFailureAsync();
14+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace ManagedCode.Orleans.Graph.Tests.Cluster.Grains.Interfaces;
2+
3+
[GenerateSerializer]
4+
public sealed record StatelessWorkerCallResult(
5+
[property: Id(0)] int Value,
6+
[property: Id(1)] string ActivationId);
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using ManagedCode.Orleans.Graph.Tests.Cluster.Grains.Interfaces;
2+
3+
namespace ManagedCode.Orleans.Graph.Tests.Cluster.Grains;
4+
5+
public class StatelessWorkerCallStateGrain : Grain, IStatelessWorkerCallStateGrain
6+
{
7+
private int? _timerOriginatedCallResult;
8+
private string? _timerOriginatedCallFailure;
9+
10+
public Task ResetTimerOriginatedCallAsync()
11+
{
12+
_timerOriginatedCallResult = null;
13+
_timerOriginatedCallFailure = null;
14+
15+
return Task.CompletedTask;
16+
}
17+
18+
public Task RecordTimerOriginatedCallResultAsync(int result)
19+
{
20+
_timerOriginatedCallResult = result;
21+
22+
return Task.CompletedTask;
23+
}
24+
25+
public Task RecordTimerOriginatedCallFailureAsync(string failure)
26+
{
27+
_timerOriginatedCallFailure = failure;
28+
29+
return Task.CompletedTask;
30+
}
31+
32+
public Task<int?> GetTimerOriginatedCallResultAsync()
33+
{
34+
return Task.FromResult(_timerOriginatedCallResult);
35+
}
36+
37+
public Task<string?> GetTimerOriginatedCallFailureAsync()
38+
{
39+
return Task.FromResult(_timerOriginatedCallFailure);
40+
}
41+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using ManagedCode.Orleans.Graph.Tests.Cluster.Grains.Interfaces;
2+
using Orleans.Concurrency;
3+
4+
namespace ManagedCode.Orleans.Graph.Tests.Cluster.Grains;
5+
6+
[StatelessWorker]
7+
public class StatelessWorkerCallerGrain : Grain, IStatelessWorkerCallerGrain
8+
{
9+
private const int TimerOriginatedCallInput = 41;
10+
private static readonly TimeSpan TimerOriginatedCallDueTime = TimeSpan.FromMilliseconds(25);
11+
private static readonly TimeSpan TimerOriginatedCallPeriod = Timeout.InfiniteTimeSpan;
12+
private IGrainTimer? _timerOriginatedCall;
13+
14+
public async Task<int> CallGrainBAsync(int input)
15+
{
16+
return await GrainFactory.GetGrain<IGrainB>(this.GetPrimaryKeyString())
17+
.MethodB1(input);
18+
}
19+
20+
public async Task<StatelessWorkerCallResult> CallGrainBWithActivationAsync(int input, int delayMilliseconds)
21+
{
22+
await Task.Delay(delayMilliseconds);
23+
24+
var result = await GrainFactory.GetGrain<IGrainB>(this.GetPrimaryKeyString())
25+
.MethodB1(input);
26+
27+
return new StatelessWorkerCallResult(result, GrainContext.ActivationId.ToString());
28+
}
29+
30+
public async Task StartTimerOriginatedCallAsync()
31+
{
32+
_timerOriginatedCall?.Dispose();
33+
await GetStateGrain().ResetTimerOriginatedCallAsync();
34+
35+
_timerOriginatedCall = this.RegisterGrainTimer(
36+
RunTimerOriginatedCallAsync,
37+
new GrainTimerCreationOptions
38+
{
39+
DueTime = TimerOriginatedCallDueTime,
40+
Period = TimerOriginatedCallPeriod,
41+
Interleave = true,
42+
KeepAlive = false
43+
});
44+
}
45+
46+
public async Task<int?> GetTimerOriginatedCallResultAsync()
47+
{
48+
return await GetStateGrain().GetTimerOriginatedCallResultAsync();
49+
}
50+
51+
public async Task<string?> GetTimerOriginatedCallFailureAsync()
52+
{
53+
return await GetStateGrain().GetTimerOriginatedCallFailureAsync();
54+
}
55+
56+
private async Task RunTimerOriginatedCallAsync(CancellationToken cancellationToken)
57+
{
58+
if (cancellationToken.IsCancellationRequested)
59+
{
60+
return;
61+
}
62+
63+
try
64+
{
65+
var result = await GrainFactory.GetGrain<IGrainB>(this.GetPrimaryKeyString())
66+
.MethodB1(TimerOriginatedCallInput);
67+
68+
await GetStateGrain().RecordTimerOriginatedCallResultAsync(result);
69+
}
70+
catch (Exception exception)
71+
{
72+
await GetStateGrain().RecordTimerOriginatedCallFailureAsync(
73+
$"{exception.GetType().FullName}: {exception.Message}");
74+
}
75+
finally
76+
{
77+
_timerOriginatedCall?.Dispose();
78+
_timerOriginatedCall = null;
79+
}
80+
}
81+
82+
private IStatelessWorkerCallStateGrain GetStateGrain()
83+
{
84+
return GrainFactory.GetGrain<IStatelessWorkerCallStateGrain>(this.GetPrimaryKeyString());
85+
}
86+
}

0 commit comments

Comments
 (0)