Skip to content

Fixes #3982 - #3985

Merged
ScriptedAlchemy merged 2 commits into
masterfrom
fix/3982-replay-consumer-ownership
Oct 15, 2026
Merged

ScriptedAlchemy merged 2 commits into
masterfrom
fix/3982-replay-consumer-ownership

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Oct 15, 2026 •

Copy link
Copy Markdown
Owner

Fixes #3982.

A project's Hook V2 replay consumer is registered per hook data root. Two owners of one project share that root, for example a reopened route and the retiring owner it replaces. Two defects let the older owner strand the newer one:

  • Retirement called shutdown_hook_v2_replay_consumer(data_root), which removed, aborted and detached whichever consumer held the root, including a newer owner's.
  • A reopen that registered while the retiring owner's server still held the older graph was refused. When that graph was released, its consumer exited and the project had no replay at all.

Change

  • register_hook_v2_replay_consumer: the newest graph for a data root takes it over and aborts the displaced task. Registering the same live graph twice still returns false.
  • Each registration keeps its spool wake (Arc<Notify>). The consumer task's exit path and spool_watch::detach_consumer act only on the registration or wake they own.
  • New retire_hook_v2_replay_consumer(data_root, owner) stops the consumer only when owner's graph owns it. Capacity retirement, unserved-owner retirement and remote recovery now call it.
  • shutdown_hook_v2_replay_consumer(data_root) still stops any owner. Its remaining callers are daemon shutdown and remote project deletion, which end every owner of the root.

Verification

Both new tests in hook_v2_replay_consumer::tests fail on master and pass with the fix:

  • a_late_retirement_leaves_the_newer_owners_replay_consumer_running: on master it fails with "the newer owner must own the data root's running replay consumer".
  • a_reopen_takes_over_replay_from_an_owner_still_retiring: on master it fails with "the reopened owner's registration must be accepted".

Gates: clippy --config=clippy //crates/tracedecay/..., //crates/tracedecay:rustfmt_check_2024, //crates/tracedecay:unit_test and //crates/tracedecay:daemon_suite all pass. twelve_project_journey_retires_idle_owners_without_empty_graphs passed 20/20 runs (20 jobs pinned to 4 cores) on master, on #3981's head and on this branch. The original flake did not reproduce locally.


Devin Review

Two owners of one project share a hook data root. A late retirement of the older owner must not stop the newer owner's consumer, and a reopen must take over replay from an owner still retiring. Both fail on master (#3982).
Retiring an owner stopped whichever consumer held its hook data root, and a reopen that registered while the retiring owner's graph was alive was refused, so the older graph's release left the project without replay. The newest owner now takes the data root over, retirement stops only the consumer its graph owns, and spool detach clears only the wake it attached. Daemon shutdown and project deletion still stop any owner. Fixes #3982.
@changeset-bot

changeset-bot Bot commented Oct 15, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: cfa3217

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.

Devin Review

Comment on lines 502 to +503
spool_watch::attach_consumer(&data_root, &project_root, Arc::clone(&wake));
let task_wake = Arc::clone(&wake);

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.

🟡 Spool wakes target an aborted consumer

When registrations overlap, register_hook_v2_replay_consumer can attach the displaced owner's wake after the new owner's wake. The displaced task is aborted, so new records wait for the live consumer's 30-second sweep.

Learn more

The registry insertion determines which graph owns the data root, but attaching its filesystem wake happens afterward. Another registration can replace the first registry entry and attach its wake before the first registration reaches the watch. The first then overwrites the second wake; its task is aborted when it tries to save its handle, so the watcher keeps notifying a task that cannot drain records. The live task still polls on the replay interval, so the result is delayed delivery rather than permanent loss.

Example: Registration A inserts its entry and pauses. Registration B replaces it and attaches wake B. A attaches wake A, then aborts its own task because its entry is gone. An appended record wakes A instead of B and waits until B's next interval sweep.

Recommended fix: Serialize registry ownership changes with wake attachment, or conditionally attach a wake only when its registration still owns the data root. Ensure that a displaced registration cannot overwrite the active watch target after its registry entry has been replaced.

Devin Review


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

@ScriptedAlchemy
ScriptedAlchemy merged commit 62e368e into master Oct 15, 2026
2 of 5 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.

fix(daemon): late retirement can remove a newer Hook V2 replay consumer

1 participant