diff --git a/src/core/Akka.Tests/Actor/TimerStartupCrashBugFixSpec.cs b/src/core/Akka.Tests/Actor/TimerStartupCrashBugFixSpec.cs new file mode 100644 index 00000000000..cead9732c08 --- /dev/null +++ b/src/core/Akka.Tests/Actor/TimerStartupCrashBugFixSpec.cs @@ -0,0 +1,97 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2024 Lightbend Inc. +// Copyright (C) 2013-2024 .NET Foundation +// +// ----------------------------------------------------------------------- + +#nullable enable +using System; +using System.Linq; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Event; +using Akka.Routing; +using Akka.TestKit; +using FluentAssertions; +using FsCheck; +using Xunit; +using Xunit.Abstractions; + +namespace Akka.Tests.Actor; + +public class TimerStartupCrashBugFixSpec : AkkaSpec +{ + public TimerStartupCrashBugFixSpec(ITestOutputHelper output) : base(output: output, Akka.Configuration.Config.Empty) + { + Sys.Log.Info("Starting TimerStartupCrashBugFixSpec"); + } + + private class TimerActor : UntypedActor, IWithTimers + { + public sealed class Check + { + public static Check Instance { get; } = new Check(); + + private Check() + { + } + } + + public sealed class Hit + { + public static Hit Instance { get; } = new Hit(); + + private Hit() + { + } + } + + private readonly ILoggingAdapter _log = Context.GetLogger(); + private int _counter = 0; + public ITimerScheduler? Timers { get; set; } = null; + + protected override void PreStart() + { + Timers?.StartPeriodicTimer("key", Hit.Instance, TimeSpan.FromMilliseconds(1)); + } + + protected override void OnReceive(object message) + { + switch (message) + { + case Check _: + _log.Info("Check received"); + Sender.Tell(_counter); + break; + case Hit _: + _log.Info("Hit received"); + _counter++; + break; + } + } + + protected override void PreRestart(Exception reason, object message) + { + _log.Error(reason, "Not restarting - shutting down"); + Context.Stop(Self); + } + } + + [Fact] + public async Task TimerActor_should_not_crash_on_startup() + { + var actors = Enumerable.Range(0, 10).Select(c => Sys.ActorOf(Props.Create(() => new TimerActor()))).ToList(); + var watchTasks = actors.Select(actor => actor.WatchAsync()).ToList(); + + var i = 0; + while (i == 0) + { + // guarantee that the actor has started and processed a message from scheduler + i = await actors[0].Ask(TimerActor.Check.Instance); + } + + + watchTasks.Any(c => c.IsCompleted).Should().BeFalse(); + } +} \ No newline at end of file diff --git a/src/core/Akka/Actor/Scheduler/HashedWheelTimerScheduler.cs b/src/core/Akka/Actor/Scheduler/HashedWheelTimerScheduler.cs index a746c03bd21..24121e73358 100644 --- a/src/core/Akka/Actor/Scheduler/HashedWheelTimerScheduler.cs +++ b/src/core/Akka/Actor/Scheduler/HashedWheelTimerScheduler.cs @@ -147,25 +147,27 @@ private static int NormalizeTicksPerWheel(int ticksPerWheel) private void Start() { - if (_workerState == WORKER_STATE_STARTED) + // only read the worker state once so it can't be a moving target for else-branch + var workerStateRead = _workerState; + if (workerStateRead == WORKER_STATE_STARTED) { // do nothing } - else if (_workerState == WORKER_STATE_INIT) + else if (workerStateRead == WORKER_STATE_INIT) { if (Interlocked.CompareExchange(ref _workerState, WORKER_STATE_STARTED, WORKER_STATE_INIT) == WORKER_STATE_INIT) { _timer ??= new PeriodicTimer(_timerDuration); - Task.Run(() => RunAsync(_cts.Token)); // start the clock + Task.Run(() => RunAsync(_cts.Token).ConfigureAwait(false)); // start the clock } } - else if (_workerState == WORKER_STATE_SHUTDOWN) + else if (workerStateRead == WORKER_STATE_SHUTDOWN) { throw new SchedulerException("cannot enqueue after timer shutdown"); } else { - throw new InvalidOperationException($"Worker in invalid state: {_workerState}"); + throw new InvalidOperationException($"Worker in invalid state: {workerStateRead}"); } if(_startTime == 0)