Repository navigation
Keep admitted work running when stop-if-idle finds the namespace busy; bump to 1.2.6 - #219
Merged
Shengyu Fu (shengyfu) merged 2 commits intoOct 31, 2026
Conversation
A stop-if-idle probe closed catalog admission and tested activity.lock before it counted protected work. Jobs of operations accepted before the probe then failed with busy/namespace-admission-closed even when the probe decided not to stop. Count catalog work and close admission in one transaction, and close only when no work is visible. Acceptance checks admission in its own write transaction, so the count includes every unfinished operation accepted before the closure. A probe that sees unreleased leases or owners, reservations, unfinished operations, receipt readers or external work now refuses the stop without closing admission or testing activity.lock, discards only expired verification, and records the decision through the same receipt path. The closed path is unchanged: the final all-category count and activity.lock test follow the closure, and only a committed stop ends in-flight work. namespace_readers_or_work is now observed only after admission closes and is false when the probe refuses before closing. Fixes #218 Co-authored-by: Copilot App <[email protected]>
Co-authored-by: Copilot App <[email protected]>
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
No active review issues were identified for the changes to busy idle-stop probes.
No open issues 🎉
What changed in this PR
Fixes #218 by letting managed stop-if-idle probes refuse a stop without interrupting already-admitted work when the namespace is visibly busy. Prepares release 1.2.6.
Changes:
- Counts catalog work and conditionally closes admission in one transaction; busy probes leave
activity.lockuntouched. - Adds regression coverage for accepted operations and adjusts reader tests for the diagnostic field’s updated semantics.
- Documents the idle-stop behavior and updates release versions and examples.
| File | Description |
|---|---|
tgrep-core/tests/managed_lifecycle.rs |
Adds three regression tests protecting admitted work during busy probes. |
tgrep-core/src/managed/maintenance.rs |
Adds the busy fast path and shared idle-decision commit logic. |
tgrep-core/src/managed/collection.rs |
Updates assertions for busy probes that skip the activity lock. |
tgrep-cli/tests/shared_daemon/managed_faults.rs |
Adds an end-to-end accepted-attachment regression test. |
tgrep-cli/tests/shared_daemon/managed_clients.rs |
Waits for unrelated work to clear before checking escaped readers. |
SHARED_WORKTREE_INDEXES.md |
Documents admission handling and reader diagnostic semantics. |
fuzz/Cargo.lock |
Updates the locked core version to 1.2.6. |
CONTRIBUTING.md |
Updates release examples to v1.2.6. |
Cargo.toml |
Bumps the workspace version to 1.2.6. |
Cargo.lock |
Updates locked CLI and core versions to 1.2.6. |
Review effort: Balanced
Shengyu Fu (shengyfu)
deleted the
shengyfu-fix-tgrep-idle-stop-race-v1-2-6
branch
October 31, 2026 01:52
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #218.
Problem
In v1.2.5,
stop-if-idleclosed catalog admission before it counted protected work. It then always tried to lockactivity.lockexclusively, even when the count had already found work. Admission stayed closed until the decision committed, and a lock the probe obtained stayed held until the probe returned, even when the decision was busy. Work accepted before the probe could therefore fail its next admission check with retryablebusy/namespace-admission-closed, even though the probe then refused the stop. Two checks fail that way:work::ensure_admissionrejects while catalog admission isclosed. Operation acceptance and owner preparation, registration and claims call it. So do steps inside accepted jobs, including reservations, object creation and pinning, generation opening, metadata mutations, and view attach, reconciliation and publish.ActivityGuard::acquiretakesactivity.lockshared for reservations, owner preparation, catalog handles, object and root guards, collection, inventory and housekeeping. While the probe holds the lock exclusively, the shared lock fails and is reported with the same code.In #218 this left an accepted
views.attachoperationfailed.Fix (
tgrep-core/src/managed/maintenance.rs)accepted/preparing/cancellingfilter as v1.2.5.ensure_admissioninside theIMMEDIATEcatalog transaction that records them, so they serialize with the probe's count-and-close transaction. Every unfinished operation accepted before the closure is counted and makes the probe refuse. A later one gets the documented retryablebusy/namespace-admission-closedbefore acceptance. An accepted operation is never then failed by an idle-probe closure. Owner claims also check admission but write nothing to the catalog. A claim needs an issued, unreleased owner, which the count includes, so an idle probe never closes admission under a claim.activity.lock, and never reaches theIdleAdmissionClosedfault point. Like the v1.2.5 busy decision, it discards only expired verifier caches (discard_idle_verification(true)). It then records its non-stopping result through the same token and operation-result path. It still writesadmission='open'as v1.2.5's busy decision did, which also repairs a closure left behind by a failed reopen.activity.lock. It commits a stop only when every category is idle. A busy result reopens admission and a committed stop leaves it closed. Only a committed stop ends in-flight work.commit_idle_decision, which writes the stopping service record oradmission='open'together with the receipt, then hitsIdleCommitted.Compatibility
stop-if-idle,busy,namespace-admission-*anddaemon-admission-closedare unchanged, andstoppingkeeps its meaning. No CLI change.IdleOutcome::namespace_readers_or_workis now observed only after admission closes, and isfalsewhen the probe refuses before closing it. In v1.2.5 every probe testedactivity.lock, so a busy probe could reporttrue. The Copilot runtime reads onlystopping, and SHARED_WORKTREE_INDEXES.md did not define this field. The field's doc comment and thestop-if-idleAPI row now state this.managed::collection::tests::busy_idle_probes_preserve_staging_reclamation_and_storage_admissionassertednamespace_readers_or_workfor a probe that an issued owner and an accepted attachment had already made busy. It now assertsoperations == 1and!namespace_readers_or_work. The inverted assertion fails on v1.2.5 withIdleOutcome { stopping: false, .., owners: 1, reservations: 0, operations: 1, .., namespace_readers_or_work: true, .. }, so it doubles as regression evidence that a busy probe leavesactivity.lockalone.Why there is no deferral, CLI change or race test
ViewManager::executedeferral list is unchanged.stop-if-idlecall. The accept loop and the maintenance tick lock the same mutex, so they wait for the decision rather than being rejected. They seedaemon-admission-closedonly after a committed stop or an unresolvedidle-state-unavailable. Jobs never consult that gate. This corrects step 1 of the issue's code path, which says new connections and receipt polls are rejected mid-probe.atomic_idle_admission_rejects_new_owners_operations_and_independent_readerscovers that.Tests
The new tests use existing
managed-test-hooksfault points (IdleAdmissionClosed,IdleCommitted). They synchronize on hook state or probe completion, without sleeps, retries or longer timeouts. With onlytgrep-core/src/managed/maintenance.rsreverted to v1.2.5 (tests unchanged), each one fails:managed_lifecycle:busy_idle_probe_keeps_admission_open_for_an_accepted_attachensure_admissionIdleAdmissionClosed, and the accepted attach endedFailedwithbusy/namespace-admission-closed. ExpectedCompleted.managed_lifecycle:busy_idle_decision_leaves_the_activity_lock_to_admitted_workActivityGuard::acquireIdleCommitted,reservefor the accepted attach returnedError { category: Busy, reason_code: "namespace-admission-closed", retryable: true, committed_state: NotCommitted, .. }.managed_lifecycle:operation_accepted_before_an_idle_probe_refuses_the_stop_and_completesensure_admission, with an operation as the only protected workFailedwithbusy/namespace-admission-closed. ExpectedCompleted.shared_daemon:managed::faults::busy_idle_probe_keeps_admission_open_for_an_accepted_attachmentviews.attachended"state":"failed"withbusy/namespace-admission-closed. Expected"completed".managed::collection::tests::busy_idle_probes_preserve_staging_reclamation_and_storage_admission(inverted assertion)activity.lockon a busy probenamespace_readers_or_work: trueAdjusted existing daemon test.
managed::clients::escaped_cross_process_readers_block_collection_and_idle_stop_then_allow_exact_rebuildexpects an escaped cross-process reader to show up asnamespace_readers_or_work: true. Itsidle_statushelper took the first non-busy probe outcome. With this change, a probe that happens to see other daemon work refuses before it closes admission and reportsfalse. Two sources of such work already exist in v1.2.5:background_batches;With the fix and v1.2.5's helper, the test failed this way (
left: Bool(false)) in 11 of 20 runs. The helper now repeats the probe until an outcome shows no other work: every lease, owner, reservation, operation, receipt-reader andexternalcount is zero. It polls every 20 ms, like the existingstop()helper, within a 10 s bound. Only a probe that closed admission and testedactivity.lockcan return an outcome like that. The assertions are unchanged, apart from now printing the outcome. No product code depends on this.Reproduction and stress runs
At this PR's head, each run a separate
cargo test --locked ... --features managed-test-hooks ... --exactinvocation. The v1.2.5 rows check out onlytgrep-core/src/managed/maintenance.rsfromrefs/tags/v1.2.5, then restore it.managed_lifecycletestsmanaged_lifecycletestsmanaged_clients.rsassert_eq!(idle["namespace_readers_or_work"], true)withleft: Bool(false); 9 passedThe adjusted helper passes against both v1.2.5 and fixed product code, and still asserts
namespace_readers_or_work: truefor the escaped reader.Release
Cargo.toml,Cargo.lock,fuzz/Cargo.lockand the twoCONTRIBUTING.mdexamples.stop-if-idleAPI row, and the idle-stop sequence and recovery text. Theactivity.locklayout entry keeps its v1.2.5 wording, which is still accurate: work and independent readers hold that lock shared, and only a probe that has closed admission tests it exclusively.release.ymlorsign-and-release.yml, or publishing a release.Validation
Run one at a time at this PR's head,
f606863, on Windows 11 Enterprise (16 logical CPUs) with rustc/cargo 1.98.0, rustc 1.88.0 and Python 3.14.7.git status --porcelainwas empty before and after.TGREP_V1_BINARYandTGREP_V1_READER, which only theshared_daemonmanaged tests read, pointed to the debugtgrepandlegacy_readerexample built asci.ymlbuilds them: agit archiveof the pinned1120aca, prepared withscripts/qualification/prepare_legacy.py. The Rust 1.88 rows used thewindows-msrvjob's environment (CARGO_BUILD_JOBS,RAYON_NUM_THREADSandRUST_TEST_THREADSset to 2, plus its hardeningRUSTFLAGS) withCARGO_TARGET_DIR=target\msrv.cargo fmt --all -- --checkgit --no-pager diff --checkgit --no-pager diff --check refs/tags/v1.2.5..HEADcargo clippy --locked --workspace --all-targets --features tgrep-cli/managed-test-hooks -- -D warningscargo test --locked -p tgrep-core --features managed-test-hooks managed:: -- --test-threads=2cargo test --locked -p tgrep-core --features managed-test-hooks --test managed_lifecycle -- --test-threads=2cargo test --locked -p tgrep-cli --features managed-test-hooks --test shared_daemon managed:: -- --test-threads=2cargo test --locked -p tgrep-cli --features managed-test-hooks --bin tgrepcargo test --locked --workspaceshared_daemon(66 passed, 1 failed, 1 ignored), after the 11 earliertgrep-clitargets passed. The failure is the pre-existing one below.cargo test --locked --workspace --no-fail-fast(to run the remaining targets)cargo test --locked --workspace --features tgrep-cli/managed-test-hooks --no-fail-fast -- --test-threads=2shared_daemon: 115 passed, 4 ignored), including every new and adjusted testpython -B -m unittest discover -s scripts/qualification -p test_*.py -vpython -B -m unittest discover -s scripts -p test_*.py -vpython -B scripts/qualification/qualify.py dependenciescargo +1.88.0 check --locked --workspace --all-targets --features tgrep-cli/managed-test-hookscargo +1.88.0 test --locked -p tgrep-core --features managed-test-hooks --test managed_catalog_windows --test managed_lifecycle -- --test-threads=2cargo +1.88.0 build --release --locked -p tgrep-cli --bin tgrep --target x86_64-pc-windows-msvctgrep.exe --version, thentgrep.exe --no-index -F -q -- "[workspace.package]" Cargo.tomltgrep 1.2.6cargo build --locked --workspace --benchespython -B scripts/qualification/qualify.py installedcargo install --locked, then checked the installed binary: local index and server parity; shared attach, refresh and stale-daemon fallback; restart, detach and worktree deletion; and managed v2 owner proof, generation reuse, indexed parity, migration and replay. All three lockfiles unchanged. Its cleanup retried once while Windows still held the exitedtgrep.exeimage, which the harness expects.cargo test --manifest-path vendor/ignore/Cargo.toml --locked --target-dir target\vendor-ignore--target-dirkeeps the build out ofvendor/ignore/target, which.gitignoredoesn't cover.managed-measurementjob's steps:cargo test --locked --release -p tgrep-cli --features managed-test-hooks --test shared_daemon --no-run --message-format=json, thenscripts/qualification/capture_managed_artifacts.py, then the captured harness withmanaged::performance::native_managed_lifecycle_measurement --ignored --exact --test-threads=1 --nocapturestop-if-idle, passed in 72 s. The host was shared rather than quiet, so this is a functional pass, not a measurement.git --no-pager diff --exit-code -- Cargo.lock fuzz\Cargo.lock vendor\ignore\Cargo.lock, after each batchPre-existing failure, out of scope. Without
managed-test-hooks,managed::polling_modes_repair_unreported_restored_mtime_and_membership_changesfails attgrep-cli/tests/shared_daemon.rs:151withunknown managed method: testing.reconcile-timer. That RPC exists only with the feature, but the test is not gated on it. Unmodified v1.2.5 fails the same way: from agit archiveofv1.2.5,cargo test --locked -p tgrep-cli --test shared_daemon -- managed::polling_modes_repair_unreported_restored_mtime_and_membership_changes --exactexits 101 with the identical panic. CI always enables the feature, and the test passes in the feature runs above. This PR leaves it unchanged.Not run locally: the Linux and macOS jobs, including the Unix installer test and the
scripts/agentintegration tests, which CI runs only off Windows.