mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-23 23:29:32 +00:00
docs: C6 5.4 close-out dispositions (design, packet section 10, architecture row, ledger)
Lens findings on b4938c31 disposed: the ENOLCK tier is a recorded name tier with a
per-caller refusal (money only), the unprobeable-directory answer is enforced and
UNCACHED, the reset-beside-archive verdict lasts until the fresh ledger's epoch
passes the surviving segments (not "for the life of the install"), a UNIX socket
is corruption on the dir-fd shape and skipped by the path shape's stat-before-open,
and pid_is_alive's EPERM->alive flip is named as the shared liveness primitive it
is (custody settlements, claim reclaims, staging reaps defer too). Packet section 10
and the ledger carry the red-first rows and the two disclosed-not-fixed items
(heartbeat-ownership pin, R3 pin) with the reason: both usage suites sit at their
line caps, an owner decision still owed.
This commit is contained in:
parent
fc645a1b2e
commit
8fb08d44b5
4 changed files with 81 additions and 26 deletions
File diff suppressed because one or more lines are too long
|
|
@ -264,7 +264,8 @@ rounding, folding of dispatched rows — each flips at least one pin.
|
|||
`O_NONBLOCK`, so it cannot hang the question either *(qualified, round 5.4:
|
||||
an entry that OPENS but is not a regular file is skipped; one the kernel
|
||||
refuses to open at all — a UNIX socket, ENXIO — is corruption like any
|
||||
other unopenable entry)*; an entry the scan
|
||||
other unopenable entry on the dir-fd shape; the path shape's stat-before-open
|
||||
classifies a socket as not regular and skips it)*; an entry the scan
|
||||
cannot list, open or read IS corruption — the scan did not complete, the
|
||||
data root's own handle included since round 5.3, and since round 5.4 every
|
||||
path inspection the reader makes (the symlink bounds on both archive levels
|
||||
|
|
@ -273,8 +274,10 @@ rounding, folding of dispatched rows — each flips at least one pin.
|
|||
escaping as a bare `OSError` or a silent empty answer. Disclosed since
|
||||
round 5.4: with a stamp-less live file every parsable regular file in the
|
||||
archive that is not its byte-prefix is `generation newer`, so a ledger
|
||||
reset beside a surviving archive is a PERMANENT corruption verdict on every
|
||||
history question (reset both together) and an operator's stray JSON file
|
||||
reset beside a surviving archive is a `generation newer` corruption verdict
|
||||
on every history question until the fresh ledger's epoch passes the
|
||||
surviving segments, which are then silently ignored (reset both together)
|
||||
and an operator's stray JSON file
|
||||
there is corruption on a stamp-less ledger only; and the readers being
|
||||
lock-free, a compaction that commits between a question's live-header read
|
||||
and its anchor scan makes that ONE question UNKNOWN (`generation newer`),
|
||||
|
|
@ -303,11 +306,16 @@ rounding, folding of dispatched rows — each flips at least one pin.
|
|||
EOPNOTSUPP/ENOTSUP/ENOSYS *(correction, round 5.4: ENOLCK was in the set
|
||||
until then — "no locks available" is a missing lock daemon OR an exhausted
|
||||
kernel lock table, not a capability answer, and it selected the tier where
|
||||
the round-3 race returns; it now keeps the enforced tier and every live
|
||||
acquisition the kernel refuses with it fails closed, so a lockd-less NFS
|
||||
refuses every monetary write with `UsageAccountingError` instead of running
|
||||
the name protocol; the per-directory verdict is decided ONCE under a module
|
||||
lock, so racing threads share one probe — §9 "Round 5.4", R1)*, and since
|
||||
the round-3 race returns; round 5.4 made it keep the enforced tier and fail
|
||||
EVERY live acquisition closed — product-wide, the lenses showed, since the
|
||||
primitive is shared — so the close-out made ENOLCK the name tier with its
|
||||
errno RECORDED and a per-caller refusal: only the monetary lock names it
|
||||
(`refuse_name_tier_errnos={ENOLCK}`), so a lockd-less NFS refuses every
|
||||
monetary write with `UsageAccountingError` instead of running the name
|
||||
protocol while every other lock keeps the protocol it always ran there;
|
||||
the per-directory verdict is decided ONCE under a module lock, so racing
|
||||
threads share one probe; an unprobeable directory answers enforced for
|
||||
that call, uncached — §9 "Round 5.4" and §10, R1)*, and since
|
||||
round 5.3 the two Win32 answers of a volume without byte-range locks —
|
||||
ERROR_INVALID_FUNCTION, which LockFileEx answers on `\\wsl$`
|
||||
(microsoft/WSL#5762), and ERROR_NOT_SUPPORTED, error 50 on a Samba share —
|
||||
|
|
@ -334,7 +342,9 @@ rounding, folding of dispatched rows — each flips at least one pin.
|
|||
mechanism: `pid_is_alive` read EPERM as DEAD, so another user's recycled
|
||||
pid was reclaimed through the age path (flock-guarded on the enforced
|
||||
tier) and only a same-uid recycle wedged — the opposite of what round 5.3
|
||||
wrote; EPERM now reads alive (the process exists), so ANY live impostor —
|
||||
wrote; EPERM now reads alive (the process exists) — in `pid_is_alive`, the
|
||||
one liveness primitive shared by every consumer (custody settlements, claim
|
||||
reclaims, staging reaps defer for such a pid too) — so ANY live impostor —
|
||||
same uid or another — wedges the lock from the 90 s staleness window
|
||||
(`usage_ledger._locked`, `stale_sec=90.0`) until it exits, the probe
|
||||
flock deliberately unconsulted while the pid reads alive (a mixed-tier
|
||||
|
|
@ -857,3 +867,21 @@ change), `d99ff6a9` (R3 + R4), `ea4d4337` (R5), `9306f962` (R6),
|
|||
ARCHITECTURE row, PERSISTENCE, this section and the ledger — follows. Gate
|
||||
evidence: `docs/v7next/LEDGER_CORRECTIONS.md` §"From the C6 micro-round 5.4
|
||||
(owner batch №12 A, base 096437c2)".
|
||||
|
||||
## 10. Round 5.4 close-out — three read-only lenses on `b4938c31`, operator disposition (owner batch №12 A)
|
||||
|
||||
Verdicts: 3 × NEEDS_FIXES, no HIGH; 3 MEDIUM + 7 LOW. Fixed here (base `b4938c31`), pinned red-first:
|
||||
|
||||
| finding | disposition | pin → pre-fix shape → observed red |
|
||||
|---|---|---|
|
||||
| MEDIUM R1 (two lenses) — ENOLCK fail-closed landed in the SHARED primitive: on a lockd-less NFS `state/` every `acquire_exclusive_file_lock` caller failed, no model call could dispatch; the owner decided "compaction refuses", not this | **fixed**: ENOLCK is the name tier with its errno recorded beside the verdict (`_KERNEL_LOCK_TIER[dir] = (enforced, errno)`); `acquire_exclusive_file_lock(refuse_name_tier_errnos=…)` lets a caller fail closed on a recorded errno; only `usage_ledger._named_lock` names ENOLCK. Ordinary locks keep the name protocol they always ran there; money refuses typed | `test_the_capability_probe_decides_once_and_leaves_no_residue` (ENOLCK clause) and `test_enolck_is_the_name_tier_for_ordinary_locks_and_a_typed_refusal_for_money` → `platform_layer.py`/`usage_ledger.py` @ `b4938c31` → `assert True == (True, 5)` (a bare bool cached, ENOLCK enforced) / `assert True is False` |
|
||||
| MEDIUM R4 — `_segment_path` resolved with `Path.resolve(strict=False)` one line BEFORE the typed `is_symlink()`: a symlink loop escaped as `RuntimeError("Symlink loop …")`, a readlink race as bare `OSError` | **fixed**: `os.path.realpath` (non-strict, never raises on a loop) inside the same `try`, `except (OSError, RuntimeError)` → `UsageLedgerCorrupt` | `test_a_path_inspection_the_reader_cannot_make_is_typed_corruption` (self-loop clause) → `usage_compaction.py` @ `b4938c31` → `RuntimeError: Symlink loop from …` and `OSError: [Errno 40] Too many levels of symbolic links` |
|
||||
| LOW R3 — the stamp-less ENOENT exemption used a FOLLOWING `stat`: a dangling link at either archive level answered a silent empty set where the stamped reader answers corruption | **fixed**: `lstat` both levels first; `S_ISLNK` → typed `usage archive path is a symlink`, other `OSError` → typed `cannot be inspected`; pin deferred (the compaction suite sits at its 1600-line cap, disclosed below); mutation-verified by hand on this host (dangling link at `archive/` → typed) | — |
|
||||
| LOW R5 — the old-inode witness was opened by PATH before the proof and not tied to the inode the precondition proved; a vanished ledger at the witness open was a bare `OSError` | **fixed**: `owned_and_intact` also proves `fstat(old_fd)` and `stat(path)` name one inode; the witness open is wrapped into `_Abort` (an abort by policy) | — (behaviour-preserving strengthening; no pin, disclosed) |
|
||||
| LOW R1 — DESIGN §8 "decides once … cached" was absolute; an unprobeable directory answers enforced UNCACHED | **docs**: DESIGN §8, packet §5.10/§9, ARCHITECTURE row | — |
|
||||
| LOW R6 — the strengthened heartbeat pin proves renewal + True, not ownership: a lock-TOUCHING stub survives it | **disclosed, not fixed**: the pin proves the callable renews THIS lock file's age (the production wire's only observable) — a stub that touches the production lock path is a contrived mutation; the suite is at its line cap | — |
|
||||
| LOW R7 — EPERM→alive is a flip of a primitive shared by 12 non-test consumers, disclosed only for the monetary lock | **docs**: DESIGN §8 residual, packet §9 R7, ARCHITECTURE/PERSISTENCE wording name the shared primitive and the consumers that now defer | — |
|
||||
| LOW R8d — "PERMANENT … for the life of the install" over-stated: the verdict lasts until the fresh ledger's epoch passes the surviving segments, which are then silently ignored | **docs**: DESIGN §10, packet §5.9, ARCHITECTURE row | — |
|
||||
| LOW R8e — the socket qualification introduced its own absolute: a UNIX socket is corruption on the dir-fd shape only; the path shape's stat-before-open skips it | **docs**: DESIGN §10/§12.5, packet §5.9, ARCHITECTURE row | — |
|
||||
|
||||
Sizes after the close-out: `platform_layer.py` 1500/1500 (band ceiling; net +3 on the policy, paid by rewrapping two prose blocks — no contract text dropped), `usage_compaction.py` 1262 (band), `tests/test_usage_compaction.py` **1600/1600** (owner decision still owed: the next pin in this suite needs either the archive-reader tests moved to their own module — a natural organ boundary, `archived_attempt_ids` vs the pass — or a cap decision), `tests/test_lockfile_helpers.py` 568.
|
||||
|
|
|
|||
|
|
@ -211,17 +211,25 @@ exactly the per-row branch taken `weight` times with the sums pre-added.
|
|||
own winerror→errno table lands both on `EINVAL` and would leave the name
|
||||
tier structurally unreachable there: a Windows volume without byte-range
|
||||
locks would fail EVERY monetary append closed instead of degrading to it.
|
||||
`ENOLCK` ("no locks available") is NOT in that set (round 5.4; until then
|
||||
it selected the name tier): a filesystem without a lock daemon answers it,
|
||||
but so does an exhausted kernel lock table, and neither is the kernel
|
||||
saying this filesystem cannot — the probe keeps the enforced tier and every
|
||||
live acquisition the kernel refuses with it fails closed, so an install
|
||||
whose `state/` answers ENOLCK persistently (bare NFS without lockd) refuses
|
||||
every monetary write with `UsageAccountingError` — no lock, no append, no
|
||||
pass — instead of running the name protocol; moving `state/` onto a
|
||||
filesystem that locks is the repair. The verdict is cached per process per
|
||||
directory under one module lock, so racing threads share one probe and one
|
||||
answer rather than two probes that could disagree.
|
||||
`ENOLCK` ("no locks available" — a filesystem without a lock daemon, or an
|
||||
exhausted kernel lock table) is the third answer (round 5.4 close-out; round
|
||||
5.4 proper made it fail EVERY caller closed, which the lenses showed to be
|
||||
product-wide: the same primitive locks state singletons, task results and
|
||||
custody, so a lockd-less NFS `state/` would have stopped every locked write
|
||||
and every model dispatch — a capability the name protocol had always
|
||||
provided there): it selects the **name tier** like a filesystem that cannot,
|
||||
but the probe RECORDS the errno beside the verdict, and a caller may refuse
|
||||
that tier by errno (`acquire_exclusive_file_lock(refuse_name_tier_errnos=…)`).
|
||||
Only the monetary lock does: `usage_ledger._named_lock` names `ENOLCK`, so on
|
||||
such an install every monetary write refuses typed (`UsageAccountingError` —
|
||||
no lock, no append, no pass; money never runs the name protocol where locks
|
||||
might merely be unavailable) while every other lock keeps the name protocol
|
||||
it always ran there; moving `state/` onto a filesystem that locks is the
|
||||
repair. The verdict is cached per process per directory under one module
|
||||
lock, so racing threads share one probe and one answer rather than two
|
||||
probes that could disagree — except a directory where the scratch probe
|
||||
cannot be created, which answers enforced for that call and is probed again
|
||||
next time (not cached).
|
||||
*Enforced tier* (POSIX `fcntl.flock`, Windows `LockFileEx` — both held on
|
||||
the lock fd): every other hold of this lock is milliseconds; a compaction
|
||||
pass over a multi-megabyte ledger can legitimately exceed its 90 s
|
||||
|
|
@ -286,7 +294,11 @@ exactly the per-row branch taken `weight` times with the sums pre-added.
|
|||
reclaimed through the age path — the probe flock guarding it on the
|
||||
enforced tier — while only a same-uid recycle wedged; this note named the
|
||||
opposite mechanism). So a lock whose owner died and whose pid was reused
|
||||
is never reclaimed by age while the impostor lives: the wedge begins once
|
||||
is never reclaimed by age while the impostor lives (`pid_is_alive` is the
|
||||
ONE liveness primitive, shared by every consumer — custody settlements,
|
||||
claim reclaims, staging reaps — so a pid recycled onto another user's
|
||||
process reads alive everywhere and those defer while the impostor lives,
|
||||
exactly as they do for a same-uid recycle): the wedge begins once
|
||||
the dead owner's file is older than the 90 s staleness window
|
||||
(`usage_ledger._locked`, `stale_sec=90.0` — a literal there, not a
|
||||
`config.py` constant) and ends when the impostor exits, even though the
|
||||
|
|
@ -473,7 +485,9 @@ the live replay, so this lane ships the join surface the sweep must use:
|
|||
FIFO, a device — is no segment: segments are regular files by
|
||||
construction, no generation lives there, and it is skipped; one the
|
||||
kernel refuses to open at all (a UNIX socket, `ENXIO`) is corruption like
|
||||
any other unopenable entry. Where a dir-fd is held that classification is
|
||||
any other unopenable entry — on the dir-fd shape; without a held handle
|
||||
(Windows, no `O_DIRECTORY`) the `stat` before the open classifies a socket
|
||||
as not regular and skips it. Where a dir-fd is held that classification is
|
||||
an `fstat` after an `O_NONBLOCK` open; where none is (Windows, or any os
|
||||
without `O_DIRECTORY`) it is a `stat` BEFORE the open, because there the
|
||||
open is the step a directory refuses and a writer-less FIFO blocks on. A
|
||||
|
|
@ -494,8 +508,10 @@ the live replay, so this lane ships the join surface the sweep must use:
|
|||
regular file in `archive/usage_ledger/` that is not a byte-prefix of a
|
||||
stamp-less live file is `generation newer` — a fresh ledger started
|
||||
beside a surviving archive (a reset that deleted the ledger alone) leaves
|
||||
every history question a PERMANENT corruption verdict for the life of the
|
||||
install, and an operator's stray `notes.jsonl` there is skipped on a
|
||||
every history question a `generation newer` corruption verdict until the
|
||||
fresh ledger has compacted as many times as the old one had — after which
|
||||
the surviving segments fall under the epoch floor and are silently ignored,
|
||||
their ids absent from every history question — and an operator's stray `notes.jsonl` there is skipped on a
|
||||
stamped ledger but is corruption on a stamp-less one; move or delete the
|
||||
archive with the ledger, or keep both. Disclosed, lock-free readers: a
|
||||
compaction that commits between a question's live-header read and its
|
||||
|
|
@ -614,7 +630,8 @@ byte authority.
|
|||
the archive — an entry that opens but is not a regular file — is no
|
||||
segment and is skipped on both shapes of the scan (the FIFO cannot hang
|
||||
the question; an entry the kernel refuses to open at all, a UNIX socket,
|
||||
is corruption), and the chain union is built once per chain and cached
|
||||
is corruption on the dir-fd shape and skipped by the path shape's
|
||||
stat-before-open), and the chain union is built once per chain and cached
|
||||
within a bound.
|
||||
6. **Idempotency survives**: subscription/external replays after compaction
|
||||
dedup (no double charge) and still conflict-check; legacy import stays
|
||||
|
|
|
|||
|
|
@ -5614,3 +5614,13 @@ Cross-cutting disclosures:
|
|||
surfaces are the two pinned suites and the lock/liveness consumers above,
|
||||
all run), and the Windows LockFileEx mechanics remain owed to the 3-OS CI
|
||||
matrix.
|
||||
|
||||
## From the C6 micro-round 5.4 close-out (operator, base b4938c31; owner batch №12 A)
|
||||
|
||||
Three read-only lenses on the 5.4 delta: NEEDS_FIXES × 3, no HIGH (3 MEDIUM, 7 LOW). The operator closed them without a further agent round (the owner bound was one micro-round):
|
||||
|
||||
1. **R1 blast radius (MEDIUM, two lenses) — fixed.** Round 5.4 had moved `ENOLCK` out of the unsupported set so the probe kept the enforced tier and EVERY live acquisition failed closed. The primitive is shared (state singletons, task results, custody, claims, …), so a lockd-less NFS `state/` would have refused every locked write in the product, where the name protocol had always worked. Now `ENOLCK` selects the name tier like a filesystem that cannot, the probe records the errno beside the verdict, and `acquire_exclusive_file_lock(refuse_name_tier_errnos=…)` lets a caller fail closed on it — only `usage_ledger._named_lock` names `ENOLCK`. Red-first: `platform_layer.py`/`usage_ledger.py` @ `b4938c31` → `assert True == (True, 5)`, `assert True is False`.
|
||||
2. **R4 typed resolution (MEDIUM) — fixed.** `_segment_path` resolves with the non-strict `os.path.realpath` inside the typed `try` (`except (OSError, RuntimeError)`). Red-first: a self-loop segment link → `RuntimeError: Symlink loop` / `OSError [Errno 40]` escaped @ `b4938c31`.
|
||||
3. **R3 stamp-less symlink levels (LOW) — fixed**, pin deferred (suite at its 1600-line cap; mutation-verified by hand). **R5 witness inode tie + `_Abort` on a vanished ledger (LOW) — fixed**, no pin (disclosed). **R6 heartbeat-ownership pin (LOW) — disclosed, not fixed.** **R1 uncached unprobeable directory, R7 shared liveness primitive, R8d epoch-floor duration, R8e socket shape (LOW) — docs corrected** in DESIGN §8/§10/§12.5, packet §5.9/§5.10/§9/§10, ARCHITECTURE row.
|
||||
|
||||
Gates at the close-out tip: `tests/test_lockfile_helpers.py` + `tests/test_usage_compaction.py` + `tests/test_usage_*` + `tests/test_persistence_inventory.py` rc 0 (107 passed, 1 skipped Windows-only); `ruff --select F` rc 0; `scripts/check_domains.py` rc 0; `scripts/regenerate_size_ratchet.py --check` rc 0 (`platform_layer.py` 1500, `tests/test_usage_compaction.py` 1600 — both AT their ceilings); `git diff --check` rc 0. The CI-shape battery and `-m serial` run on the integration tree after the lane merges.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue