- PgOpts.orphan_reap_permitted (default false) — the deployment-verified declaration (ADR-012 §3's class): the operator's assertion that this database hosts no other large-object consumer; read only by pg-lo, dormant on the postgres kv engine - reap_orphans refuses with a named Error::Io self-naming message while the declaration is absent (the constructor-declaration refusal's existing family member, the fleet: false precedent in sweep.rs:127); tested — the refusal runs nothing - the missing-LO pass: each unlink rides a savepoint; a vanished loid (SQLSTATE 42704 — empirically probed) passes the sweep and is not counted reaped instead of aborting the one-tx sweep; candidates re-validated at delete time, covering both the vanished-loid and the READ COMMITTED same-engine-delete race arms (no REPEATABLE READ, per the ADR's chosen form) - reap_orphans_tx takes the candidate list: the stale-candidate race is driven deterministically in test (the committed class survives, the report is honest, the tx reaches its end) - bootstrap_ddl adds lo_entries_loid_idx — the claim probe's supporting index (WHERE loid = $1 seq-scanned); idempotent, so existing databases gain it on next construction; tested (drop + reopen) - the hostile-schema test carries ADR-016 §3's framing: row isolation stands proven; reaping is what the declaration gates (asserted) - task f4-reap-declaration: status completed, Notes/Summary filled Verification: cargo test (default), cargo test --all-features server-less, cargo test --all-features against the live harness (pglo-poc :15432; 290+125+4, pg-lo's 35 live at default parallelism), clippy -D warnings on default/all-features/pg-lo+pem-pure variants, cargo fmt --check, cargo doc --no-deps — all pass.
10 KiB
id, name, status, depends_on, scope, risk, impact, level, tags
| id | name | status | depends_on | scope | risk | impact | level | tags | |||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| f4-reap-declaration | F-4 — the orphan_reap_permitted constructor declaration + the riding reap fixes (ADR-016) | completed | moderate | medium | component | implementation |
|
Description
Review-001 F-4, resolved by OQ-13 → ADR-016
(docs/architecture/decisions/016-one-large-object-consumer-per-
database.md — the task implements the ADR's Decision section; read it
line by line before starting). reap_orphans
(src/pg_lo/reap.rs:22/41) validates candidate LOs against this
engine's companion rows only, but pg_largeobject is database-level:
a second pg-lo engine — or any other large-object consumer — on the
same database under a different schema gets its committed LOs
lo_unlinked by this engine's reaping sweep. Cross-engine data loss
with no error raised, in a configuration that is technically supported
today (PgOpts.schema is public; the hostile-schema test demonstrates
co-tenancy).
Landing, per the ADR:
orphan_reap_permitted: bool, default false, on the pg-lo constructor options — the operator's explicit assertion "this database hosts no other large-object consumer" (the deployment-verified-invariant pattern, ADR-012 §3'sfleet: trueclass).reap_orphanson an engine constructed with the flag false refuses with a named error (a new error variant or the existing family's honest member — the naming is the task's design point, recorded). No guard probing foreign schemas — the ADR rejected the guard direction on the catalog-ambiguity hole; do not resurrect it.- The riding fixes (unconditional):
- an index on
lo_entries(loid)— the claim probe isWHERE loid = $1per oid and today seq-scans (the PK iskey); created in the schema bootstrap so existing databases gain it on next construction; - a loid vanishing between the candidate snapshot and the unlink
no longer aborts the sweep transaction (a missing-LO pass —
tolerate and continue, the ADR's chosen form over
REPEATABLE READ); - the READ COMMITTED candidate snapshot no longer races a concurrent same-engine delete into a spurious whole-sweep failure (re-validate candidates at delete time instead of trusting the snapshot).
- an index on
- The hostile-schema test's framing shifts (the ADR's §3): schema isolation of rows is unaffected and the test stays as an isolation test; what the constraint bounds is the reaping method. The test's docs/assertions updated to the declaration-gates-reaping framing.
Tests: the refusal without the declaration (named error), the full reap with the declaration, the vanished-loid tolerance (a loid deleted between snapshot and unlink — the sweep continues and reports honestly), the index's existence, and the race fix's shape as the harness allows.
Live harness
All pg-lo shapes run against the local dockerized postgres container
(pglo-poc, postgres:16-alpine on :15432, running for dev/testing):
export ALKBLOBS_PG_HOST=localhost
export ALKBLOBS_PG_PORT=15432
export ALKBLOBS_PG_USER=postgres
export ALKBLOBS_PG_PASSWORD=poc
export ALKBLOBS_PG_DB=blobs
Server-less, the pg tests skip cleanly; acceptance requires green against the live harness.
Acceptance Criteria
orphan_reap_permittedon the pg-lo constructor options, default false, documented as the deployment-verified-invariant declaration it isreap_orphansrefuses with a named error when the flag is false; the refusal tested; the naming recorded- The
lo_entries(loid)index exists (bootstrap-created; existing databases gain it) - A vanished loid between snapshot and unlink no longer aborts the sweep tx (missing-LO pass; tested)
- The candidate-snapshot race fix landed (re-validation at delete time; tested as the harness allows)
- The hostile-schema test's framing updated (isolation works; reaping is what the declaration gates)
- Gates green:
cargo test(default),cargo test --all-featuresserver-less,cargo test --all-features+ live harness, clippy--all-targets -- -D warningsacross the feature variants,cargo fmt --check
References
- docs/architecture/decisions/016-one-large-object-consumer-per-database.md (the ADR this implements — OQ-13's resolution)
- docs/reviews/001-storage-surface-general-review.md (F-4 and its compounding minor arms)
- docs/plans/implementation.md (§Wave 9 — "the ADR's unconditional riding fixes")
- src/pg_lo/reap.rs (reap_orphans/reap_orphans_tx)
- src/pg_lo/mod.rs (the constructor options — where the declaration lands)
- docs/architecture/decisions/012-pre-decomposition-consistency-rulings.md §3 (the constructor-declaration pattern reused)
- tasks/store-pg-lo-construction.md, tasks/engine-pg-lo.md (the constructor and schema-bootstrap shapes this extends)
Notes
Filled by implementation agent (2026-10-15):
- The declaration's home:
PgOpts(src/pg_common.rs), shared — per the one-opts-type-per-consumer-story design point (pg-lo's constructor options arePgOpts; the store constructors pass it through unchanged). The field carries the ADR-016 doc in full and is documented as dormant on the postgres kv engine (read only by pg-lo).PgLoBackendstores it at construction and the sweep reads it at its entry; no facade/trait change. - The refusal's naming (the task's design point): the existing
family's honest member —
Error::Io(String) with a named message — a new error variant would be a new failure family, and the family is ADR-pinned (src/error.rs: "new failure families are new ADRs"); a new ADR is not owed here (ADR-016 §5's consequence surface lists no error-family change, and ADR-016 did not decide a variant). The form mirrors the existing constructor-declaration refusal in the family — thefleet: falsefleet-liveness-table refusal (src/store/sweep.rs:127, ADR-012 §3's class):Error::Io, self-naming ("orphan reap refused (ADR-016)"), naming the declaration (orphan_reap_permitted = true) and the two-databases alternative. Both strings are asserted in the refusal test. - The missing-LO pass mechanism: a savepoint per unlink — the
empirical probe settled the discriminant:
lo_unlinkof a vanished loid raises SQLSTATE 42704 (UNDEFINED_OBJECT), and the failed statement aborts the sweep tx (every subsequent statement dies with it) — exactly why the old shape turned a vanished loid into a whole-sweep failure. Each unlink ridesSAVEPOINT orphan_reap_unlink; the 42704 pass rolls back to the savepoint and continues (not counted reaped — the report stays honest); any other unlink failure is a media failure and aborts. This mechanism covers both race arms at once (a loid vanishing between snapshot and unlink, and the same-engine concurrent delete racing the READ COMMITTED snapshot — both present identically at delete time: row unclaimed, LO gone); the claim probe itself remains per-oid at delete time (candidates re-validated, never trusted from the snapshot). NoREPEATABLE READposture, per the ADR's chosen form. - Race-fix testability ("as the harness allows"):
reap_orphans_txnow takes the candidate list as a parameter (the snapshot fetch stays inreap_orphans), so the stale-candidate race is driven deterministically — the test snapshots, unlinks the orphan concurrently-shape, then runs the sweep body over the stale list — asserting the pass, the honest zero-count, and the tx reaching its end with the committed class intact. No flaky concurrency test was introduced for an equivalent outcome. - The index:
lo_entries_loid_idxinbootstrap_ddl(CREATE INDEX IF NOT EXISTS … ON lo_entries (loid)) — schema-scoped names, idempotent, so existing databases gain it on next construction; the test drops it by hand and proves the re-open brings it back. - The hostile-schema test (
src/pg_lo/mod.rs) now carries the ADR-016 §3 framing in its doc and an assertion at the shifted boundary: storage isolates across schemas;reap_orphanson the default (declaration-absent) co-tenant construction refuses.
Summary
Filled on completion (2026-10-15):
ADR-016's Decision section is landed: PgOpts.orphan_reap_permitted
(default false) is the pg-lo engine's orphan-reap declaration — the
operator's deployment-verified assertion that the database hosts no
other large-object consumer — and reap_orphans refuses with a named
Error::Io self-naming message while it is absent (the
constructor-declaration refusal pattern's existing family member, per
the fleet: false precedent at src/store/sweep.rs:127). The
ADR's riding fixes all landed: the lo_entries_loid_idx bootstrap
index (idempotent, existing databases gain it on next construction),
and the savepoint-guarded missing-LO pass (SQLSTATE 42704 on a
vanished loid — a concurrent same-engine delete or a foreign
consumer's lifecycle — continues the sweep honestly instead of
aborting the one-tx sweep; candidates are re-validated at delete
time, never trusted from the READ COMMITTED snapshot). The
hostile-schema test's framing shifted to the ADR's §3: row isolation
is unaffected and still proven; the reaping method is what the
declaration gates, asserted at the boundary. Tests: the refusal
(named strings asserted, nothing runs), the full reap with the
declaration, the stale-candidate pass driven deterministically
through the now candidate-parameterized reap_orphans_tx, and the
index's existence/gain-on-reconstruction.
Verification (all green, 2026-10-15):
cargo test(default: mem + sqlite) — passcargo test --all-featuresserver-less — pass (pg skips cleanly)cargo test --all-features+ live harness (pglo-poc, :15432) — pass at default parallelism (290 + 125 + 4), the pg-lo suite's 35 tests running livecargo clippy --all-targets -- -D warnings(default),--all-features --all-targets, and the pg-lo-pure variant (--no-default-features --features pg-lo,mem --all-targets) — passcargo fmt --check— pass;cargo doc --no-deps— clean