Skip to content

fix(host-admission): one wake per replay kick; signal-driven daemon tests - #4167

Merged
ScriptedAlchemy merged 11 commits into
masterfrom
fix/load-flakes-2
Oct 17, 2026
Merged

ScriptedAlchemy merged 11 commits into
masterfrom
fix/load-flakes-2

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Oct 17, 2026 •

Copy link
Copy Markdown
Owner

Refs #4149

//crates/tracedecay-daemon-service:unit_test failed under the #4149 load recipe in eleven tests. One cause was a production bug. The rest were tests that raced a wall clock or shared a process-wide counter. After this branch, the whole binary passes 30 of 30 under the recipe:

env -u CC -u CXX bazel test //crates/tracedecay-daemon-service:unit_test --runs_per_test=30 \
  --local_test_jobs=20 --nocache_test_results --run_under=<taskset -c 0-7>
//crates/tracedecay-daemon-service:unit_test   PASSED in 142.3s   (failing runs: 0 of 30)

Production fix

Host-admission replay ran extra passes. One kick raised two wake signals: the worker's own Notify permit and the broker's replay request. Neither was cleared when a pass started, so an idle worker ran one or two redundant passes over the same spool. The worker now wakes only through the broker request and clears it when a pass starts (clear_replay_request in crates/tracedecay-host-admission/src/lib.rs).

  • Before: not_applicable_pending_record_does_not_back_off failed 1 to 2 in 40 with left: 3, right: 2 (the issue report).
  • The rewritten test pins the pass count.

Test fixes

Each item lists the cause, the failing-before output, and the defect the test still catches.

Test Cause Before Still fails when
profile_host_admission_replay replay tests Fixed sleeps and wall bounds issue item 2 (signal-driven rewrite, 15971cf)
bootstrap_gives_up_terminally_once_its_retry_budget_is_spent and 9 other bootstrap waits wait_bootstrap_completed raced a 1 to 10 s timer left: TimedOut, right: Completed (1/30) completion is never signalled (hangs)
idle_notification_during_probe_is_not_lost 250 ms timer around wait_idle idle notification must avoid the full grace: Elapsed(()) (1/30) mark_idle drops its notify: TIMEOUT
a_worker_ensured_off_tokio_replays_and_joins, shutting_down_off_tokio_empties_the_registry Attempt tests polled attempts on a native thread, which production never does, and raised the process-wide native_thread_writer_waits counter these tests read left: 5, right: 0 unchanged assertion
terminal_owner_waits_for_admitted_commit_to_drain A 5 s deadline is shorter than the reserves behind the client drain, so DaemonShutdownBudget::phase floored that phase at now assertion failed: !terminal_cancelled.load(Ordering::Acquire) (1/30) wait_for_idle ignores the admitted activity: fails at line 918
a_wall_exhausted_provider_seals_timed_out_and_emits_the_no_progress_terminal The 2 s wall is armed after the provider spawns. Probes showed the spawn took 0.5 to 2.6 s under the recipe, versus 7 ms alone, so production armed a zero budget and the payload contract refused the terminal exactly one no-progress terminal, left: 0, right: 1 (2/30, 3/30) the producer is dropped from the no-progress offer: left: 0, right: 1
contended_prepare_lock_times_out_with_retryable_receipt The prepare guard was dropped on first poll; a deadline firing before that poll hung the test 5 FAILED and 1 hang in 40 (a30d9e3)
a_handshake_finished_by_a_flush_wakes_the_parked_reader, a_late_listener_poll_does_not_time_out_a_prompt_handshake Assumed a slow TLS client the reader parks on the handshake (159c22b)
retained_begin_and_join_report_partial_effect_and_restart_recovers_same_operation 2 s wake wait on a restarted refresh pass that settled at 4.4 to 5.4 s 27 of 40 (3c411cf)

The wall-exhausted test now records when the attempt was marked running. When that is at or past the deadline, it asserts the branch production took: TimedOut and no terminal. Unloaded runs take the positive branch, which the break check above exercises.

Not changed

