Skip to content

Fix scheduler race in BackgroundJobManagerActor startup reconciliation - #1417

Merged
Aaronontheweb merged 1 commit into
netclaw-dev:devfrom
Aaronontheweb:fix/background-job-reconcile-prestart-race
Jun 16, 2026
Merged

Fix scheduler race in BackgroundJobManagerActor startup reconciliation#1417
Aaronontheweb merged 1 commit into
netclaw-dev:devfrom
Aaronontheweb:fix/background-job-reconcile-prestart-race

Conversation

@Aaronontheweb

Copy link
Copy Markdown
Collaborator

Summary

  • Self.Tell(Reconcile.Instance) in PreStart raced on slow schedulers (macOS CI): ActorOf returns the ActorRef before PreStart executes, so external messages could arrive in the user mailbox ahead of the Reconcile message
  • Fixed by calling HandleReconcile() synchronously in PreStart — Akka.NET guarantees PreStart completes before any user message is dispatched, eliminating the race entirely
  • Removed the now-dead Reconcile record, its Receive<> handler, and _startupSchemaAlertsEmitted guard

Root cause

Four tests failed exclusively on macos-26 (job 81675499970):

Test Symptom
StartupReconciliation_MarksOrphanedJobsAsLost Health barrier resolved before reconciliation ran; store still showed Running
StartupReconciliation_EmitsAlert_ForLegacyJobMissingTrustFields Alert sink was empty when checked; emit happened during actor teardown
BackgroundJob_WithMissingWorkingDirectory_FailsWithHelpfulError Freshly-started job reconciled as Lost before the execution actor ran
CancelRunningJob_ViaCheckBackgroundJobTool Same race; delivered "was lost" instead of "cancelled"

The macOS CI workers have a different thread scheduler that delays actor dispatch enough for the test body to enqueue messages before PreStart executes.

Test plan

  • All four previously-failing tests now pass locally
  • Full Netclaw.Actors.Tests suite passes (2196 tests)
  • dotnet slopwatch analyze — no new violations
  • Copyright headers verified

@Aaronontheweb
Aaronontheweb enabled auto-merge (squash) June 16, 2026 15:00
Self.Tell(Reconcile.Instance) in PreStart races on slow schedulers (macOS
CI): ActorOf returns before PreStart executes, allowing external messages to
arrive and queue in the user mailbox ahead of the Reconcile message. This
caused StartBackgroundJob commands and GetBackgroundJobManagerHealth checks
to be processed before reconciliation, producing four flaky test failures
exclusive to macos-26:

- StartupReconciliation_MarksOrphanedJobsAsLost: health barrier resolved
  before reconciliation ran, so store still showed Running
- StartupReconciliation_EmitsAlert_ForLegacyJobMissingTrustFields: same —
  sink was empty when checked, alert emitted during actor teardown
- BackgroundJob_WithMissingWorkingDirectory_FailsWithHelpfulError: freshly
  started job was reconciled as Lost before the execution actor ran
- CancelRunningJob_ViaCheckBackgroundJobTool: same reconcile-before-execute
  race; delivered 'was lost' instead of 'cancelled'

Fix: call HandleReconcile() synchronously in PreStart. Akka.NET guarantees
PreStart completes before any user message is dispatched, eliminating the
race entirely. Remove the now-dead Reconcile record, its Receive<> handler,
and the _startupSchemaAlertsEmitted guard (which only guarded against a
re-entrant path that no longer exists).
@Aaronontheweb
Aaronontheweb force-pushed the fix/background-job-reconcile-prestart-race branch from 2bc7004 to d6242f8 Compare June 16, 2026 15:00
@Aaronontheweb
Aaronontheweb merged commit dd01e79 into netclaw-dev:dev Jun 16, 2026
15 checks passed
Aaronontheweb added a commit that referenced this pull request Aug 1, 2026
…iation test (#1731)

StartupReconciliation_DeliversLostNotificationToOwningSession was flaky
on Windows CI: the manager created in ConfigureAkka runs its PreStart
reconciliation asynchronously on the dispatcher, and if it landed after
the orphan was persisted but before its output.log was written, it
delivered a 'lost' notification with no output path (NotifyLostJob
swallows FileNotFound and nulls OutputFilePath) — so Assert.Contains
(logPath, delivery.Content) failed.

PreStart completes before any user message dispatches, so a health
barrier against the setup manager before seeding the orphan proves it
reconciled an empty store. Only the freshly created manager (after the
log write) reconciles the orphan, so the path is always present.

Fixes the race deterministically (no timeout changes); matches the
readiness-barrier idiom from #1410/#1417.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant