From 1d48fb955d5e5b805e723df49754c6a2840a97f0 Mon Sep 17 00:00:00 2001 From: Roger Johansson Date: Sun, 3 Aug 2025 16:15:01 +0200 Subject: [PATCH] test: cover supervision restart scenarios --- .../Proto.Actor.Tests/DisposableActorTests.cs | 12 ++-- .../SupervisionTests_AlwaysRestart.cs | 58 +++++++++++++---- .../SupervisionTests_ExponentialBackoff.cs | 64 +++++++++++++++++++ 3 files changed, 116 insertions(+), 18 deletions(-) diff --git a/tests/Proto.Actor.Tests/DisposableActorTests.cs b/tests/Proto.Actor.Tests/DisposableActorTests.cs index 6d16d43c8e..6e3baf0cb2 100644 --- a/tests/Proto.Actor.Tests/DisposableActorTests.cs +++ b/tests/Proto.Actor.Tests/DisposableActorTests.cs @@ -16,10 +16,10 @@ public async Task WhenActorRestarted_DisposeIsCalled() var context = system.Root; var childMailboxStats = new TestMailboxStatistics(msg => msg is Stopped); - var disposeCalled = false; + var disposed = new TaskCompletionSource(); var strategy = new OneForOneStrategy((pid, reason) => SupervisorDirective.Restart, 0, null); - var childProps = Props.FromProducer(() => new DisposableActor(() => disposeCalled = true)) + var childProps = Props.FromProducer(() => new DisposableActor(() => disposed.TrySetResult(true))) .WithMailbox(() => UnboundedMailbox.Create(childMailboxStats)) .WithChildSupervisorStrategy(strategy); @@ -30,7 +30,7 @@ public async Task WhenActorRestarted_DisposeIsCalled() var parent = context.Spawn(props); context.Send(parent, "crash"); childMailboxStats.Reset.Wait(1000); - Assert.True(disposeCalled); + Assert.True(await disposed.Task.WaitAsync(TimeSpan.FromSeconds(1))); } [Fact] @@ -41,10 +41,10 @@ public async Task WhenActorRestarted_DisposeAsyncIsCalled() var context = system.Root; var childMailboxStats = new TestMailboxStatistics(msg => msg is Stopped); - var disposeCalled = false; + var disposed = new TaskCompletionSource(); var strategy = new OneForOneStrategy((pid, reason) => SupervisorDirective.Restart, 0, null); - var childProps = Props.FromProducer(() => new AsyncDisposableActor(() => disposeCalled = true)) + var childProps = Props.FromProducer(() => new AsyncDisposableActor(() => disposed.TrySetResult(true))) .WithMailbox(() => UnboundedMailbox.Create(childMailboxStats)) .WithChildSupervisorStrategy(strategy); @@ -55,7 +55,7 @@ public async Task WhenActorRestarted_DisposeAsyncIsCalled() var parent = context.Spawn(props); context.Send(parent, "crash"); childMailboxStats.Reset.Wait(2000); - Assert.True(disposeCalled); + Assert.True(await disposed.Task.WaitAsync(TimeSpan.FromSeconds(1))); } [Fact] diff --git a/tests/Proto.Actor.Tests/SupervisionTests_AlwaysRestart.cs b/tests/Proto.Actor.Tests/SupervisionTests_AlwaysRestart.cs index 66599a06c7..3183adbd05 100644 --- a/tests/Proto.Actor.Tests/SupervisionTests_AlwaysRestart.cs +++ b/tests/Proto.Actor.Tests/SupervisionTests_AlwaysRestart.cs @@ -11,6 +11,32 @@ public class SupervisionTestsAlwaysRestart { private static readonly Exception Exception = new("boom"); + [Fact] + public async Task AlwaysRestartStrategy_Should_RestartFailingChildOnly() + { + await using var system = new ActorSystem(); + var context = system.Root; + + var child1Started = 0; + var child2Started = 0; + var strategy = new AlwaysRestartStrategy(); + + var child1Props = Props.FromProducer(() => new ChildActor(() => child1Started++)); + var child2Props = Props.FromProducer(() => new ChildActor(() => child2Started++)); + + var parentProps = Props.FromProducer(() => new ParentActor(child1Props, child2Props)) + .WithChildSupervisorStrategy(strategy); + + var parent = context.Spawn(parentProps); + + context.Send(parent, "fail"); + + await Task.Delay(1000); + + Assert.Equal(2, child1Started); + Assert.Equal(1, child2Started); + } + [Fact] public async Task AlwaysRestartStrategy_Should_RestartChildOnEveryFailure() { @@ -48,27 +74,23 @@ public async Task AlwaysRestartStrategy_Should_RestartChildOnEveryFailure() private class ParentActor : IActor { - private readonly Props _childProps; + private readonly Props[] _childProps; + private PID[]? _children; - public ParentActor(Props childProps) + public ParentActor(params Props[] childProps) { _childProps = childProps; } - private PID? _child; - public Task ReceiveAsync(IContext context) { switch (context.Message) { case Started: - _child = context.Spawn(_childProps); + _children = _childProps.Select(context.Spawn).ToArray(); break; - default: - if (_child != null) - { - context.Forward(_child); - } + case string when _children != null: + context.Forward(_children[0]); break; } @@ -78,14 +100,26 @@ public Task ReceiveAsync(IContext context) private class ChildActor : IActor { + private readonly Action? _onStarted; + + public ChildActor(Action? onStarted = null) + { + _onStarted = onStarted; + } + public Task ReceiveAsync(IContext context) { - if (context.Message is string) + switch (context.Message) { - throw Exception; + case Started: + _onStarted?.Invoke(); + break; + case string: + throw Exception; } return Task.CompletedTask; } } } + diff --git a/tests/Proto.Actor.Tests/SupervisionTests_ExponentialBackoff.cs b/tests/Proto.Actor.Tests/SupervisionTests_ExponentialBackoff.cs index 7b8d8ffaef..21ddd503c9 100644 --- a/tests/Proto.Actor.Tests/SupervisionTests_ExponentialBackoff.cs +++ b/tests/Proto.Actor.Tests/SupervisionTests_ExponentialBackoff.cs @@ -1,4 +1,7 @@ using System; +using System.Threading.Tasks; +using Proto.Mailbox; +using Proto.TestFixtures; using Xunit; namespace Proto.Tests; @@ -24,4 +27,65 @@ public void FailureInsideWindow_IncrementsFailureCount() strategy.HandleFailure(null!, null!, rs, null!, null); Assert.Equal(3, rs.FailureCount); } + + [Fact] + public async Task ExponentialBackoffStrategy_Should_RestartChild() + { + await using var system = new ActorSystem(); + var context = system.Root; + + var childMailboxStats = new TestMailboxStatistics(msg => msg is Stopped); + var strategy = new ExponentialBackoffStrategy(TimeSpan.FromSeconds(10), TimeSpan.FromMilliseconds(50)); + + var childProps = Props.FromProducer(() => new BackoffChild()) + .WithMailbox(() => UnboundedMailbox.Create(childMailboxStats)); + + var parentProps = Props.FromProducer(() => new ParentActor(childProps)) + .WithChildSupervisorStrategy(strategy); + + var parent = context.Spawn(parentProps); + + context.Send(parent, "fail"); + childMailboxStats.Reset.Wait(5000); + + Assert.Contains(childMailboxStats.Posted, m => m is Restart); + Assert.Contains(childMailboxStats.Received, m => m is Restart); + } + + private class ParentActor : IActor + { + private readonly Props _childProps; + + public ParentActor(Props childProps) => _childProps = childProps; + + private PID? Child { get; set; } + + public Task ReceiveAsync(IContext context) + { + if (context.Message is Started) + { + Child = context.Spawn(_childProps); + } + + if (context.Message is string) + { + context.Forward(Child!); + } + + return Task.CompletedTask; + } + } + + private class BackoffChild : IActor + { + public Task ReceiveAsync(IContext context) + { + if (context.Message is string) + { + throw new Exception("boom"); + } + + return Task.CompletedTask; + } + } } \ No newline at end of file