Two shutdown::orchestration tests failed once each in one later round: a_stuck_background_owner_cannot_starve_project_servers_or_store_close and a_project_server_drain_that_ignores_its_deadline_hits_the_backstop. Both test real-time phase deadlines (50 to 300 ms) under a 2 to 4 s coordinator deadline. The coordinator deadline fired first. native_runtime has no virtual clock, so a fix needs either one or a wider coordinator deadline. Neither is in this PR; they are noted on #4149.

Verification

  • bazel test //crates/tracedecay-host-admission:unit_test //crates/tracedecay-session-runtime:unit_test: passed.
  • bazel test //crates/tracedecay:unit_test --test_arg=branch_admin: passed.
  • bazel build --config=clippy on daemon-service, host-admission, session-runtime and //crates/tracedecay:unit_test: clean.
  • Merged with origin/master at 30c23ba, which includes refactor(work): run the attempt chain and mutations as writer jobs #4158. The attempt-harness change applies on top of the writer-job chain.

The profile replay worker kept two wake signals for one kick (its own
Notify permit and the broker's replay request) and cleared neither when
a pass began. A pass that served both left them pending, so the idle
worker ran one or two redundant passes over the same spool.

Refs #4149
retained_begin_and_join_report_partial_effect_and_restart_recovers_same_operation
bounded the restarted scheduler's recovery pass with a 2 s wake wait. Under
CPU starvation the pass was still running (backlog 1, no blocker) at 2 s and
settled at 4.4-5.4 s, so the test failed 27 of 40 in the loaded recipe.

The test now waits on the worker's idle notification through a new
wait_project_idle test helper, which mirrors wait_profile_idle. The receipt
read still proves the restart settled the persisted operation.
The attempt harness polled attempts on a native runtime thread, which
production never does. Their synchronous storage writes raised the
process-wide native writer-wait counter that off-Tokio tests in the same
binary assert stays flat. Poll them on the blocking pool, as
spawn_for_worktree does, and drop the serializing probe mutex.
A 5 s shutdown deadline is shorter than the reserves behind the client
drain, so that phase was floored at now and ended at the first timer
tick. Under load the tick fired before the test released its admitted
commit, and the terminal owner ran early. Use the daemon deadline.
The wall budget is armed after the provider spawns. Under CPU starvation
the spawn took up to 2.6 s of the 2 s budget, production armed nothing,
and the payload contract refused the zero-budget terminal. The double now
records when the attempt ran, and the test asserts the branch production
took instead of assuming the spawn is fast.
Bootstrap tests bounded the worker's completion with 1-10 s timers. Under
load the 60 ms retry-budget test missed its 5 s bound. Tests that expect
completion now wait on the completion signal; only the deadline test
still passes a timeout.
The test bounded wait_idle with a 250 ms timer, which a loaded runner
missed. wait_idle already returns false at its grace, so the result alone
proves the notification arrived; the grace is now long enough that a
lost notification hangs to the harness timeout.
@changeset-bot

changeset-bot Bot commented Oct 17, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: adbf091

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

2 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)

Devin Review

}
self.busy.store(true, Ordering::Release);
let _ = self.dirty.swap(false, Ordering::AcqRel);
self.broker.clear_replay_request();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Concurrent replay kick strands pending records

When a kick lands after dirty.swap but before clear_replay_request, the clear erases its broker wake. If the pass stops, dirty remains set while the worker sleeps, leaving pending records unreplayed until another kick.

Learn more

A replay kick marks dirty and sets the broker's replay request. The worker consumes dirty immediately before clearing that request. A kick between those operations sets both again, but the clear removes its request. If classify_replay_pass returns Stop or TerminalNoop, the worker exits the drain loop and waits solely on that erased request, even though dirty remains true.

Example: An old pending record yields NotApplicable. A new admission kicks replay after dirty.swap(false) but before clear_replay_request(). The pass leaves the old record unchanged and stops; the new record waits until another external kick.

Recommended fix: Consume the broker request before clearing dirty, or combine both signals so a kick concurrent with pass startup cannot be erased without either leaving dirty visible to the next loop or leaving a broker wake pending. Add a deterministic interleaving test that kicks between the two operations and ends the current pass with TerminalNoop.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@ScriptedAlchemy
ScriptedAlchemy merged commit 2254620 into master Oct 17, 2026
11 of 20 checks passed
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