Skip to content

fix(daemon): reclaim control slots after retained connection failure (#540) - #543

Merged
mengmengmengqiang merged 21 commits into
Ephemeral-AI-Lab:layerfs_v2_0_0from
tokenaissance:fix/540-daemon-slots
Nov 11, 2026
Merged

mengmengmengqiang merged 21 commits into
Ephemeral-AI-Lab:layerfs_v2_0_0from
tokenaissance:fix/540-daemon-slots

Conversation

@mengmengmengqiang

@mengmengmengqiang mengmengmengqiang commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Ready for review — not a draft.

Base: layerfs_v2_0_0, not main. #540's affected code exists only on the v2.0.0 branch: api/crates/layerfs-control/src/daemon/pool.rs is absent from main, and this PR is a follow-up on the v2 daemon/control line (the same base as the other v2.0.0 PRs). A main-based PR would carry all 499 v2 commits instead of this fix.

Status on head 37cc7711a: GitHub reports the PR MERGEABLE / CLEAN, and no CI lane runs for this PR any more — ci.yml and format.yml skip a pull request whose base is layerfs_v2_0_0 (branches-ignore, owner decision 2026-11-07), so run 41800502973 on 4dbbcb7a3 is history, not a verdict on this head. The checks stated below were run locally on 37cc7711a (macOS; Linux-only legs marked NOT RUN). No human review has been submitted yet.

The head is 37cc7711a, 21 commits ahead of layerfs_v2_0_0 and 0 behind it (base head bdeb9e91f). The earlier run's one failure, sdk_blame_coarse, is fixed by #639, which is now in this branch. #68 (jiangkoumo) is merged into this branch: it restores sandbox.get's daemon_capacity and keeps a fenced control connection out of the pool.

Review focus

  • A retained slot no longer owns capacity. Slots::finish now always leaves its slot Vacant and converts a failure that used to keep custody into a compact record in the bounded store; every such failure is also logged. Check that no result path can leave a slot out of Vacant once its worker has exited, and that a stale or duplicate SlotTicket finish changes nothing.
  • The Capacity notice. When no slot is free the daemon writes one fixed advisory pre-handshake LAYERFS-CAPACITY frame and closes; the client maps it to Capacity { resource: "daemon_control_slot", retained, pool_vacant, reserved_vacant, retryable }. Legacy EOF still maps to Capacity with retained: null, a reset maps to Unavailable, and a random 24-byte record is never accepted. Check the notice is treated as advisory and unauthenticated, and that it admits nothing.
  • The doctor query. layerfs doctor asks only Sandboxes whose record says they run and whose daemon endpoint is known for backend.daemon.connection_failures, inside a hard 2 s bound; an unreachable daemon is a warning with ok still true, and nothing is started or written.
  • The 2 s close budget. All Workspace status probes inside one non-force sandbox.close share one total 2 s deadline (api/crates/layerfs-control/src/sandbox/serve.rs:43); unmount keeps 5 s, and force skips the probe. Check it is a close-phase budget, not a whole-close deadline.
  • The test-coverage gap. cargo test -p layerfs-daemon --lib is 0 tests on this head: the inline daemon suites this fix needs were removed by the product-boundary guard, and nothing in-tree replaces them. The Docker E2E below proves slot reclamation, not the retained-record path. Review the diff for the invariants rather than treating green CI as coverage.

What changed in this revision

The initial reclaim fix is extended to the full slot-recovery design:

  • Strict slot classes — Pool (8) plus Reserved (1, installer-only today: it is admitted only while the daemon is in InstallPending / Installing, and AdmissionClass::Lease is unused placeholder code). The reserved slot is not a recovery or lease slot, so a pool-exhausted sandbox.close cannot borrow it; giving it a recovery role is follow-up issue daemon: bound an admitted control slot's lifetime and keep recovery admissible when the pool is exhausted (#540 follow-up) #547. Admission is class-scoped, so ordinary connections cannot take it; each admission carries a generation-checked SlotTicket, and a stale or duplicate finish changes nothing.
  • Panic and spawn safety — the worker runs under catch_unwind and owns a SlotGuard; exactly one finish runs on the normal, error, panic, dropped-thread and spawn-failure paths, recording a synthetic bounded cause.
  • Richer retained records — class, generation, owner (instance:pid:thread), visible count, expires_in, recorded and the bounded failure list; the ring pre-reserves capacity (64 / 1 h) and is reaped lazily. A record is still never a slot.
  • Precise Capacity notice — when no slot is free the daemon writes a fixed advisory pre-handshake LAYERFS-CAPACITY frame before closing. The client maps it to Capacity { resource: "daemon_control_slot", retained, pool_vacant, reserved_vacant, retryable: true }. Legacy EOF still maps to Capacity with retained: null; resets map to Unavailable; a random 24-byte record is never accepted. The notice is unauthenticated, advisory, and admits nothing.
  • Host/operator query — backend.daemon.connection_failures is dispatched to the host (including forwarded Sandboxes) and sandbox.get exposes it best-effort as daemon_capacity with a bounded 2 s wait; the payload is nested admission (pool_vacant, pool_running, reserved_vacant, reserved_running) / retained (limit, ttl_ms, count, dropped, recorded, failures). The read was implemented in 7cfe4460e, 52f9c1666 and 31375c942 (then at api/crates/layerfs-control/src/sandbox/get.rs); the merge 82165e666, resolving the Split the control layer into one crate per concern (layerfs-project#101) #601 control split, rebuilt that file from the base copy and dropped this branch's hunk, so the new control/crates/layerfs-sandbox/src/get.rs had none of it. [v0.1.3] Optimize git_tool_workflow: cold reads, metadata traversal, and backing round trips #68 (88c88fbec) restores it on this head.

That precise Capacity is what the dial reports (Dial::open_within). At the product surface it is not a refusal: direct::unanswered re-codes any refusal that is not Busy/StaleSandbox to Unavailable and drops detail, direct::list reports Unavailable as Unreached, and sandbox.get shows it as down_reason — text only, with the notice's counts in the prose and daemon_capacity omitted. The capacity read cannot fill it either: it needs the same control slot the full pool has none of, so the notice is the only source. Measured on this head with a stand-in daemon that writes only the notice: sandbox.get returns after the 60 s admission window with state: running, workspaces: [], the counts inside down_reason, and no daemon_capacity.

  • One close deadline — all Workspace status probes share one total 2 s close deadline (5 s for unmount), and force close skips the probe so it can stop a refusing daemon.

Tests are stated once, below: on this head cargo test -p layerfs-daemon --lib is 0 tests — the daemon-side invariants still have no in-tree test and are being restored in the stacked follow-up. What this head does carry from #68 is client-side coverage: the pre-Hello capacity notice and its counts (layerfs-sandbox/tests/pool_admission.rs), the bounded and pooled read, the fence and quarantine of a failed control connection (capacity_read.rs), and when sandbox.get shows or withholds daemon_capacity (layerfs-api/tests/sandbox_capacity.rs). Neither set asserts the daemon-side "a retained failure holds no slot" invariant.

Refs #540: do not close it on merge; issue #547 tracks the residual — an admitted in-flight worker still has no deadline or reclaim verb, and with the reserved slot installer-only a saturated control plane can still require a --force close and lose uncommitted Workspaces.

Doctor capacity wiring (delta)

On top of 52f9c1666 (commit c9457e3d4):

  • Live, read-only query — layerfs doctor now asks each Sandbox whose record says it runs and whose daemon endpoint is known (a machine forward, else the endpoint the start recorded in control.json) for backend.daemon.connection_failures, the read model sandbox.get exposes as daemon_capacity. A Sandbox with no recorded endpoint is skipped; nothing is observed, started or written.
  • Strictly bounded — Dial::open_within caps the connect, the Noise Hello and one exchange at 2 s, and a hard 2 s wall around the whole probe means an endpoint that accepts and then stalls returns a warning in about 2 s, not after a 10 s connect timeout or a 60 s admission wait.
  • Warnings, not errors — an unreachable daemon is a warning naming its endpoint and the operation (backend.daemon.connection_failures), with ok still true; the doctor stays read-only and does not fail on an idle or stopped daemon. A capacity refusal is a warning naming Capacity on resource daemon_control_slot with its pool_vacant / reserved_vacant / retained counts.
  • Capacity model — a successful answer adds a warning for retained connection failures (retained <count> of <limit> ... <dropped> dropped) and, when no pool slot is vacant, for the exhausted pool (pool_vacant / pool_running / reserved counts), both pointing at backend.daemon.connection_failures and sandbox.get's daemon_capacity.
  • The report schema is unchanged: every new finding is a warning on the sandbox entity, so no schema redesign was needed.

Tests for this delta: the doctor capacity tests live in api/crates/layerfs-control/tests/doctor.rs (a fake authenticated Noise daemon answers Hello and backend.daemon.connection_failures); on the final head cargo test -p layerfs-control --test doctor → 9 passed (below), including a stalled endpoint bounded to ~2 s with no home writes and no query for a Sandbox without a recorded endpoint. cargo test -p layerfs-cli --test doctor → 2 passed, and cargo check -p layerfs-control -p layerfs-api -p layerfs-cli → clean.

Root cause

Application::serve gave each worker one of the daemon's fixed control slots. When a worker failed after a request had been decoded, the slot became Slot::Retained(cause) and stayed there:

  • only Ok results, and Channel/Io failures with no request and no received bytes, released the slot;
  • a failure after a decoded request (for example, the client disappeared before it could read the reply, so answer_call fails) was retained;
  • take_connection_failure was the only path from Retained back to Vacant, and no product/operator call made it;
  • the retained failure was never logged.

Each such client death permanently cost one of the 8 ordinary control slots. After 8, the accept loop closed every new socket and sandbox.get/close/workspace.* failed Busy after 60 s; only killing the daemon recovered. The client message attributed this to "other processes hold all 8 connections", which was not knowledge the client had.

Fix

  1. Slots do not retain failures. Slots::finish always leaves the slot Vacant; a retained failure is converted to a compact record in a bounded RetainedFailures store. Admission capacity can no longer leak.
  2. Every retained failure is logged to stderr / daemon.log in the same ConnectionFailure { ... } style as released failures. The line names the slot, received bytes, request kind, whether the reply was sent, the bounded cause, and fence/diagnostic errors.
  3. The store is bounded: 64 records, 1 h TTL, oldest-first eviction with a dropped count. It is exposed through the existing daemon operator/control path as backend.daemon.connection_failures, returning admission { pool_vacant, pool_running, reserved_vacant, reserved_running }, retained { limit, ttl_ms, count, dropped, recorded, failures }, and each failure record.
  4. The client refusal is precise. A daemon that closes before Hello now yields Capacity with detail.admission = "no-vacant-slot" (the accept loop's capacity refusal) or Unavailable with detail.admission = "connection-failed" for BrokenPipe/ConnectionReset, plus detail.observed with the I/O kind. It no longer claims other processes hold the connections.
  5. sandbox.close phase stop is bounded. Its Workspace status refresh now uses a 2 s control-admission wait (Controls::lend_within) instead of the ordinary 60 s wait, so a refusing daemon returns an exact refusal promptly instead of holding the close's op intent.

Tests

What runs on this head

  • cargo test -p layerfs-daemon --lib → 0 tests. The daemon crate has no inline #[cfg(test)] mod tests left: the product-boundary guard (tools/check_product_boundary.py) forbids test-only attributes in product src/, and the daemon tests that asserted this PR's invariants lived there. The crate compiles clean, but a clean build is not coverage.
  • The in-tree suites that remain pass on 37cc7711a: cargo test -p layerfs-sandbox --test pool_admission (2 passed), --test capacity_read (6 passed), --test host_frame (5 passed), cargo test -p layerfs-api --test sandbox_capacity (5 passed); cargo fmt --all -- --check clean, check_product_boundary.py --skip-sdk PASS (1349 files), check_fuser_integrity.py exit 0, cargo clippy -p layerfs-sandbox -p layerfs-api --all-targets -- -D warnings clean. Previously on 4dbbcb7a3: cargo test -p layerfs-bridge --test admission --test capacity_model (4 passed), cargo test -p layerfs-bridge --test duplex (9 passed), cargo test -p layerfs-control --test doctor (9 passed), cargo test -p layerfs-api --test workspace_names (2 passed), cargo test -p layerfs-persistence --test sqlite_publication (13 passed).
  • cargo +1.85.1 fmt --all -- --check → clean (pinned 1.85.1); cargo +1.85.1 clippy -p layerfs-control -p layerfs-daemon -p layerfs-bridge --all-targets -- -D warnings → clean, -p layerfs-api -p layerfs-persistence --all-targets → clean. The full workspace clippy was not run locally.
  • python3.12 -B tools/check_product_boundary.py --skip-sdk → PASS: scanned 1238 production Rust/SQL/JavaScript files.

The coverage gap, and where it is being restored

The deleted inline tests were the only direct automated assertions of: a retained failure not consuming a slot (real Application + TCP + Noise, 12 failing requests), finish freeing a retained failure, stale/duplicate finish, guard drop and spawn failure finishing exactly once, the pre-Hello capacity notice, the 16-way concurrent failure storm, and the ring's limit/TTL/eviction/cause bounds. Nothing in-tree asserts those daemon-side invariants today; #68 (merged into this branch as 4f4906aef) adds client-side assertions for the pre-Hello notice and the connection fence, not for slot retention. The Docker E2E below exercises the released-after-logging class (retained.count stays 0), not the retained-record path, and the E2E scripts are not committed.

Issue #547 tracks the restoration of this coverage on the stacked branch fix/547-slot-regression-tests; its PR will be opened separately: package-level tests under engine/crates/layerfs-daemon/tests/ that drive a real daemon process and real TCP clients, so the guard is satisfied without test-only product surface or #[path] recompiles of private modules. This PR does not wait on that PR for the leak fix, but the gap is part of this PR's accounting, and green CI does not close it.

Risk

  • Retained failures no longer keep an in-memory Box<ConnectionFailure>. The record keeps the deciding cause, request kind, reply state, and error text, but not the full decoded request/manifest or body bytes. The previous custody was never readable by any product path; the daemon log and operator query now are.
  • Slot reuse is now immediate. A slot is only reused after its previous worker has exited (serve joins it before spawning the new worker), so no two workers share a slot.
  • The new operator method is additive; existing control traffic is unchanged.
  • The 2 s close-status wait is shorter than the ordinary admission wait. A healthy daemon accepts in milliseconds; a daemon that cannot admit now fails the close with its exact cause instead of stalling it.

Limitation / follow-up

Real Docker E2E (both local engines)

Both runs use real daemons, real SIGKILLed clients, and the same 22-assertion probe. The invariant: a client that dies after its request was sent frees its control slot; a brand-new client is admitted; commit/unmount/close succeed; data survives; the 8 pool slots stay accounted for; one daemon.log line per failure.

OrbStack 29.8.2 — PASS 22/22, 9/9 rounds

  • Sealed Linux bundle build-linux-4fa4864778644346b6aee8de91010a7e, tools/worktree.py run.
  • Each round: fresh client sends workspace.commit (64 MiB change), gets SIGKILLed at +150 ms after the daemon admits it; commit completes, slot returns 295–766 ms later, exactly +1 ConnectionFailure line, a new client process answers sandbox.get in 1.9–6.8 ms.
  • Slot table after every death: pool_vacant 6 + pool_running 2 = 8, reserved_vacant 1, reserved_running 0; no false "other processes hold all 8". The reserved count is the installer-only slot and stayed vacant throughout; it is not a recovery reserve and nothing borrowed it.
  • commit/unmount/remount/close all succeed; 64 MiB cksum identical; close → NoSuchSandbox.

Docker Desktop 29.9.2 — PASS 22/22, 9/9 rounds

  • Product macOS-client topology on the host, Docker Desktop worker via bridge/host.docker.internal; macOS bundle build-macos-adf82922369f41ffa2189792546da006.
  • Container list during the run contains lfs-882b74ef-slots540 and lfs-882b74ef-docker; cleanup 204.
  • Slot freed 722–1765 ms after the kill; +1 log line per round; fresh get 6.3–15.6 ms; same pool accounting, with the installer-only reserved slot still vacant; 64 MiB cksum preserved.

Harness caveat

The pinned Linux-container harness cannot drive machine docker on Docker Desktop: the v2 worker runs with --network host and Desktop does not expose random published loopback ports to that namespace (fixed ports and bridge + host.docker.internal work; minimal repro included). This is a harness limitation, not a #540 product failure. Docker Desktop evidence therefore uses the product's macOS-client topology.

Note on retained.count = 0

A SIGKILLed client whose request was decoded leaves a plain channel EOF: the daemon writes the reply to the dead socket, reads EOF, frees the slot, and logs one ConnectionFailure. That class is released-after-logging, not stored in the bounded RetainedFailures store; the store/retention path has no in-tree test on this head (see the coverage gap) and is the reason retained.count stays 0. The E2E proves slot reclamation, not a retained record.

Install-path retained record — not reachable in this build

We attempted the requested install-path retained-record E2E on both OrbStack and Docker Desktop. Both fail for a product-path reason, not nondeterminism:

  • project.init / store.push write the project record with an installed manifest; sandbox::serve::start_claimed builds DaemonStore::Sqlite { install: Some(existing) }, and the launcher opens the already-pushed Store (setup.opens_existing()), skipping install.
  • On both engines the target /layerfs-store/store.sqlite exists before the container is created (same inode as the worker's pushed file); no store.sqlite.installing transfer file appears; the SIGKILLed create leaves only a channel EOF (original_kind=None), and retained.count stays 0.
  • The install protocol (InitialRecord::Install, install: None) is constructed only by daemon unit fixtures, never by a product launch. Install failures are held as startup custody (startup.install_failure, phase retained), not written to the bounded RetainedFailures store.

So the caveat cannot be removed in this build. Making it testable needs a product path that streams an install, or propagating install failures into the retained record path; both are out of scope for this PR.

CI fix (#543) — head 20d0d48a3

The fmt and guards and clippy -D warnings failures are fixed, and the read model is now one typed contract. All changes are on this Branch only (#544/#542 untouched).

Guard fmt and guards

python3 -B tools/check_product_boundary.py --skip-sdk reported 32 test-only attribute in product source lines from inline #[cfg(test)] mod tests in six product files. All inline test modules are gone from product src/:

  • Kept and relocated to package tests/ (public APIs only): layerfs-bridge admission encoding/classification → engine/crates/layerfs-bridge/tests/admission.rs; the capacity DTO's round-trip/tolerance → engine/crates/layerfs-bridge/tests/capacity_model.rs; doctor capacity warnings → api/crates/layerfs-control/tests/doctor.rs (a fake authenticated Noise daemon answers Hello and backend.daemon.connection_failures, covering healthy, retained + exhausted pool, and a Capacity refusal through the public doctor() call). No test-only public API, no #[path] recompilation of private source, no #[allow].
  • Deleted (private internals, impossible through a public API without new test-only surface): the pool admission unit tests in daemon/pool.rs, doctor/capacity.rs, and the daemon's application/{connection,owner,retained}.rs unit tests. Honest coverage loss: those deleted daemon tests were the only direct tests of retained-failure slot release, the dropped-SlotGuard path, the concurrent failure storm, the pre-Hello capacity notice, and the ring's limit/TTL/cause bounding. The daemon's own --lib target is now empty (0 tests); the specific invariants are covered by review and the real Docker E2E matrix below, not by an automated in-tree test, until the follow-up restores them. The pool's classification messages (EOF → Capacity with unknown retained, reset → Unavailable, notice → exact retained) also lose their unit tests; sandbox.get/doctor still cover the refusal shape indirectly.

Clippy -D warnings

  • Removed the never-constructed NotAdmitted::Starting variant and its match arms (api/crates/layerfs-control/src/daemon/pool.rs); AdmissionReason::Starting (the advisory notice) is still produced and handled.
  • Removing that error revealed a second, branch-introduced clippy::large_enum_variant on Finished::Retained(RetainedFailure) in layerfs-daemon/application/owner.rs (not present on the base). The record is now boxed, as clippy suggests.

Typed capacity model (clean-architecture delta)

  • New engine/crates/layerfs-bridge/src/control_capacity.rs: CapacityModel (AdmissionModel + RetainedModel + typed failure records), parse(&SdkValue) and to_sdk(), plus the single CAPACITY_OPERATION constant. It is a plain DTO — no trait, repository, or test-only surface.
  • The daemon producer (application/connection.rs) now builds that model instead of hand-assembling SdkValues; sandbox.get parses it once and re-publishes exactly the typed shape; doctor/capacity.rs parses once and reads typed fields. The duplicated "backend.daemon.connection_failures" literals in doctor, sandbox.get, sandbox.dispatch, daemon.operator, and the daemon producer now all use CAPACITY_OPERATION.
  • RetainedModel::exhausted/AdmissionModel::exhausted keep the doctor logic small; no probe/findings file split was needed.

Verification on this head

  • python3.12 -B tools/check_product_boundary.py --skip-sdk → PASS: scanned 1238 production Rust/SQL/JavaScript files.
  • cargo +1.85.1 fmt --all -- --check → clean (pinned 1.85.1).
  • cargo +1.85.1 clippy -p layerfs-control -p layerfs-daemon -p layerfs-bridge --all-targets -- -D warnings → clean; -p layerfs-api -p layerfs-persistence --all-targets → clean (the two edited test crates).
  • cargo +1.85.1 test -p layerfs-bridge --test admission --test capacity_model → 4 passed; --test duplex → 9 passed.
  • cargo +1.85.1 test -p layerfs-control --test doctor → 9 passed (6 existing + 3 new capacity tests).
  • cargo +1.85.1 test -p layerfs-daemon --lib → 0 tests (inline unit tests removed; compiles clean).
  • cargo +1.85.1 test -p layerfs-api --test workspace_names → 2 passed; cargo +1.85.1 test -p layerfs-persistence --test sqlite_publication → 13 passed.
  • The full workspace Clippy was not run locally; the touched crates and both edited test crates were.
  • The daemon E2E/FUSE matrix was not re-run (Linux-only; this environment is macOS).

Test-job baseline evidence (pristine layerfs_v2_0_0 @ d2c3f2c97)

Run from a clean git worktree add of the base commit, before any #543 edit:

  • cargo test -p layerfs-api --test workspace_names → FAILED, 0 passed / 2 failed: both tests panicked on machine native's recorded daemon /nonexistent is not there (Unavailable where the test expected Exists / NoSuchSandbox).
  • cargo test -p layerfs-persistence --test sqlite_publication → FAILED, 12 passed / 1 failed: wal_full_settings_and_unavailable_engine_are_explicit panicked at sqlite_publication.rs:63, left: None, right: Some(BackendUnavailable).

Both are stale test fixtures, not product regressions, and were fixed in a separate test-only commit 20d0d48a3 with no product behavior changed:

  • workspace_names: the machine record now points at a placeholder daemon file (never launched, because the Sandbox has no daemon keys) instead of /nonexistent, so the call reaches the name/NoSuchSandbox assertions the test is about.
  • sqlite_publication: removed the leftover BackendUnavailable assertion that selected the fake PostgreSQL backend removed in The connect layer: machines, Sandboxes and Stores as typed, extensible units #535; the test keeps asserting the real SQLite profile. It was renamed wal_full_settings_are_explicit.

After the fix both commands pass (2/2 and 13/13).

Diagrams

Architecture — final implementation, typed layerfs-bridge::ControlCapacity consumed by doctor and sandbox.get:

#540 control-slot custody architecture

Recovery sequence — 8 failures, no daemon kill, slot freed, fresh client admitted:

#540 recovery sequence

…phemeral-AI-Lab#540)

A worker that failed after decoding a request left its admission slot
'Retained' forever: no product path called take_connection_failure, nothing
logged the retained cause, and after all 8 slots were retained the daemon
closed every new connection. sandbox.get/close/workspace.* then failed Busy
and only killing the daemon recovered.

Release the slot immediately for every worker result and move retained
failures into a bounded, TTL-pruned operator-visible store (limit 64, TTL
1h). Log every retained failure to stderr/daemon.log in the same
ConnectionFailure style as released ones, including request kind, whether a
reply was sent, and the bounded cause. Expose the store through
backend.daemon.connection_failures alongside vacant/running slot counts.

Make the client refusal name only what it observed: a daemon that closes
before Hello has no vacant control slot (Busy), while a broken/reset
connection is a connection failure (Unavailable); stop claiming that other
processes hold all connections. Bound sandbox.close's phase-stop status
refresh to 2s so a refusing daemon yields an exact refusal instead of holding
the op intent for the full 60s admission wait.

Tests: an end-to-end daemon test sends 12 failing requests that die before a
reply, asserts every slot is reclaimed, the failures are stored/queryable and
a new authenticated client is admitted; unit tests cover the bounded ring,
TTL pruning and the precise client refusal messages.
…eral-AI-Lab#540)

Extend the slot reclamation fix to the full recovery design while keeping
the released-slot invariant.

- Slot table: pool and reserved classes with generation-checked SlotTicket.
  Admission scans only its own class; a pool claim never takes the reserved
  installer slot and vice versa. A stale or duplicate finish changes nothing.
- Panic/spawn safety: the worker runs under catch_unwind and owns a SlotGuard.
  Exactly one finish happens on the normal path, a panic, a dropped thread or
  a spawn failure; the synthetic cause is recorded, not lost.
- Retained store: records now carry class, generation, owner (instance, pid,
  thread), visible count and expiry; the ring pre-reserves capacity, tracks
  recorded and is reaped lazily when admission is refused. Still 64 / 1h.
- Capacity notice: a fixed advisory pre-handshake LAYERFS-CAPACITY frame
  (version/reason/retained/pool_vacant/reserved_vacant) is written before
  closing when no slot is free. The client decodes it before the Noise
  message and returns Capacity { resource: "daemon_control_slot", retained,
  retryable: true }; a bare EOF still maps to Capacity with retained null,
  resets map to Unavailable, and a random record is never accepted.
- Host query: backend.daemon.connection_failures is dispatched to the host,
  works for forwarded Sandboxes, and sandbox.get exposes it best-effort as
  daemon_capacity. The operator query is nested admission/retained.
- Close/unmount deadline: refresh uses one total deadline for all Workspace
  status probes (close 2s, unmount 5s), and force close skips the probe so it
  can stop a refusing daemon instead of waiting N times.

Tests: daemon 13 (class isolation, stale finish, guard release, capacity
notice, 16 concurrent failures, retained store); control 8 (notice -> precise
Capacity, EOF fallback, reset -> Unavailable, garbage rejected); bridge 2.
…Lab#540)

The new best-effort daemon capacity read model must not inherit the
ordinary 60 s admission wait, or sandbox.get itself could stall behind a
refusing daemon. It now uses a 2 s control wait; product calls keep the
ordinary wait.
@mengmengmengqiang
mengmengmengqiang marked this pull request as ready for review November 4, 2026 06:05
…#540)

The doctor now asks each running Sandbox whose daemon endpoint its
record or control.json names for the same
backend.daemon.connection_failures read model sandbox.get exposes as
daemon_capacity. The query is best-effort and strictly bounded to 2 s
(connect, Noise Hello and one exchange), reuses Dial::open_within, and
runs under a hard wall so an endpoint that accepts and stalls cannot
hold the doctor.

A daemon that does not answer is a warning naming its endpoint and the
operation, never an error: the doctor stays read-only and does not fail
on an idle or stopped daemon, and it starts no machine or Sandbox. An
admission-capacity refusal is reported as Capacity on resource
daemon_control_slot with its vacant-slot counts; a successful model adds
warnings for retained connection failures (count of limit, dropped) and
for a pool with no vacant control slot (pool_vacant/running), pointing
at backend.daemon.connection_failures.

Tests: layerfs-control --lib doctor 3 passed; --test doctor 6 passed
(stalled endpoint bounded in ~2 s, no home writes, no endpoint means no
query); layerfs-cli --test doctor 2 passed.
…pacity model (Ephemeral-AI-Lab#543)

- Remove every inline #[cfg(test)] module from product src/: the bridge
  admission encoding/classification tests move to
  engine/crates/layerfs-bridge/tests/admission.rs; the private pool,
  doctor, daemon connection/owner/retained unit tests are deleted because
  they cannot reach the code through a public API without new test-only
  surface. Their coverage is replaced by public-behavior tests (doctor
  capacity warnings via a fake authenticated daemon, bridge capacity DTO)
  and the existing real-TCP/in-process suites.
- Remove the never-constructed NotAdmitted::Starting variant and its
  match arms; AdmissionReason::Starting (the daemon notice) stays.
- Box Finished::Retained's record to satisfy clippy::large_enum_variant
  now that the Starting error no longer masks it.
- Add a typed CapacityModel/AdmissionModel/RetainedModel DTO to
  layerfs-bridge as the one wire contract: the daemon producer builds it,
  sandbox.get and doctor parse it once, and OPERATION replaces the
  duplicated method-name literal.
- Relocate the doctor capacity-warning tests to public doctor() calls
  against a fake authenticated daemon (healthy, retained+exhausted, and
  Capacity refusal).
…phemeral-AI-Lab#543)

These failed at d2c3f2c before any Ephemeral-AI-Lab#543 change and are test-only:

- workspace_names pointed the native machine's daemon at /nonexistent,
  which the pre-launch is_file check now refuses (Unavailable) before the
  name/NoSuchSandbox assertions. Write a placeholder daemon file instead.
- sqlite_publication expected SqliteHandles::create to return
  BackendUnavailable, a leftover from the removed fake-PostgreSQL backend
  selection (Ephemeral-AI-Lab#535). SQLite always has an engine on macOS/Linux, so the
  stale assertion is removed and the test keeps asserting the real
  profile.

No product behavior is changed to make these pass.
mengmengmengqiang added a commit to tokenaissance/layerfs that referenced this pull request Nov 4, 2026
…phemeral-AI-Lab#543)

These failed at d2c3f2c before any Ephemeral-AI-Lab#543 change and are test-only:

- workspace_names pointed the native machine's daemon at /nonexistent,
  which the pre-launch is_file check now refuses (Unavailable) before the
  name/NoSuchSandbox assertions. Write a placeholder daemon file instead.
- sqlite_publication expected SqliteHandles::create to return
  BackendUnavailable, a leftover from the removed fake-PostgreSQL backend
  selection (Ephemeral-AI-Lab#535). SQLite always has an engine on macOS/Linux, so the
  stale assertion is removed and the test keeps asserting the real
  profile.

No product behavior is changed to make these pass.
mengmengmengqiang added a commit to tokenaissance/layerfs that referenced this pull request Nov 4, 2026
…base-proof claim (Ephemeral-AI-Lab#547)

- Commit the base half of the proof as core/docs/issues/547/pre543_repro.rs,
  with the command that runs it at d2c3f2c. It is not built with this branch:
  the restored suite names slot types that do not exist on the base.
- Record the actual result of that run: nine deaths, then the tenth client is
  refused with Io(Kind(UnexpectedEof)) at pre543_repro.rs:166:6.
- Correct the slice-1 claims: the real-process deaths fail at
  `InitialRecord::decode`, so they are not a decoded-request/reply-lost path;
  the guard-drop test covers `Slots::abandon`, not the `Drop` wiring;
  `NoPoolSlot` has no real-process case; and the client's decoded-notice
  mapping lost its test when the refusal message builders went private.

Production LOC: 138203 -> 138203 (delta 0). Documentation and a test fixture
that is not built with the branch.
mengmengmengqiang added a commit to tokenaissance/layerfs that referenced this pull request Nov 5, 2026
…phemeral-AI-Lab#543)

These failed at d2c3f2c before any Ephemeral-AI-Lab#543 change and are test-only:

- workspace_names pointed the native machine's daemon at /nonexistent,
  which the pre-launch is_file check now refuses (Unavailable) before the
  name/NoSuchSandbox assertions. Write a placeholder daemon file instead.
- sqlite_publication expected SqliteHandles::create to return
  BackendUnavailable, a leftover from the removed fake-PostgreSQL backend
  selection (Ephemeral-AI-Lab#535). SQLite always has an engine on macOS/Linux, so the
  stale assertion is removed and the test keeps asserting the real
  profile.

No product behavior is changed to make these pass.
mengmengmengqiang added a commit to tokenaissance/layerfs that referenced this pull request Nov 5, 2026
…phemeral-AI-Lab#543)

These failed at d2c3f2c before any Ephemeral-AI-Lab#543 change and are test-only:

- workspace_names pointed the native machine's daemon at /nonexistent,
  which the pre-launch is_file check now refuses (Unavailable) before the
  name/NoSuchSandbox assertions. Write a placeholder daemon file instead.
- sqlite_publication expected SqliteHandles::create to return
  BackendUnavailable, a leftover from the removed fake-PostgreSQL backend
  selection (Ephemeral-AI-Lab#535). SQLite always has an engine on macOS/Linux, so the
  stale assertion is removed and the test keeps asserting the real
  profile.

No product behavior is changed to make these pass.
mengmengmengqiang added a commit to tokenaissance/layerfs that referenced this pull request Nov 5, 2026
…phemeral-AI-Lab#543)

These failed at d2c3f2c before any Ephemeral-AI-Lab#543 change and are test-only:

- workspace_names pointed the native machine's daemon at /nonexistent,
  which the pre-launch is_file check now refuses (Unavailable) before the
  name/NoSuchSandbox assertions. Write a placeholder daemon file instead.
- sqlite_publication expected SqliteHandles::create to return
  BackendUnavailable, a leftover from the removed fake-PostgreSQL backend
  selection (Ephemeral-AI-Lab#535). SQLite always has an engine on macOS/Linux, so the
  stale assertion is removed and the test keeps asserting the real
  profile.

No product behavior is changed to make these pass.
mengmengmengqiang added a commit to tokenaissance/layerfs that referenced this pull request Nov 5, 2026
…phemeral-AI-Lab#543)

These failed at d2c3f2c before any Ephemeral-AI-Lab#543 change and are test-only:

- workspace_names pointed the native machine's daemon at /nonexistent,
  which the pre-launch is_file check now refuses (Unavailable) before the
  name/NoSuchSandbox assertions. Write a placeholder daemon file instead.
- sqlite_publication expected SqliteHandles::create to return
  BackendUnavailable, a leftover from the removed fake-PostgreSQL backend
  selection (Ephemeral-AI-Lab#535). SQLite always has an engine on macOS/Linux, so the
  stale assertion is removed and the test keeps asserting the real
  profile.

No product behavior is changed to make these pass.
mengmengmengqiang added a commit to tokenaissance/layerfs that referenced this pull request Nov 5, 2026
…phemeral-AI-Lab#543)

These failed at d2c3f2c before any Ephemeral-AI-Lab#543 change and are test-only:

- workspace_names pointed the native machine's daemon at /nonexistent,
  which the pre-launch is_file check now refuses (Unavailable) before the
  name/NoSuchSandbox assertions. Write a placeholder daemon file instead.
- sqlite_publication expected SqliteHandles::create to return
  BackendUnavailable, a leftover from the removed fake-PostgreSQL backend
  selection (Ephemeral-AI-Lab#535). SQLite always has an engine on macOS/Linux, so the
  stale assertion is removed and the test keeps asserting the real
  profile.

No product behavior is changed to make these pass.
mengmengmengqiang added a commit to tokenaissance/layerfs that referenced this pull request Nov 5, 2026
…phemeral-AI-Lab#543)

These failed at d2c3f2c before any Ephemeral-AI-Lab#543 change and are test-only:

- workspace_names pointed the native machine's daemon at /nonexistent,
  which the pre-launch is_file check now refuses (Unavailable) before the
  name/NoSuchSandbox assertions. Write a placeholder daemon file instead.
- sqlite_publication expected SqliteHandles::create to return
  BackendUnavailable, a leftover from the removed fake-PostgreSQL backend
  selection (Ephemeral-AI-Lab#535). SQLite always has an engine on macOS/Linux, so the
  stale assertion is removed and the test keeps asserting the real
  profile.

No product behavior is changed to make these pass.
@mengmengmengqiang mengmengmengqiang self-assigned this Nov 6, 2026
@yhcedpn yhcedpn added the bug Something isn't working label Nov 6, 2026
@yhcedpn yhcedpn added this to the LayerFS v2.0.0 milestone Nov 6, 2026
@Mola-maker

Copy link
Copy Markdown
Member

Hey @mengmengmengqiang, PR #543 is currently conflicting with recent layerfs_v2_0_0 updates. Could you rebase onto the latest v2 base when convenient? Thanks!

@mengmengmengqiang

Copy link
Copy Markdown
Contributor Author

CI note

The single failing check on this PR is not caused by these changes:

layerfs-store-service::sdk_blame_coarse::lines_between_distant_edits_keep_their_origin

It is a pre-existing base regression from #517 (commit ca0d60a77), which fixed layerfs-content's diff but not layerfs-store-service's line_alignment::exact. No layerfs-store-service file is touched in this PR. Tracked in #638.

This PR's own checks pass: fmt, clippy -D warnings, product-boundary guard, fuser integrity, daemon/bridge/control-doctor tests.

…phemeral-AI-Lab#540)

`sandbox.get` now carries `daemon_capacity`: the daemon's
`backend.daemon.connection_failures` read model, the one `layerfs doctor`
explains and its fix text already pointed at. The field is best-effort and
bounded to 2 s: the query runs in its own thread behind a hard wall, so a
daemon that accepts and stalls cannot hold the read, and a daemon that does
not answer (stopped, gone, at no known endpoint, or holding no vacant
control slot) leaves the field out instead of failing `sandbox.get`. The
field is only added for a running Sandbox, and nothing is started to find
out.

The one query itself moved out of the doctor into
`layerfs_sandbox::daemon::capacity` (`endpoint`, `read`, `OPERATION`,
`WAIT`), so the doctor and the read model share one implementation: where
the daemon is, how long an answer may take, and what an unanswered query is
are decided once.

Tests: layerfs-api --test sandbox_capacity 2 passed — a stand-in daemon's
model reaches `sandbox.get` with its admission counts and retained failure
record, and a daemon that reads the query and never answers leaves the field
out after the bounded wait while the get still answers.
layerfs-control --test doctor unchanged and passing (6 passed). Clippy
-D warnings clean on layerfs-api, layerfs-control, layerfs-bridge and
layerfs-sandbox; no test failures in those four or layerfs-daemon.

Production LOC: 156991 -> 157011 (delta +20); api 4966 -> 4975 (+9),
control +11. Counted with tools/production_loc.py over core/crates/*/src,
engine/crates/*/src, control/crates/*/src and api/crates/*/src (tests,
docs, manifests and generated artifacts excluded).
…ads (Ephemeral-AI-Lab#540)

A daemon whose pool has no vacant slot answers the next connection with
its advisory notice before any handshake. This pins what a client makes of
it: `Dial::open_within` refuses `Capacity` on resource
`daemon_control_slot` with the notice's vacant-slot and retained counts,
retryable because nothing was attempted; a daemon that is still starting
is `Unavailable` with admission `starting` instead, so a full pool and a
starting one stay distinguishable.

Measured against a real macOS daemon before this test existed (LP543-PATCH
§4): with eight admitted clients holding every pool slot, the ninth raw
connection read
`notice version=1 reason=NoPoolSlot retained=0 pool_vacant=0 reserved_vacant=1`.

Tests: layerfs-sandbox --test pool_admission 2 passed. Clippy
-D warnings clean.

Production LOC: 157011 -> 157011 (delta 0); a test-only commit.
…ral-AI-Lab#543)

`sandbox.get` shows `daemon_capacity`, the daemon's
`backend.daemon.connection_failures` read model, but only through the half of
it that survived the `layerfs_v2_0_0` merge (82165e6): the API layer parsed
an endpoint and dialed it itself. That half reaches neither a `docker` nor an
`apple-container` Sandbox, whose daemon is behind a machine's worker; it dials
a forwarded Sandbox's old port after its machine stopped or its forward exited
because it skips `direct::check`; and it opened a second control connection per
read, so one `sandbox.get` took two of the daemon's slots and, with seven slots
held by other clients, reported no capacity at all - the field the doctor's own
fix text tells an operator to read.

This restores the operation author's design from 7cfe446, 52f9c16 and
31375c9 (all present before that merge, lost when it resolved Ephemeral-AI-Lab#540's
conflicts): the read is a host operation again (`Call::Capacity`,
`backend.daemon.connection_failures`), routed like the Workspace list it
follows - `direct::capacity` for a forwarded Sandbox, its machine's worker for
the rest - so `native`, `lima`, `docker` and `apple-container` all answer it,
and `api/crates/layerfs-api/src/sandbox/get.rs` is a thin shell over the typed
`Got` again.

It is asked only for a `running` Sandbox whose machine and forward were
reached, on the connection the Workspace list lent from the Sandbox's pool (one
connection, one slot), within 2 s for admission and for the exchange, and
fenced at that deadline (`daemon/fence.rs`, the pattern the doctor's bounded
observation uses): a daemon that trickles its handshake or its reply is shut
down rather than left holding a thread, a connection or a slot.

Doctor behavior and wording are unchanged. Its own query now ends its control
session instead of dropping the socket, which the daemon logged as an
unexpected end.

Docs: `docs/architecture/api/sdk/sandbox/get.md`,
`docs/architecture/api/sdk/sandbox/README.md`,
`docs/architecture/control/routing/daemon-channel.md`.

Production LOC: 157011 -> 157130 (delta +119)
Scope and method: first-party production Rust/SQL/JavaScript in core, engine,
control and api (tests, fixtures, tools and docs excluded), counted with
`tools/production_loc.py` at the parent commit 8978c2e and the committed
tree. control 38406 -> 38524 (+118), api 4975 -> 4976 (+1); core 39749 and
engine 73881 unchanged.
Reference absent (retired R9c; last counted 65,417 at tree 498dd19).
…phemeral-AI-Lab#543)

`sandbox.get`'s `daemon_capacity` (Ephemeral-AI-Lab#540) had one test of the shown answer and
one of the 2 s bound, both on a forwarded Sandbox. This covers what the
restored route decides:

- `api/crates/layerfs-api/tests/sandbox_capacity.rs`: the Workspace list and
  the capacity read share one connection (the stand-in daemon counts them), a
  stopped machine with no worker record, an ended forward with a recorded
  process, and a Sandbox with no id are never dialed and answer in well under a
  second, and a daemon that reads the query and never answers still returns the
  get at its bound.
- `control/crates/layerfs-sandbox/tests/capacity_read.rs`: a query ends its
  control session (`EndSession`) instead of dropping the socket, a trickling
  daemon is fenced at the deadline (its own write fails, so the connection was
  shut down), two pooled reads take the one connection the pool holds, and the
  host operation a machine's worker runs (`dispatch::call`, `Call::Capacity`)
  returns the model.
- `control/crates/layerfs-sandbox/tests/host_frame.rs`: the `sandbox.host`
  frame carries `backend.daemon.connection_failures` and reads back as
  `Call::Capacity`.

Production LOC: 157130 -> 157130 (delta 0)
Scope and method: first-party production Rust/SQL/JavaScript in core, engine,
control and api (tests, fixtures, tools and docs excluded), counted with
`tools/production_loc.py` at the parent commit 88c88fb and the committed
tree; this commit changes tests only, so the production total is unchanged.
Reference absent (retired R9c; last counted 65,417 at tree 498dd19).
…ld table (Ephemeral-AI-Lab#543)

The `daemon_capacity` row linked an issue URL that the guides do not have a
record of; it cites the issue the way the surrounding documents do, `(Ephemeral-AI-Lab#540)`.

Production LOC: 157130 -> 157130 (delta 0)
Scope and method: first-party production Rust/SQL/JavaScript in core, engine,
control and api (tests, fixtures, tools and docs excluded), counted with
`tools/production_loc.py` at the parent commit 811f59e and the committed
tree; this commit changes one documentation line, so the production total is
unchanged.
Reference absent (retired R9c; last counted 65,417 at tree 498dd19).
…eral-AI-Lab#543)

`capacity::lent` (the `sandbox.get` capacity read, Ephemeral-AI-Lab#543) arms a fence on the
pool connection it just borrowed, and `Lease::drop` decides reuse from
`Control::is_failed()`. That answer was only `Control.failed`, which a fence's
end never sets: `CloseHandle::close` (layerfs-bridge) sets the channel's own
shared `failed` bit and shuts its socket down, and the `Control` above it
learned of that only when a call it sent subsequently failed.

The window, in order:

1. `trace::control_within` borrows a connection and `Fence::channel` arms it
   at the read's deadline.
2. `exchange` returns `Ok(answer)`: `Control.failed` is still false and the
   channel is healthy.
3. Before `drop(fence)` sends its stop, the fence thread's deadline expires
   and `CloseHandle::close()` quarantines the channel and shuts both
   directions down. `drop(fence)` joins that thread, so the close has
   completed before the lease is returned.
4. `Lease::drop` asks `Control::is_failed()`, hears false, and pushes the
   dead connection back into `pool.idle`.
5. The next call in the same long-lived process (a machine's worker, an SDK
   binding) pops it. `Sender::send` refuses at `shared.ready()` with
   `Quarantined` before writing a byte, but `Control::call_with` had already
   set `attempted = true`, so the caller is told `phase: Send, attempted:
   true` for an operation that never left.

`capacity::lent` widened that window: its deadline was taken before
`control_within(controls, WAIT)`, which re-reads the clock and may use the
whole 2 s, so a read admitted at the boundary armed a fence already 0 ms from
firing - a shutdown racing a reply that took microseconds.

Fix, at the three points the ordering above crosses:

- `layerfs-bridge`: `Connection::is_quarantined()` reads the one shared bit
  that every direction and close handle write.
- `Control::is_failed()` is that bit or `Control.failed`, and `call_with` and
  `install_file` refuse on it before setting `attempted`. A quarantined
  connection now reports `phase: Encode, attempted: false` (nothing can have
  taken effect), and `Lease::drop` closes it instead of lending it again.
- `capacity::lent` checks the window left after admission and refuses
  (`Unavailable`, nothing sent) instead of arming a spent deadline, which
  would shut a healthy connection down for nothing.

Tests (`control/crates/layerfs-sandbox/tests/capacity_read.rs`): a fenced pool
connection is not lent again (the next read opens the second connection and
answers instead of reporting a quarantine), and a quarantined connection's
call is `Encode` with `attempted: false`. With `is_failed` reverted to its
previous form both fail; the first reproduces the reviewed symptom exactly
(`phase: Send, attempted: true, cause: Channel(Quarantined)`).

Docs: `docs/architecture/control/routing/daemon-channel.md` (the pool's drop
line and the `Control` row) and
`docs/architecture/api/sdk/sandbox/get.md` (what the fence and the spent
admission window decide).

Production LOC: 157130 -> 157143 (delta +13)
Scope and method: first-party production Rust/SQL/JavaScript in core, engine,
control and api (tests, fixtures, tools, docs, manifests and generated
artifacts excluded), counted with `tools/production_loc.py` at the parent
commit de009a8 and the committed tree. engine 73881 -> 73884 (+3,
`duplex::Connection::is_quarantined`), control 38524 -> 38534 (+10,
`daemon/capacity.rs` and `daemon/channel.rs`); core 39749 and api 4976
unchanged.
Reference absent (retired R9c; last counted 65,417 at tree 498dd19).
@jiangkoumo

Copy link
Copy Markdown
Collaborator

@mengmengmengqiang 你好,我给这个 PR 补了几处缺口,开在你的分支上:tokenaissance#68

主要是:sandbox.get 的 daemon_capacity 读取在 82165e666 合并时遗落了(7cfe4460e 等提交里原本已实现),现按原设计恢复;另外修了隔离连接会被放回池的问题,并补了测试。Linux 全量 fast 1839/1840(唯一失败 sdk_blame_coarse 来自上游 #638,#639 已修)、slow 146/146、mount 45/0,真实 daemon 满池场景也验证过。方便时看一下,有问题我再改。

fix(sandbox): 恢复 sandbox.get 的 daemon_capacity 读取,并修隔离连接回池问题(Ephemeral-AI-Lab#543)
@mengmengmengqiang
mengmengmengqiang merged commit e9b903e into Ephemeral-AI-Lab:layerfs_v2_0_0 Nov 11, 2026
NelsonLongxiang added a commit to NelsonLongxiang/layerfs that referenced this pull request Nov 11, 2026
…ral-AI-Lab#543)

# Conflicts:
#	engine/crates/layerfs-daemon/src/application/mod.rs
mengmengmengqiang added a commit to tokenaissance/layerfs that referenced this pull request Nov 11, 2026
…sh the state machine (Ephemeral-AI-Lab#547)

Slice 1: restore the four deleted daemon assertions (the pre-Ephemeral-AI-Lab#543 connection
tests) as package integration tests, and make the pure Slots/RetainedFailures
state machine public behind the #[doc(hidden)] internals module so it is
unit-testable from tests/. No behavior change.

- engine/crates/layerfs-daemon/tests/daemon_slots.rs: the four real-daemon
  custody assertions over TCP (undecodable records free their slot, the
  capacity notice on a full pool, and a concurrent failure storm).
- engine/crates/layerfs-daemon/tests/slot_state.rs: the deleted owner/retained
  unit assertions plus the guard-drop release, via the public state machine.
- application/internals.rs: #[doc(hidden)] re-export of the pub'd state-machine
  values; application/{owner,retained}.rs: Slots/RetainedFailures and their
  methods are now pub.
mengmengmengqiang added a commit to tokenaissance/layerfs that referenced this pull request Nov 11, 2026
…sh the state machine (Ephemeral-AI-Lab#547)

Slice 1: restore the four deleted daemon assertions (the pre-Ephemeral-AI-Lab#543 connection
tests) as package integration tests, and make the pure Slots/RetainedFailures
state machine public behind the #[doc(hidden)] internals module so it is
unit-testable from tests/. No behavior change.

- engine/crates/layerfs-daemon/tests/daemon_slots.rs: the four real-daemon
  custody assertions over TCP (undecodable records free their slot, the
  capacity notice on a full pool, and a concurrent failure storm).
- engine/crates/layerfs-daemon/tests/slot_state.rs: the deleted owner/retained
  unit assertions plus the guard-drop release, via the public state machine.
- application/internals.rs: #[doc(hidden)] re-export of the pub'd state-machine
  values; application/{owner,retained}.rs: Slots/RetainedFailures and their
  methods are now pub.
@mengmengmengqiang

Copy link
Copy Markdown
Contributor Author

Problems this PR (and its follow-ups) fix

This PR is the base of the daemon-slot stack: #543 -> #650 -> #651 -> #652 -> #655. Together they close:

Position in the stack: base. Merge order is #543 -> #650 -> #651 -> #652 -> #655; each follow-up is stacked on the previous one and retargeted to layerfs_v2_0_0 as the one below it merges.

Why split into separate PRs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants