From 1766a7a445acba7d9aea7a33edb28b3876784555 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Sat, 24 Jan 2026 18:47:14 -0600 Subject: [PATCH] Fix TestActor initialization race in parallel test startup Add synchronization mechanism to ensure TestActor.PreStart() completes before CreateInitialTestActor returns. This fixes a race condition where the TestActor's Create message hasn't been processed yet when another actor (like PingerActor) tries to send messages to it. Changes: - InternalTestActor: Add optional ManualResetEventSlim parameter that gets signaled in PreStart() when actor initialization is complete - TestKitBase.CreateInitialTestActor: Wait for the init signal before returning, ensuring the actor is fully ready This provides a deterministic fix for ParallelTestActorDeadlockSpec failures that were occurring under high parallelism on Windows CI. --- .../Internal/InternalTestActor.cs | 25 ++++++++++++++++--- src/core/Akka.TestKit/TestKitBase.cs | 18 ++++++++++--- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/core/Akka.TestKit/Internal/InternalTestActor.cs b/src/core/Akka.TestKit/Internal/InternalTestActor.cs index ce0491d1ff1..9a1a8042f00 100644 --- a/src/core/Akka.TestKit/Internal/InternalTestActor.cs +++ b/src/core/Akka.TestKit/Internal/InternalTestActor.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Concurrent; +using System.Threading; using System.Threading.Channels; using Akka.Actor; using Akka.Event; @@ -21,17 +22,35 @@ namespace Akka.TestKit.Internal; internal sealed class InternalTestActor : UntypedActor { private readonly ChannelWriter _queue; + private readonly ManualResetEventSlim? _initComplete; private TestActor.Ignore? _ignore; private AutoPilot? _autoPilot; private readonly DelegatingSupervisorStrategy _supervisorStrategy = new(); /// - /// TBD + /// Creates a new internal test actor. /// - /// TBD - public InternalTestActor(ChannelWriter queue) + /// The message queue to write received messages to. + public InternalTestActor(ChannelWriter queue) : this(queue, null) + { + } + + /// + /// Creates a new internal test actor with initialization synchronization. + /// + /// The message queue to write received messages to. + /// Event to signal when actor initialization is complete. + public InternalTestActor(ChannelWriter queue, ManualResetEventSlim? initComplete) { _queue = queue; + _initComplete = initComplete; + } + + protected override void PreStart() + { + base.PreStart(); + // Signal that actor initialization is complete + _initComplete?.Set(); } /// diff --git a/src/core/Akka.TestKit/TestKitBase.cs b/src/core/Akka.TestKit/TestKitBase.cs index 28df99051a9..190022246a1 100644 --- a/src/core/Akka.TestKit/TestKitBase.cs +++ b/src/core/Akka.TestKit/TestKitBase.cs @@ -721,17 +721,27 @@ private IActorRef CreateInitialTestActor(ActorSystem system, string name) // Fix both serialization and deadlock issues: // 1. Use isSystemService=true to skip serialization checks // 2. Use isAsync=false to create LocalActorRef synchronously (avoids RepointableActorRef deadlock) - var testActorProps = Props.Create(() => new InternalTestActor(_testState.Queue)) + // 3. Use ManualResetEventSlim to ensure PreStart completes before returning (fixes parallel init race) + using var initComplete = new ManualResetEventSlim(false); + + var testActorProps = Props.Create(() => new InternalTestActor(_testState.Queue, initComplete)) .WithDispatcher("akka.test.test-actor.dispatcher"); - + var systemImpl = system.AsInstanceOf(); // Use the new AttachChildWithAsync method to create TestActor synchronously var testActor = systemImpl.Provider.SystemGuardian.Cell.AttachChildWithAsync( - testActorProps, + testActorProps, isSystemService: true, // Skip serialization checks isAsync: false, // Create synchronously to avoid deadlock name: name); - + + // Wait for TestActor.PreStart() to complete before returning + // This ensures the actor is fully initialized and ready to receive messages + if (!initComplete.Wait(TimeSpan.FromSeconds(10))) + { + throw new TimeoutException($"TestActor '{name}' failed to initialize within 10 seconds"); + } + return testActor; }