Repository navigation
feat(daemon): expose the unmount abort outcome in telemetry - #501
Open
mengmengmengqiang wants to merge 2 commits into
Open
mengmengmengqiang wants to merge 2 commits into
mengmengmengqiang wants to merge 2 commits into
Conversation
) A normal unmount that has to end a stalled, already-detached connection with the connection's one abort write now reports that path beside the ordinary reply: a traced Unmount carries an `unmount` telemetry section inside the existing `Observed` envelope, on success (still `Unmounted`, still no replay) and on a retained stop, where the entry keeps the observation for the reply that later reports that custody. Its stable keys are `stage` (`Join`, `Abort`, `Revoke` or `Close`), `abort` (`Written`, `Short` or `Failed`), `bytes` for a short write or `errno` for a failed one, `detached`, `loops_exited`, `loops_joined`, `active`, `retained` and `waited_ms` from the stalled drain to the end of the abort path. The daemon also writes one `LAYERFS_DAEMON_UNMOUNT_ABORT <field>=<value>` stderr line when that path ends, traced or not, with the same fields; it is one line per abort, never per-request logging. An unmount that never stalls carries neither. The observation is daemon-internal and encoded in no reply field: `Success` carries it, `TeardownCustody` and the `Observed` envelope are unchanged, and the application merges it into the telemetry it already builds. No retry, second abort, second detach, syscall, dependency or test hook is added. Deleted tests: none. Validation on Linux ARM64, Rust 1.85.1, locked dependencies, disposable Store only (Durable NOT_RUN, disabled by owner): - The actual daemon executable under a copied mount namespace: the traced stalled unmount's `unmount` section and its one stderr line, and a plain unmount with neither (layerfs-api unmount_observability: 1 passed, 5.2 s). - The stable key set for Written/Short/Failed and the stderr line, plus no observation for a normal unmount (layerfs-daemon unmount_observability: 3 passed, 0.1 s). - stalled_unmount with the new attachment and retention assertions: 6 passed, 21.9 s; the six existing cases keep their exact custody. - cargo check -p layerfs-daemon -p layerfs-api --all-targets: pass. - cargo fmt --all -- --check: pass. - cargo clippy --manifest-path Cargo.toml --locked --workspace --exclude layerfs-node --all-targets -- -D warnings: pass. - python3 -B tools/check_product_boundary.py --skip-sdk: PASS (821 files), run with the tree at a path without a `src` component; mounted at `/src` the guard misreads the mount itself as a module path (the same 2 pre-existing findings appear on the untouched parent tree). No full SDK/benchmark qualification; the Host view that aggregates these facts is a dsh-layerfs follow-up, outside this repository. Production LOC: 102831 -> 103059 (delta +228) First-parent comparison: 35d3657 to the committed tree. Scopes: core 38395 -> 38395 (0); engine 61777 -> 62005 (+228); api 2659 -> 2659 (0); reference absent (retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root <exported tree>, first-party production Rust, runtime SQL and JavaScript; tests, docs, tooling, generated and third-party code excluded.
This was referenced Oct 28, 2026
…er a retained stop The layerfs-api `unmount_observability` target mounts a real Workspace, but it was not one of the named SDK mount targets, so the fast lane ran it as a non-root runner without `/dev/fuse`: the daemon exited before printing `LAYERFS_DAEMON_LISTEN`, and the test panicked at the line that parses it (upstream Ephemeral-AI-Lab#501, run 40781324309). Register the target in `tools/ci_tests.py` `API_MOUNT_TARGETS` (the same routing the Linux-only daemon FUSE targets use) and assert it in `tools/test_ci_tests.py`; the nightly mount lane now builds it and runs it in its own container with `/dev/fuse`, SYS_ADMIN and an unconfined AppArmor profile. The test's assertions are unchanged; it is skipped by lane selection in the fast lane, not failed. Two hardened start-up observations from the failure: when the LISTEN line is still missing (for example a direct run in a non-privileged container), `start` now panics with the daemon's stderr instead of an opaque `unwrap`, and it does not join the blocked stdout reader on that path (the killed child ends it). A Drop guard removes the fixture directory on every exit, including a panic. The retained Revoke/Close stop now reaches a traced reply. The new test drives the real in-process `Service` over the SDK control owner, stages the later boundary through the entry's own owner (a poisoned overlay job for Revoke, an already-closed overlay for Close), and asserts the `Reply::Retained` custody and the same `unmount` telemetry section a completed abort path carries, at the named stage. A retained stop cannot be staged through a separately spawned daemon, and the daemon application's startup owner is not a library API, so the reply envelope is the test adapter's; the custody, the abort observation and the section are production values (`Service::retained_abort`, `telemetry::unmount_abort`, `Reply::Retained`), `src/` is unchanged, and no test-only production API or test hook is added. The test lives in `layerfs-api/tests`, the SDK-facing daemon integration boundary; the adapter selection in `application/diagnostics.rs` remains the one composition link the test does not exercise (documented in the test header and architecture docs). Deleted tests: none. Validation on Linux ARM64, Rust 1.85.1, locked dependencies, Disposable Store only (Durable NOT_RUN, disabled by owner), each invocation under 120 s: - layerfs-api unmount_observability (privileged container, /dev/fuse + SYS_ADMIN, --test-threads=1): 3 passed, 11.27 s; repeated 11.46 s and 11.30 s. Includes the completed abort path through the actual daemon binary and the retained Revoke and Close traced replies. - Negative check: dropping the `unmount` field from the test's observed reply fails the retained test at "the retained stop's telemetry section" (the assertion is live; no false pass). - layerfs-daemon unmount_observability: 3 passed, 0.09 s; stalled_unmount: 6 passed, 21.66 s. - A container without /dev/fuse: `tools/ci_tests.py mount-targets` selects `(layerfs-api, unmount_observability)`; `targets fast` does not name it; the fast nextest filter excludes `binary_id(layerfs-api::unmount_observability)`; `tools.test_ci_tests` passes (6 tests). - The exact mount-lane build (`cargo test --locked -p layerfs-daemon -p layerfs-api $(python3 -B tools/ci_tests.py mount-targets) --no-run --message-format json` piped through `ci_tests.py executables`) yields both unmount_observability binaries and stalled_unmount (45 mount binaries); the ordinary layerfs-daemon binary is built beside them. - cargo check --locked --workspace --exclude layerfs-node: pass. - cargo fmt --all -- --check: pass. - cargo clippy --manifest-path Cargo.toml --locked --workspace --exclude layerfs-node --all-targets -- -D warnings: pass. - python3 -B tools/check_product_boundary.py --skip-sdk: PASS (821 files, run at a path without a `src` component; at `/src` the guard misreads the mount and reports the same 2 pre-existing findings as the untouched parent). - python3 -B -m unittest discover -s tools -p 'test_*.py': 155 tests OK. - tools/check_fuser_integrity.py: exit 0 (no dependency/config change). - Pinned ruff 0.17.2 `format --diff` on the changed Python tools: already formatted. Production LOC: 103059 -> 103059 (delta 0) First-parent comparison: b0f7d43 to the committed tree. Scopes: core 38395 -> 38395 (0); engine 62005 -> 62005 (0); api 2659 -> 2659 (0); reference absent (retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py --root <exported tree>, first-party production Rust, runtime SQL and JavaScript; tests, docs, tooling, generated and third-party code excluded.
mengmengmengqiang
added a commit
to tokenaissance/layerfs
that referenced
this pull request
Oct 28, 2026
…emetry run (comic3) Adds the qualified Ephemeral-AI-Lab#501-head numbers in the Ephemeral-AI-Lab#499/fork #5 row: early-mount unmount 5034/5056/5123/5175 ms (n=10, 10/10 Join abort), normal unmount 11.5/14.6/17.2/17.8 ms and exec 27.7/33.1/35.2/35.5 ms (n=5, exit 7), control flag-off unmount 5038/5050/5136/5188 ms (n=5). Notes that the telemetry run is on Ephemeral-AI-Lab#501's head, not Ephemeral-AI-Lab#499's, and that Ephemeral-AI-Lab#501 head has no private-ns support (LAYERFS_PRIVATE_MOUNT_NS=1 is a no-op there).
mengmengmengqiang
added a commit
to tokenaissance/layerfs
that referenced
this pull request
Oct 28, 2026
The Ephemeral-AI-Lab#501 telemetry numbers were measured with the b0f7d43 production artifacts; the branch head fe6e993 only adds docs/tests/tooling and its sealed bundle is byte-identical (all 184 files). The matrix note now says so.
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.
What and why
Issue tokenaissance#2 asks for the aborted normal unmount to be visible: a structured daemon warning and structured custody/abort facts in the SDK trace line, including the success path — an abort-on-
Joinsuccess currently reports onlyUnmounted.Upstream #500 (already merged) reworked #496 with exact abort custody: a normal unmount whose drain stops at
Joinmakes the connection's one abort write and completes withUnmounted; a short or failed write, a second stall, or a laterRevoke/Closestop keeps the exact custody inTeardownCustody.forced. Those facts were visible only to a caller that read the receipt or the failure custody, never to the trace line.This PR attaches the same facts to the reply as a daemon
unmountobservation, inside the already existingReply::Observed { reply, telemetry }envelope, on success and on a retained stop. It adds no wire field, no reply record, no custody or stalled-unmount behaviour change, no retry, no second abort or detach, and no dependency.Base:
mainat 35d3657 (the #500 merge). Builds on the telemetry envelope that #500 merged, and changes no part of its custody correction. The same commit is the head of fork PR tokenaissance#6.CI lane routing
The SDK-side target
layerfs-api::unmount_observabilitymounts a real Workspace, so it needs root,/dev/fuseandCAP_SYS_ADMIN. It is now one of the named SDK mount targets intools/ci_tests.py: the fast lane neither builds nor selects it, and the nightly mount lane runs it in its own container with/dev/fuseandSYS_ADMIN. Before that registration the fast lane ran it as a non-root runner, the daemon exited before printingLAYERFS_DAEMON_LISTEN, and the test panicked there (#501 run 40781324309). When that line is still missing,startnow panics with the daemon's stderr instead of an opaqueunwrap, and a Drop guard removes the fixture directory on every exit.Telemetry key set
A traced
Unmountthat took the abort path carries a nestedunmountmap (stable keys, in this order):stageJoin(the stalled drain was ended by the abort),Abort(the one write was short or failed), orRevoke/Closewhen a later step stopped with the abort recordedabortWritten,ShortorFailedbytesShortwrite onlyerrnoFailedwrite only (e.g.EIO)detachedloops_exited,loops_joinedactive,retainedwaited_msThe daemon also writes one
LAYERFS_DAEMON_UNMOUNT_ABORT workspace=… namespace=… stage=… abort=… [bytes=|errno=] detached=… loops_exited=… loops_joined=… active=… retained=… waited_ms=…stderr line when that path ends, traced or not. It is one diagnostic line per abort, never per-request logging. The line'sstageis the abort path's own (Join, orAbortfor a short/failed write); a laterRevoke/Closestop is reported by the retained entry's section for the reply that reports that custody.The SDK needs no change:
api/agent/src/trace.jsalready records the wholedaemontelemetry map, so the facts appear asdaemon.unmountin the trace line.Success and failure behaviour
Reply::Unmounted(token); the section saysstage: Join, abort: Written. The drained receipt is unchanged.RetainedatAbortwith the original errno/count indetailandTeardownCustody.forced; the section addsabort: Short|Failedwithbytes/errno.RetainedatJoinwithabort: Written.Revoke/Close.ForcedFactsin its reply.E2E on a real Sandbox (SDK, isolated worker)
Success path (
create→ mount → 1500 ms → unmount): the reply isUnmounted, the trace line'sdaemontelemetry carries{"stage":"Join","abort":"Written","detached":true,"loops_exited":2,"loops_joined":2,"active":0,"retained":0,"waited_ms":5083}and the Sandbox's
docker logscontain exactly oneLAYERFS_DAEMON_UNMOUNT_ABORT …line with the same fields. A normalmount → exec → unmountemits no section and no line (5/5), andworkspace.execstill sees the FUSE mount and passes exit code 7 through. Stall window: clean at 700 ms, abort at 800/900/1000/1500 ms.Benchmark (min / p50 / p90 / max ms, n): early-mount unmount 5033.6 / 5055.7 / 5122.6 / 5175.1 (n=10); normal unmount 11.5 / 14.6 / 17.2 / 17.8 and exec 27.7 / 33.1 / 35.2 / 35.5 (n=5); control (restart, same build) 5038.3 / 5049.7 / 5136.4 / 5187.7 (n=5). The ~5 s early-mount is expected here: this PR adds the telemetry, not the private namespace or the 1 s join grace, so the abort path waits the full drain.
Four layers: the daemon renders one
Reply::Observedtelemetry section and one debug line; the SDK trace records the daemon map; the Host (dsh-layerfs) journals and counts — no counter or metric name in the engine.Tests and exact results
Linux ARM64, Rust 1.85.1, locked dependencies, Disposable Store only (Durable NOT_RUN, owner-disabled):
layerfs-apiunmount_observability, in a privileged container with/dev/fuse,SYS_ADMINand--test-threads=1(the mount-lane container shape) — 3 passed, 11.27 s; repeated 3 passed in 11.46 s and 11.30 s. The first case starts the actual daemon executable, mounts a Workspace, holds the mount in a copied mount namespace, and drives a traced unmount: asserts the exactunmountkey set and values (Join,Written,detached, loops,waited_ms), that the daemon wrote exactly oneLAYERFS_DAEMON_UNMOUNT_ABORTline with the same fields, and that a second Workspace unmounted without a copy has no section and adds no line. The two new cases drive the retainedRevokeandClosestops through the SDK control owner with the real in-processService: the later boundary is staged through the entry's own owner (a poisoned overlay job forRevoke, an already-closed overlay forClose), and the test assertsReply::Retainedcustody and the sameunmountsection at the named stage.layerfs-daemonunmount_observability— 3 passed, 0.09 s. Pins the key set forWritten,ShortandFailed(bytes/errno present only then), the stderr line fields, and no observation for a normal unmount.layerfs-daemonstalled_unmount— 6 passed, 21.66 s. The existing six stalled cases also assert the observation on the success reply and the observation the retained entry keeps, including itsRevoke/Closestage; custody, detail andforcedare unchanged.unmountfield from the test's observed reply fails the retained test at "the retained stop's telemetry section"; the assertion is live (no false pass)./dev/fuse:tools/ci_tests.py mount-targetsselects(layerfs-api, unmount_observability);targets fastdoes not name it; the fast nextest filter excludesbinary_id(layerfs-api::unmount_observability);python3 -B -m unittest tools.test_ci_tests— 6 tests OK.cargo test --locked -p layerfs-daemon -p layerfs-api $(python3 -B tools/ci_tests.py mount-targets) --no-run --message-format json, piped throughtools/ci_tests.py executables) yields bothunmount_observabilitybinaries andstalled_unmount(45 mount binaries); the ordinarylayerfs-daemonbinary is built beside them.cargo check --locked --workspace --exclude layerfs-node— Finished, exit 0.cargo fmt --all -- --check— exit 0.cargo clippy --manifest-path Cargo.toml --locked --workspace --exclude layerfs-node --all-targets -- -D warnings— Finished, exit 0.python3 -B tools/check_product_boundary.py --skip-sdk—PASS: scanned 821 production Rust/SQL/JavaScript files. (Run with the tree at a path whose components are notsrc; mounted at/srcthe guard misreads the mount as a module path and reports the same 2 pre-existing findings as the untouched parent tree.)python3 -B -m unittest discover -s tools -p 'test_*.py'— 155 tests OK;tools/check_fuser_integrity.py— exit 0; pinned ruff 0.17.2format --diffon the changed tools — already formatted.Test invocations are capped well under the 120 s ceiling. No lints or guards were suppressed with
#[allow].CI on the new head
Head
fe6e9935a94b3e9024e446c7a52b9e34d7dc5604:fmt and guards,clippy,test). The whole run log contains nounmount_observability: the target is not built or selected by the fast lane.mountjob — success. Bothunmount_observabilitybinaries ran in their own containers with/dev/fuseandSYS_ADMIN: the SDK binary 3 passed in 11.12 s (both retained cases and the actual-daemon case) and the daemon binary 3 passed in 0.03 s.Files changed
engine/crates/layerfs-daemon/src/control/unmount_abort.rs(new): the observation type.engine/crates/layerfs-daemon/src/control/{detach,unmount,serving,registry,types,force,status,operations,attach,mod}.rs: build, carry and keep the observation; no custody change.engine/crates/layerfs-daemon/src/telemetry.rs: theunmountsection and the stderr line, from one field list.engine/crates/layerfs-daemon/src/application/diagnostics.rs: merge the observation into the traced reply's telemetry.engine/crates/layerfs-daemon/tests/{stalled_unmount,unmount_observability}.rs,engine/crates/layerfs-api/tests/unmount_observability.rs: tests above, plus the in-process retained-stop seam and the daemon start-up diagnostics.tools/ci_tests.py: the target's SDK mount-target registration;tools/test_ci_tests.py: the selection assertion.core/docs/architecture/76-native-mount-session.md,80-forced-teardown.md,README.md: key set, line, retained-stop coverage, lane routing, and the Host follow-up.Limitations
waited_msis an observation of the drain wait and abort path, not a latency budget or a benchmark claim.Revoke/Closesection is now also covered end-to-end through a traced reply, but the reply envelope in that test is composed by the test adapter: a retained stop can only be staged in-process, and the daemon application's startup owner is not a library API (exposing it for the test would be a test-only production API). The custody, the abort observation and the section are production values; the one composition link the test does not exercise is the three-line abort selection inapplication/diagnostics.rs. No production source changed.Related: #500 (merged; the envelope this builds on) and tokenaissance#2 (the observation request, closed by fork PR tokenaissance#6).