Skip to content

De-flake RemotingTerminatorSpecs: don't depend on the best-effort graceful Terminated - #8622

Merged
Aaronontheweb merged 1 commit into
akkadotnet:devfrom
Aaronontheweb:fix/remotingterminatorspecs-watch-fd
Sep 24, 2026
Merged

Aaronontheweb merged 1 commit into
akkadotnet:devfrom
Aaronontheweb:fix/remotingterminatorspecs-watch-fd

Conversation

@Aaronontheweb

Copy link
Copy Markdown
Member

RemotingTerminatorSpecs.RemotingTerminator_should_shutdown_properly_with_remotely_deployed_actor failed on a 2-vCPU Windows agent (in a build of AOT-stack PR #8613, which doesn't touch remoting): it timed out after 10 s waiting for the remote-deployed actor's Terminated after System2.Terminate().

Cause

There are two ways the deployer can learn that the remote-deployed actor died.

  • Fast path: a DeathWatchNotification sent over the association while System2 shuts down gracefully. The ordering is correct: the child notifies remote watchers first, and the notification reaches the EndpointManager before ShutdownAndFlush. But delivery is best-effort:
    • ShutdownAndFlush writes whatever is pending and stops the writer.
    • The resend buffer is dropped without waiting for acks (Endpoint.cs:776-786: "don't know if they were properly delivered").
    • TcpAssociationHandle.Write doesn't await the DotNetty write before the transport closes its channels.
  • Guaranteed path: the watch failure detector raises AddressTerminated, after acceptable-heartbeat-pause (10 s by default) plus the heartbeat interval.

In the failing run the fast path's notice never arrived. The failure detector fired about 14 s after shutdown began, past the test's 10 s window. The test had been widened once before, from 3 s to 10 s (#8328), which only made the window the same size as the pause.

I didn't find a product bug in the terminator's ordering. Classic remoting has no acked flush on shutdown, and I believe classic JVM Akka behaves the same way (not verified).

Change

  • akka.remote.watch-failure-detector.acceptable-heartbeat-pause = 2s in this spec's config. When the fast notice is lost, the guaranteed path now lands well inside the 10 s windows.
  • WatchRemoteDeployedAsync runs WatchAsync, then an Identify round trip with the local /system/remote-watcher. TestKit.Watch only waits for the TestActor. This round trip makes sure the RemoteWatcher has sent its remote Watch before the test's Identify confirms the association, which closes a smaller ordering race.
  • Both tests with this deploy, watch and terminate shape get the change. The sibling ..._without_exception_logging_while_graceful_shutdown has the same exposure.

Verification (local)

  • RemotingTerminatorSpecs passes 4/4 across 3 runs, 3 s each.

…rom a graceful shutdown

RemotingTerminator_should_shutdown_properly_with_remotely_deployed_actor
waited 10 s for the remote-deployed actor's Terminated after the deployed
system terminated. That Terminated normally arrives in milliseconds over the
graceful disassociation, but the delivery is best-effort: ShutdownAndFlush
writes what's pending and closes without waiting for system-message acks.
When it's lost (as on a 2-vCPU Windows agent), only the watch failure
detector reports the death - after its 10 s acceptable pause plus the
heartbeat interval, i.e. past the 10 s window (~14 s observed).

- akka.remote.watch-failure-detector.acceptable-heartbeat-pause = 2s for
  this spec, so the guaranteed path lands well inside the 10 s windows.
- WatchRemoteDeployedAsync: WatchAsync, then an Identify round trip with the
  local RemoteWatcher, so the remote Watch is on the wire before the
  Identify used to confirm the association (TestKit.Watch only waits for the
  TestActor, not the RemoteWatcher).

Both tests with this shape get the change.

@Aaronontheweb Aaronontheweb left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

private async Task WatchRemoteDeployedAsync(IActorRef remoteDeployed)
{
await WatchAsync(remoteDeployed);
await Sys.ActorSelection("/system/remote-watcher").Ask<ActorIdentity>(new Identify(null), RemainingOrDefault);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea - the watch must have gone through in order for this to pass

@Aaronontheweb
Aaronontheweb merged commit a4b2383 into akkadotnet:dev Sep 24, 2026
15 checks passed
@Aaronontheweb
Aaronontheweb deleted the fix/remotingterminatorspecs-watch-fd branch September 24, 2026 12:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant