Repository navigation
fix(overlay): wake an orphan only when nothing awaits its captured layer - #475
Merged
yifanxuaaa merged 1 commit intoOct 27, 2026
Merged
Conversation
A Commit can stall after publication when an open orphan's lower source
belongs to the captured generation. The mounted reproduction creates files
since the previous Commit, holds them open and unlinks them after capture;
readdir and create then do not answer on the base revision.
Known install and definite failed-capture resolution queued orphan-source
wakes while the Commit's captured reader still held the generation. Each
woken orphan parked again and restored its wait entry, keeping the wake
pending. OwnerClient::maintain drained that work before returning the job's
result, while the Commit released its reader only after the driver's
outcome.
Share orphan_source_awaited between migration and wake eligibility. An
orphan waits while the capture has not settled its lower generation or a
captured reader still holds it. While that predicate holds, no orphan-source
wake is queued.
The final reader release wakes the orphan if settlement happened first;
settlement wakes it if the reader released first. Reader lifetime,
wake_generation, retirement and OwnerClient::maintain are unchanged.
Document both event orders in 35-shared-physical-capacity.md.
Add five public-overlay regressions and one isolated real-mount regression.
The mounted test bounds mount-dependent observations and failure handling,
retaining unfinished threads and the Harness until process exit and
leaving fixture files for external cleanup.
Outside-sandbox validation on vm2, Linux aarch64, Rust 1.85.1, --locked,
Disposable, LAYERFS_CONSTRUCTION_WORKERS=1:
- orphan_wake: base 3 failed / 2 passed; fix 5 passed.
- mounted_orphan_commit: base failed in 9.04 s; fix passed in 0.03 s.
- Full overlay binaries: base 76/3/1; fix 79/0/1.
- Full daemon binaries: base 127/2/5; fix 128/1/5.
Counts are passed/failed/ignored. forced_unavailable fails its fusectl
precondition on both sides.
- Final fmt, scoped Clippy -D warnings, workspace check excluding
layerfs-node, boundary scan with --skip-sdk and 155 tooling tests passed.
General foreground-maintenance bounds, the large failed-Commit fold and
the separately observed Construct / InvalidRecord("binding kind") failure
remain outside this change. Descriptor close was not tested as a way out
of the stall. Durable: NOT_RUN — disabled by owner until explicit
reauthorization.
Production LOC: 102,200 -> 102,203 (delta +3)
engine 61,313 -> 61,316 (+3): layerfs-overlay 9,730 -> 9,733 (+3);
core 38,241, api 2,646 and layerfs-store-service 10,032 unchanged.
Reference absent (retired R9c; last counted 65,417 at tree
498dd19).
Method: python3 -B tools/production_loc.py --root <tree> --detail on
a git archive of parent caebd26 and the final source tree;
counter blob 3341e4d in both.
Tests and documentation excluded.
Signed-off-by: Jackmeson1 <130693642+Jackmeson1@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TTGAv6fe859ebbCLLfLHP
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.
Summary
A Commit can stall after publication when an open orphan's lower source belongs to the captured generation. The mounted reproductions create files since the previous Commit, hold them open and unlink them after capture. On the base revision, the Commit did not return and
readdirandcreatedid not answer.Known install and definite failed-capture resolution queued orphan-source wakes while the Commit's captured reader still held the generation. Each woken orphan parked again and restored its wait entry, keeping the wake pending. The owner drained that maintenance before returning the job's result, while the Commit could release its reader only afterward.
This change makes parking and waking use the same predicate: an orphan must wait while the capture has not settled its lower generation or a captured reader still holds it.
Repro
The issue's standalone
orphan_wake_repro.rs, saved atengine/crates/layerfs-overlay/tests/orphan_wake_repro.rs, uses the existingpayload_supportmodule. It writes a file in the captured generation, acquires a captured reader, unlinks the file with a descriptor open, then installs the capture. Maintenance does not go idle within 256 turns oncaebd268d; it does with this change.The new
mounted_orphan_committest reaches the product control Commit through a real mount. An external bash holds three files open. The real History provider publishes, the external History wrapper holds the Commit before local install, the files are unlinked, and the Commit is released.On
caebd268d, the mounted test reports:Closing the descriptors was not tried as a way out of the stall. Failure cleanup is not that experiment.
Root cause
The following source references are at
caebd268d, underengine/crates/:layerfs-overlay/src/lifetime/generation.rs:24-25: install callswake_orphan_sources; definite failed-capture resolution and closed-capture release call it too.layerfs-overlay/src/maintenance/source_wait.rs:9-22: a generation-wait entry queues aWAKE_ORPHANitem without checking whether the generation remains awaited.source_wait.rs:23-42: the wake removes entries from the indexed in-memory wait map and makes their orphan items ready. The wake finishes only after finding an empty batch.maintenance/orphan.rs:237-242: migration finds the captured generation held, restores its wait entry and parks again.layerfs-store-service/src/overlay/owner.rs:401-403,413-474:OwnerClient::maintaindrains ready work before the job returns.layerfs-store-service/src/store/captured.rs:82-103,190-202: the captured reader is released after the driver's outcome, so it cannot end the wait while the install job remains in that drain.The mounted failures establish that
readdirandcreatealso did not answer. I have not established their precise starvation mechanism.Fix
orphan_source_awaited(gen)inmaintenance/source_wait.rs: the current capture owns the generation, or a captured reader holds it.migrate_orphan, replacing the same inline check.wake_orphan_sourceswithout queueing an orphan-source wake while the predicate holds.core/docs/architecture/35-shared-physical-capacity.md.The wait ends when both ownership conditions clear. If settlement happens first, the final reader release reaches the wake through
wake_generation. If the reader releases first, later settlement reaches the same check. Settlement includes known install, definite failed-capture resolution and release of a closed capture.Captured-reader lifetime,
wake_generation, wake stepping, generation retirement andOwnerClient::maintainare unchanged. The owner still drains until no work is ready. This fixes the cycle; it does not bound foreground maintenance generally or fix the large failed-Commit fold.Tests
Outside-sandbox validation was performed on vm2: Linux aarch64, Ubuntu 24.04, kernel 6.8.0-147, Rust 1.85.1,
--locked, Disposable profile andLAYERFS_CONSTRUCTION_WORKERS=1.The suite comparison used
caebd268dplus the two new regression-test files as the base, and the final five-file change as the fix. Source fingerprints in the retained receipts matchf160d6e80.Each suite package was built first with
cargo +1.85.1 test --locked -p <crate> --no-run. Its binaries were copied under/tmpand executed separately as root, withLAYERFS_CONSTRUCTION_WORKERS=1,timeout 120and--test-threads=1. The standalone issue reproduction was added temporarily, built first and executed separately undertimeout 30; it needs neither root nor FUSE.layerfs-overlayorphan_wake: install, failed capture, early reader release, no-reader control, no-descriptor controllayerfs-daemonmounted_orphan_commitorphan_wake_reproThe three overlay failures on base report:
The overlay tests also check retained captured bytes, wake progress and reclamation. Multiple-reader and closed-capture orphan combinations are not additional cases in the new test file.
The mounted test bounds mount-dependent observations from Ready through its unmount attempt. Scripts lead their own process groups and enter the mount inside bash. Owner/control calls run on worker threads with channel deadlines. Both Commits are observed for five seconds; only a handle already observed finished is joined. The product unmount is observed for ten seconds.
Timed-out scripts and probes signal their own groups and allow up to one second to reap each leader. If the experiment fails,
Scene::abandonthen attempts one lazy detach of its own mount, observing it for three seconds and allowing up to one further second to reap a killed unmount leader. It prints whether the mount entry remains, then kills the retained holder group and allows one second to reap its leader.Cleanup is deliberately incomplete. Retained worker/Commit threads, the Harness and its serial gate remain until process exit; the fixture directory remains on disk for external cleanup. A killed helper blocked on the mount may outlive its reap budget. A failed lazy detach can leave a mount entry: process exit closes the test's FUSE connection but is not proof of detachment. The test never releases the captured reader early, retries the Commit or manipulates another FUSE connection. It is the only test in its binary. Bring-up and clean Harness shutdown retain the harness's existing lifecycle behavior and the invocation's external timeout.
In the retained base execution, the guard reported the mount entry gone and the holder leader reaped. A separate post-exit inspection found no matching mount or helper processes before external cleanup.
Whole suite-binary results, summed as passed / failed / ignored:
layerfs-overlaylayerfs-store-servicelayerfs-daemonlayerfs-workspacelayerfs-fuseThe remaining daemon failure is
forced_unavailable, at its precondition:the control filesystem must not be mounted here. vm2 has fusectl mounted; the same test fails on base. The additional base daemon failure ismounted_orphan_commit.Ignored on both sides, requiring external setup:
layerfs-overlay:allocation_filesystem(1).layerfs-daemon:complete_installed_roots(2),host_handoff(1),mounted_sqlite_full(2).The throwaway
unlink_during_commitrace harness is not included in the tree. It creates 4,000 Workspace files, holds 100 open, and begins unlinking all names at delays of 0, 5 or 20 ms after Commit starts. Each run usedtimeout 60.On
caebd268d, all three runs reached the harness's 25-second Commit observation limit;readdirandcreatedid not answer within five seconds. The root stat answered at delay 0. In that run, the harness had not exited after 52 seconds and its own FUSE connection was aborted externally.With this change, the first Commit returned
Committedin 549, 532 and 502 ms at those delays. These are diagnostic observations from one run per delay, not a performance claim. After unlinking completed, the harness committed again and compared the live and published trees with mtime excluded:The following Commit returned
UpToDate.An earlier delay-0 execution on an earlier build returned:
That is a separate unresolved failure: the Commit returned rather than stalling. This change does not address it. It did not recur in the three final runs, and the deterministic mounted regression unlinks only after publication, so it does not exercise that earlier construction window.
Checks on the final source:
cargo +1.85.1 fmt --manifest-path Cargo.toml --all -- --check: clean.cargo +1.85.1 clippy --manifest-path Cargo.toml --locked -p layerfs-overlay -p layerfs-store-service -p layerfs-daemon --all-targets -- -D warnings: clean.cargo +1.85.1 check --manifest-path Cargo.toml --locked --workspace --exclude layerfs-node --all-targets: clean.python3 -B tools/check_product_boundary.py --skip-sdk: PASS, 818 production files; Node and JavaScript source scanning excluded.python3 -B -m unittest discover -s tools -p 'test_*.py': 155 passed.Not run:
layerfs-apitest execution, macOS and Windows execution. The workspace check compiledlayerfs-api. Durable:NOT_RUN — disabled by owner until explicit reauthorization.Production LOC: 102,200 -> 102,203 (delta +3)
Counted with
python3 -B tools/production_loc.py --root <tree> --detailon agit archiveof parentcaebd268dand the final source tree. Engine: 61,313 -> 61,316;layerfs-overlay: 9,730 -> 9,733. Core 38,241, API 2,646 andlayerfs-store-service10,032 are unchanged. Reference absent (retired R9c; last counted 65,417 at tree 498dd1917812). Counter blob3341e4d0362c822b6d9a08419c432942b3a13800is identical in both snapshots. Tests and documentation are excluded.Fixes #474
AI disclosure: this was prepared with AI assistance (Claude Opus 5.5).
🤖 Generated with Claude Code
https://claude.ai/code/session_019TTGAv6fe859ebbCLLfLHP