From cf64b8d6d470c0e220c624c58a9444324ce961c1 Mon Sep 17 00:00:00 2001 From: Oisin Grehan Date: Fri, 19 Jul 2024 17:32:54 -0400 Subject: [PATCH] Update to Orleans 8.2 and add support for RegisterGrainTimer (#163) --- src/OrleansTestKit/OrleansTestKit.csproj | 8 ++-- .../TestGrainActivationContext.cs | 6 +++ src/OrleansTestKit/TestGrainCreator.cs | 2 +- src/OrleansTestKit/Timers/TestTimer.cs | 27 +++++++++++++- .../Timers/TestTimerRegistry.cs | 24 +++++++++++- .../Grains/HelloTimers.cs | 25 +++++++++++++ .../OrleansTestKit.Tests.csproj | 2 +- test/OrleansTestKit.Tests/Tests/TimerTests.cs | 37 +++++++++++++++++++ 8 files changed, 121 insertions(+), 10 deletions(-) diff --git a/src/OrleansTestKit/OrleansTestKit.csproj b/src/OrleansTestKit/OrleansTestKit.csproj index 545b724..3784c2c 100644 --- a/src/OrleansTestKit/OrleansTestKit.csproj +++ b/src/OrleansTestKit/OrleansTestKit.csproj @@ -12,7 +12,7 @@ Orleans Cloud-Computing Actor-Model Actors Distributed-Systems C# .NET Test Testing true snupkg - 8.1.0 + 8.2.0 @@ -24,9 +24,9 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + runtime; build; native; contentfiles; analyzers all diff --git a/src/OrleansTestKit/TestGrainActivationContext.cs b/src/OrleansTestKit/TestGrainActivationContext.cs index 1ad5f65..5fadec4 100644 --- a/src/OrleansTestKit/TestGrainActivationContext.cs +++ b/src/OrleansTestKit/TestGrainActivationContext.cs @@ -27,6 +27,8 @@ public sealed class TestGrainActivationContext : IGrainContext /// public object GrainInstance { get; set; } = default!; + public void Migrate(Dictionary? requestContext, CancellationToken cancellationToken = new CancellationToken()) => throw new NotImplementedException(); + /// public GrainReference GrainReference { get; set; } = default!; @@ -62,6 +64,10 @@ public sealed class TestGrainActivationContext : IGrainContext /// public void ReceiveMessage(object message) => throw new NotImplementedException(); + public void Activate(Dictionary? requestContext, CancellationToken cancellationToken = new CancellationToken()) => throw new NotImplementedException(); + + public void Deactivate(DeactivationReason deactivationReason, CancellationToken cancellationToken = new CancellationToken()) => throw new NotImplementedException(); + /// public void Rehydrate(IRehydrationContext context) => throw new NotImplementedException(); diff --git a/src/OrleansTestKit/TestGrainCreator.cs b/src/OrleansTestKit/TestGrainCreator.cs index d204891..6c46171 100644 --- a/src/OrleansTestKit/TestGrainCreator.cs +++ b/src/OrleansTestKit/TestGrainCreator.cs @@ -36,7 +36,7 @@ public TestGrainCreator(IGrainRuntime runtime, IReminderRegistry reminderRegistr _runtime = runtime ?? throw new ArgumentNullException(nameof(runtime)); _reminderRegistry = reminderRegistry; _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(runtime)); - _contextProperty = typeof(Grain).GetProperty(GRAINCONTEXT_PROPERTYNAME, BindingFlags.Instance | BindingFlags.NonPublic); + _contextProperty = typeof(Grain).GetProperty(GRAINCONTEXT_PROPERTYNAME, BindingFlags.Instance | BindingFlags.Public); _runtimeProperty = typeof(Grain).GetProperty(RUNTIME_PROPERTYNAME, BindingFlags.Instance | BindingFlags.NonPublic); _contextPropertyBase = typeof(IGrainBase).GetProperty(GRAINCONTEXT_PROPERTYNAME, BindingFlags.Instance | BindingFlags.Public); } diff --git a/src/OrleansTestKit/Timers/TestTimer.cs b/src/OrleansTestKit/Timers/TestTimer.cs index 095bba4..37295fb 100644 --- a/src/OrleansTestKit/Timers/TestTimer.cs +++ b/src/OrleansTestKit/Timers/TestTimer.cs @@ -1,20 +1,38 @@ namespace Orleans.TestKit.Timers; +/// +/// Represents a specialization of TestTimer that is used for grain timers. +/// +public sealed class TestGrainTimer(Func asyncCallback, object state) + : TestTimer(asyncCallback, state), IGrainTimer +{ + public void Change(TimeSpan dueTime, TimeSpan period) => throw new NotSupportedException(); +} + /// /// A test timer /// -public sealed class TestTimer : IDisposable +public class TestTimer : IDisposable { private Func? _asyncCallback; + public readonly CancellationTokenSource Cts = new(); /// /// Initializes a new instance of the class. /// /// A callback function to invoke when the timer is fired /// The timer's state - public TestTimer(Func asyncCallback, object? state) => + public TestTimer(Func asyncCallback, object state) => _asyncCallback = () => asyncCallback(state); + /// + /// Initializes a new instance of the class. + /// + /// A callback function to invoke when the timer is fired + /// The timer's state + public TestTimer(Func asyncCallback, object state) => + _asyncCallback = () => asyncCallback(state, Cts.Token); + /// /// Gets a value indicating whether the timer has been disposed of /// @@ -25,6 +43,10 @@ public void Dispose() { _asyncCallback = null; IsDisposed = true; + + // used by TestGrainTimer callback only + Cts.Cancel(); + Cts.Dispose(); } /// @@ -36,3 +58,4 @@ public Task FireAsync() => ? throw new ObjectDisposedException(GetType().FullName) : _asyncCallback(); } + diff --git a/src/OrleansTestKit/Timers/TestTimerRegistry.cs b/src/OrleansTestKit/Timers/TestTimerRegistry.cs index edf63af..551290c 100644 --- a/src/OrleansTestKit/Timers/TestTimerRegistry.cs +++ b/src/OrleansTestKit/Timers/TestTimerRegistry.cs @@ -1,5 +1,4 @@ using Moq; -using Orleans.Runtime; using Orleans.Timers; namespace Orleans.TestKit.Timers; @@ -41,6 +40,7 @@ public async Task FireAllAsync() public Task FireAsync(int index) => _timers[index].FireAsync(); /// + [Obsolete] public IDisposable RegisterTimer(IGrainContext grainContext, Func asyncCallback, object? state, TimeSpan dueTime, TimeSpan period) { if (grainContext == null) @@ -49,8 +49,28 @@ public IDisposable RegisterTimer(IGrainContext grainContext, Func } Mock.Object.RegisterTimer(grainContext, asyncCallback, state, dueTime, period); - var timer = new TestTimer(asyncCallback, state); + + var timer = new TestTimer(asyncCallback, state!); _timers.Add(timer); + return timer; } + + public IGrainTimer RegisterGrainTimer(IGrainContext grainContext, Func asyncCallback, TState state, + GrainTimerCreationOptions options) + { + if (grainContext == null) + { + throw new ArgumentNullException(nameof(grainContext)); + } + + var cb = new Func((s, ct) => asyncCallback(((TState?)s)!, ct)); + + Mock.Object.RegisterGrainTimer(grainContext, asyncCallback, state, options); + + var grainTimer = new TestGrainTimer(cb, state!); + _timers.Add(grainTimer); + + return grainTimer; + } } diff --git a/test/OrleansTestKit.Tests/Grains/HelloTimers.cs b/test/OrleansTestKit.Tests/Grains/HelloTimers.cs index 4da50e8..0d67f32 100644 --- a/test/OrleansTestKit.Tests/Grains/HelloTimers.cs +++ b/test/OrleansTestKit.Tests/Grains/HelloTimers.cs @@ -2,6 +2,10 @@ public class HelloTimers : Grain, IGrainWithIntegerKey { + internal const int GrainTimer0 = 3; + + internal IGrainTimer _grainTimer0 = default!; + private IDisposable? _secretTimer; private IDisposable _timer0 = default!; @@ -16,6 +20,8 @@ public override Task OnActivateAsync(CancellationToken cancellationToken) _timer1 = RegisterTimer(_ => OnTimer1(), null, TimeSpan.Zero, TimeSpan.FromSeconds(1)); _timer2 = RegisterTimer(_ => OnTimer2(), null, TimeSpan.Zero, TimeSpan.FromSeconds(1)); + _grainTimer0 = this.RegisterGrainTimer((_, c) => OnGrainTimer0(c), null, TimeSpan.Zero, TimeSpan.FromSeconds(1)); + return base.OnActivateAsync(cancellationToken); } @@ -40,6 +46,21 @@ private Task OnSecretTimer() return Task.CompletedTask; } + private async Task OnGrainTimer0(CancellationToken cancellationToken) + { + State.GrainTimer0Fired = true; + + var delay = Task.Delay(1000, cancellationToken); + try + { + await delay; + } + catch (TaskCanceledException) + { + State.GrainTimer0Cancelled = true; + } + } + private Task OnTimer0() { State.Timer0Fired = true; @@ -74,4 +95,8 @@ public HelloTimersState() public bool Timer1Fired { get; set; } public bool Timer2Fired { get; set; } + + public bool GrainTimer0Fired { get; set; } + + public bool GrainTimer0Cancelled { get; set; } } diff --git a/test/OrleansTestKit.Tests/OrleansTestKit.Tests.csproj b/test/OrleansTestKit.Tests/OrleansTestKit.Tests.csproj index 9dd68db..009e502 100644 --- a/test/OrleansTestKit.Tests/OrleansTestKit.Tests.csproj +++ b/test/OrleansTestKit.Tests/OrleansTestKit.Tests.csproj @@ -14,7 +14,7 @@ - + diff --git a/test/OrleansTestKit.Tests/Tests/TimerTests.cs b/test/OrleansTestKit.Tests/Tests/TimerTests.cs index 3ef9efa..db450ac 100644 --- a/test/OrleansTestKit.Tests/Tests/TimerTests.cs +++ b/test/OrleansTestKit.Tests/Tests/TimerTests.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using Orleans.TestKit.Timers; using TestGrains; using Xunit; @@ -6,6 +7,39 @@ namespace Orleans.TestKit.Tests; public class TimerTests : TestKitBase { + [Fact] + public async Task ShouldFirstGrainTimerAsync() + { + // Arrange + var grain = await Silo.CreateGrainAsync(0); + + // Act + await Silo.FireTimerAsync(HelloTimers.GrainTimer0); + + // Assert + var state = Silo.State(); + state.GrainTimer0Fired.Should().BeTrue(); + state.GrainTimer0Cancelled.Should().BeFalse(); + } + + [Fact] + public async Task ShouldCancelFirstGrainTimerAsync() + { + // Arrange + var grain = await Silo.CreateGrainAsync(0); + + // Act + _ = Silo.FireTimerAsync(HelloTimers.GrainTimer0); + await Task.Delay(100); + grain._grainTimer0.Dispose(); + await Task.Delay(100); + + // Assert + var state = Silo.State(); + state.GrainTimer0Fired.Should().BeTrue(); + state.GrainTimer0Cancelled.Should().BeTrue(); + } + [Fact] public async Task ShouldFireAllTimersAsync() { @@ -19,6 +53,8 @@ public async Task ShouldFireAllTimersAsync() var state = Silo.State(); state.Timer0Fired.Should().BeTrue(); state.Timer1Fired.Should().BeTrue(); + state.Timer2Fired.Should().BeTrue(); + state.GrainTimer0Fired.Should().BeTrue(); } [Fact] @@ -36,6 +72,7 @@ public async Task ShouldFireAllTimersRepeatedlyAsync() state.Timer0Fired.Should().BeTrue(); state.Timer1Fired.Should().BeTrue(); state.Timer2Fired.Should().BeTrue(); + state.GrainTimer0Fired.Should().BeTrue(); }