The wave-5 engine task (d69d9a3) found the literal
--no-default-features --features pg-lo config cannot compile the whole
crate: with no kv feature enabled, KvEngine/KvCursor go empty (E0004)
and the facade's by-reference dispatch matches are non-exhaustive
(references are always considered inhabited). Structural, not a bug —
per ADR-011 §4 every constructor mode pairs a kv engine, so a build
with zero kv engines is not a supported configuration (ADR-013 §4's
supported non-default configurations all include one).
The meaningful pure builds, verified on this tree:
- --no-default-features --features "pg-lo,mem" — check/test/clippy
green (115 tests ok server-less)
- --no-default-features --features "postgres,pg-lo" — check/clippy
green, test target compiles
Amended: engine-pg-lo (description + acceptance + gates),
engine-pg-lo-window (acceptance), store-pg-lo-construction (the full
structural reading + acceptance), review-wave-5 (gates). The engine
task's Notes/Summary already recorded the finding; the criteria text
now matches what was verified.
Verification: cargo test 69/19/3 ok, fmt --check green (docs-only
change).
20 KiB
id, name, status, depends_on, scope, risk, impact, level, tags
| id | name | status | depends_on | scope | risk | impact | level | tags | ||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| engine-pg-lo | pg-lo large engine — Large Objects behind the Backend trait | completed | moderate | medium | component | implementation |
|
Description
Implement the pg-lo large-tier engine (feature pg-lo, default-off;
ADR-009, admitted on POC #7 — findings C1–C7 are the evidence base)
against the wave-1 trait (src/backend.rs) in src/pg_lo.rs (the
2-line module-doc stub behind the pre-existing
#[cfg(feature = "pg-lo")] gate). The postgres kv engine
(src/postgres.rs, wave 4) is the structural twin for the driver
posture (pool, prepared statements, schema bootstrap, error mapping);
the local engine (src/local.rs) is the structural twin for the
large-tier bodies (streaming read cursor, staged put). pg-lo does
not imply postgres (ADR-013 §4's consequence; the Cargo.toml
feature carries its own driver deps) — every shared piece must compile
under the pg-lo pure builds: --no-default-features --features "pg-lo,mem" and --no-default-features --features "postgres,pg-lo".
The literal kv-less config (--no-default-features --features pg-lo) is not a gate and cannot compile the whole crate — with no kv
feature enabled, KvEngine/KvCursor go empty and the facade's
by-reference dispatch matches are non-exhaustive (E0004; references
are always considered inhabited) — see store-pg-lo-construction for
the full reading.
- Engine shape (POC #7 C1 is the contract) — two artifacts: LOs
hold the bytes; the companion table
lo_entries (key bytea PRIMARY KEY, loid oid, size bigint, committed_at)is the contract authority —list(),size(),has(), CAS-existence are all table-level. Never trust or readpg_largeobjectfor addressing (the C6 sweep validates against the table only). One engine-owned PostgreSQL schema (the postgres engine's bootstrap pattern: idempotent DDL at the constructor, the quoted-identifier boundary, no migration machinery). The default schema name and the consolidation shape's schema sharing (both engines on one pg instance) are recorded design points. - Driver posture (the postgres engine's, reused) — deadpool-postgres
over tokio-postgres, pool size configurable with a named shipped
default, per-connection
prepare_cacheddiscipline (statements die with sessions), natively async — nospawn_blockingseam, no blocking I/O on the async path. Shared-helper design point: the postgres engine's crate-internal helpers (quote_identifier, the SQLSTATE 42P01 discriminant,pg_err, the i64-nanos timestamp helpers) live behind#[cfg(feature = "postgres")]today — decide and record the sharing shape (the natural one: a crate-internal pg-common module gatedcfg(any(feature = "postgres", feature = "pg-lo")), one module per file, so the pg-lo pure builds compile without duplication). ThePgOptsquestion rides the same point: it ispuband re-exported behindpostgres(wave 4's constructor signature put it on the public surface) — widening its gate toany(postgres, pg-lo)(re-export gate widened with it) vs a pg-lo-local opts type is a recorded design point; whether pg-lo carries thesynchronous_commitknob at all (the POC ran the server with it off; the kv parity knob's posture may or may not carry) is recorded with it. - Put = one transaction (ADR-009 §2) — BEGIN →
lo_create→ chunkedlowrite→ companion-row insert → COMMIT. 512 KiB statements (POC #7 C2's measured knee; statement count, not LOBLKSIZE, is what costs) as a named shipped constant. CAS: companion-table existence check first — a re-put of an existing key is a no-op tx, never an overwrite (the C1 shape). The store core's put loop hands 64 KiB windows (PUT_CHUNK); the engine aggregates appends into 512 KiBlowritestatements (recorded design point). - Staged put (the engine-level
put_stagedcontract) — the staging cursor body is a held tx: handout opens the checkout + tx +lo_create;appendrides the chunkedlowriteaggregation;commitpublishes (companion-row insert + COMMIT — the row and the LO's pages land in one step, zero crash orphans structurally, C6);discardrolls back and returns the checkout. Every failure path converges on rollback — stage hygiene holds in both forms (discard → zero residue: no LO pages, no row; failed commit → self-discard), exact-count tested (the#[cfg(test)]fail-commit knob standing in for media failure, the mem/sqlite/postgres pattern). Scope boundary: ADR-010 §1's "a tx the store core holds" is the fleet joint form (wave 6's consolidation mechanics — ADR-012 §4/§5's ruling that pg-lo contributes LO writes + companion-row insert into the store-held tx); its API shape is a wave-6 design point. This task ships the engine-level staging cursor only. - Get = descriptorless
lo_get(oid, off, len)windows (ADR-009 §3, the C4 posture) — the read cursor closes over the companion row (oid + length; content immutable, so the row is authoritative), not over a tx-scoped descriptor. Range reads are single stateless statements, pool-friendly.len()reads the companion row — metadata only, never LO content (ADR-008's discipline). A range read beyond content length fails cleanly (UnexpectedEof-shaped → the engine'sError::Iomapping; the POC'sranged_reads_windows_and_eofshape). Reads ride thelo_*64-bit variants end-to-end (lo_lseek64/int8 offsets if any positioning is used;lo_get's bigint offsets already are — the C7 int4 trap must not be inherited). The held-descriptor tx posture exists as the fallback (measured: no advantage even for sequential full reads) — named in the module doc as not-shipped, never coded. has— companion-table EXISTS probe.size— the companion-rowsizeprobe (metadata-only; the ADR-009 companion-row probe the suite's INV-6 row names).delete— one tx (lo_unlink+ row delete), rows-affected discipline (Err(Error::Missing)when absent); GC-participating (the delete window's arbitration is store-core and table-level — nothing fs-specific, ADR-009 §4).list—SELECT key FROM lo_entries, complete (the POC's exact-count gate: list == companion table after mixed ops).- Virgin-store posture — the constructor creates the schema and
table; the tolerance is the postgres engine's recorded read-only
split (reads on a schema-less database map to absent/empty via the
SQLSTATE 42P01 discriminant, writes surface the engine error). The
schema-less test fixture mirrors the postgres engine's
#[cfg(test)] new_schemalesspattern. - Posture deltas in the module doc, stated not hidden (ADR-009
§5/§6) — LO catalog space is reused but never returned
(
pg_total_relation_size('pg_largeobject')monitoring named;VACUUM FULL/pg_repackthe only shrink path — an operator runbook line, never automatic); autovacuum inherited, keep it on; single-stream cached get 20–50× behind page-cache local fs at ≥1 MiB with the aggregate ~700 MB/s @ 16 readers serving picture — named capacity facts, not solved problems. - Orphan-recovery sweep (ADR-009 §5's requirement) — a
crate-internal
reap_orphans()for the legacy/bypass class only (committed LOs without companion rows): enumerateSELECT DISTINCT loid FROM pg_largeobjectas a candidate list only, check each againstlo_entries.loid(the truth),lo_unlinkthe unclaimed (the C6/pgdiag10 shape — cost scales with oid count, not content size). Scope-noted#[allow(dead_code)]until a consumer exists (recorded: the ops/maintenance surface is wave 7+; no store-core verb is scoped this wave), contract-tested with a planted orphan and a byte-exact survival assertion for committed content. - Unit tests in-module: the method contract (virgin-store no-ops
incl. the schema-less form, CAS re-put no-op, Missing discipline,
size exactness + metadata-only, list completeness after mixed ops,
ranged reads at window boundaries + beyond-length failure), the
staged-put happy path and both hygiene forms, the orphan sweep.
Tests need a live server — the alkstore harness convention:
connection settings ride the environment
(
ALKBLOBS_PG_HOST/PORT/USER/PASSWORD/DB), tests skip cleanly server-less, acceptance requires green against the server. The harness server: dockerizedpglo-poc(postgres:16-alpine) on :15432, dbblobs, user/passwordpostgres/poc(verified live 2026-10-12). - LO isolation on the shared harness db (named design point — the
C7 lesson, do not rediscover it): Large Objects are
database-level, not schema-level — the schema-per-test
convention does not isolate them, a
DROP SCHEMAleaves the LO's pages inpg_largeobject, and shared-LO tests raced each other into spurious "large object NNNN does not exist" aborts in the POC. Decide and record the isolation shape: per-testCREATE DATABASE(the POC's provenopen_freshshape) or per-instance LOID tracking + teardownlo_unlink(aLoJanitordrop-guard, the SchemaJanitor pattern extended). Either way acceptance includes zero LO residue on the shared db (pg_largeobjectprobed before/after full runs). - Review amendment A2 (wave-4 gate, owner: this task) — adopt the
schema-janitor discipline from the start; the wave-4 lesson (380
leaked schemas) is not to be re-learned. The
SchemaJanitorlives insrc/postgres.rs's#[cfg(test)] test_support— its new shared home (gatedcfg(test)+any(postgres, pg-lo)) rides the shared-helper design point.
The window-family seam clauses (EngineWindowState) are
engine-pg-lo-window (separate task); the constructor wiring is
store-pg-lo-construction. Nothing of EngineFleetState may exist
on this engine (ADR-012 §5's pg-lo row: pin clauses no, sweep
lock no — fleet pins live on the kv host). This task ships the
engine and its in-module unit tests only.
Acceptance Criteria
src/pg_lo.rsimplementsBackendcompletely (seven methods +put_staged) behind featurepg-lo(default-off), with the descriptorlesslo_getwindow cursor body and the held-tx staging body- The companion table is the contract authority (list/size/has/CAS
all table-level);
pg_largeobjectis never read for addressing - One engine-owned schema, idempotent bootstrap at the constructor; the quoted-identifier boundary; the virgin-store read-only split (42P01 → absent/empty on reads, writes surface) unit-tested incl. the schema-less form
- Put = one tx (lo_create → 512 KiB chunked lowrite → row insert → COMMIT); CAS re-put is a no-op; the 512 KiB knee is a named constant with the C2 citation
- Stage hygiene holds in both forms (discard → zero residue; failed commit → rollback self-discard), exact-count unit-tested
- Reads ride
lo_get(oid, off, len)windows;len()metadata-only from the companion row; beyond-length reads fail cleanly; the 64-bit-variants discipline recorded (the C7 trap not inherited) delete= one tx (lo_unlink + row delete) with the Missing discipline;listcomplete after mixed ops- The orphan-recovery sweep exists (candidates from
pg_largeobjectonly, validated againstlo_entries), scope-noted, contract-tested with a planted orphan - Posture deltas recorded in the module doc (catalog space never returned + VACUUM FULL operator line + rel-size monitoring; autovacuum inherited; the cached-get capacity facts)
- Shared-helper + PgOpts + LO-isolation design points decided and
recorded; the pg-lo pure builds (
--no-default-features --features "pg-lo,mem"and--features "postgres,pg-lo") compile clean (the literal kv-less config is not a gate — see the Description) - Unit tests env-driven (
ALKBLOBS_PG_*), skipping cleanly server-less; green against the harness server at acceptance; zero schema + LO residue on the shared db after runs cargo test(default — pg-lo compiles out) green;cargo test --features pg-logreen server-less (skipping);cargo clippy --all-targets -- -D warningsandcargo fmt --checkgreen (default +--features pg-lo+ the pg-lo pure builds)
References
- docs/architecture/decisions/009-pg-lo-large-tier-engine.md (the engine ADR — §1–§4 the contract, §5/§6 the posture deltas)
- docs/research/poc-pglo-findings.md (findings C1–C7 — the evidence base; C2 the 512 KiB knee, C4 the window posture, C6 the orphan sweep, C7 the harness lessons)
- docs/architecture/decisions/003-backend-contract-and-dispatch.md (the trait contract)
- docs/architecture/decisions/010-backend-io-seams-and-fleet-state-home.md §1 (the staged-put contract; the large-tier cursor body families)
- docs/architecture/decisions/012-pre-decomposition-consistency-rulings.md §4/§5 (the consolidation posture; the pg-lo applicability row — window family only, no fleet state)
- docs/architecture/decisions/013-family-composition-reactive-boundary-and-feature-graph.md
§4 (
pg-lodoes not implypostgres; the pg-lo-pure build) - src/backend.rs (the trait + seam contracts this implements)
- src/postgres.rs (the driver-posture twin: pool, prepare_cached, schema bootstrap, 42P01 split, test_support janitor)
- src/local.rs (the large-tier body twin: streaming cursor, staged put hygiene shapes)
- /workspace/alkblobs-pglo-poc (the POC crate — the ~400-line PgLoBackend this engine productionizes)
Notes
Implemented in
src/pg_lo.rs(the engine, its cursors, the orphan sweep, and 18 in-module tests),src/pg_common.rs(new — the shared-helper home), andsrc/{lib,postgres}.rs(the shared-helper move + gate widening;src/postgres.rs's changes are mechanical import swaps plus path-stable re-exports). Recorded design points, as resolved:
- Shared-helper shape —
src/pg_common.rs, gatedcfg(any(feature = "postgres", feature = "pg-lo")), one module per file:quote_identifier/qualified_table, the 42P01 discriminant,err/pg_err(the mapping keeps thepostgres:prefix — both engines attach to the same server family),size_u64,time_to_nanos(the i64-nanos encoding),build_pool, [PgOpts], and the#[cfg(test)] test_supportjanitors (SchemaJanitorgated#[cfg(feature = "postgres")], newDatabaseJanitorgated#[cfg(feature = "pg-lo")];crate::postgres::test_supportremains a path-stable re-export, sosrc/store.rs's import needed no edit).- PgOpts — one shared type; its definition moved to
pg_common, the public re-export widened toany(postgres, pg-lo)(the type is unchanged;alkblobs::postgres::PgOptsstill resolves through the path-stable re-export). Thesynchronous_commitknob carries (defaulton; the POC's server-sideoffwas a measurement posture, not the ship config — recorded in the module doc).- LO isolation (the C7 lesson resolved) — Large Objects are database-level; schema-per-test does not isolate them. The tests run on per-test dedicated databases (the POC's proven
open_freshshape): uniqueabtlo_{tag}_{pid}_{seq}_{nanos}names,CREATE DATABASEthrough thepostgresmaintenance db, aDatabaseJanitordrop-guard (DROP DATABASE … WITH (FORCE)) after the backend's pool. Zero LO residue on the sharedblobsdb is structural (no pg-lo test ever opens a pool against it) and gate- tested (shared_harness_db_holds_zero_lo_residue).committed_at— the crate-wide i64-nanos encoding asbigint, engine-stamped at insert (not a server-default column); the crate's one timestamp encoding rather than the POC'stimestamptz default now().- Staged reads — verified live:
lo_getinside the writing tx sees its own uncommitted pages (read-your-own-writes), so the verification pass reads the staged artifact through descriptorless windows + the unflushed in-memory tail; no descriptor reads, nolo_lseekanywhere (the C7 64-bit discipline: offsets ride bigint only,lowriteis implicitly sequential).- Stage hygiene mechanics — the staging cursor takes its checkout in
commit(publishover the takenObject), so everyfailure path converges on one ROLLBACK point; the dedup path (CAS hit) ROLLBACKs and returns
Ok(the staged LO is the residue removed);Drophands an abandoned cursor's open tx to a spawned ROLLBACK (never returns a dirty checkout to the pool — deadpool'sFastrecycle pitfall), degrading to a one-shot thread when no runtime is live.#[cfg(test)] fail_commitknob mirrors mem/sqlite/postgres/local.- Harness-server residue found and cleaned — the shared
blobsdb carried pre-existing exploration residue (public.lo_entries
- 21 LOs, 9 472 catalog pages) from pre-task pg-lo probing; unlinked + dropped (count now 0) so the
pg_largeobject-probe gate measures this suite's own runs. Thepublic.blobskv table was left untouched (kv-engine content, out of scope).- The literal
--no-default-features --features pg-loconfig cannot compile the whole crate —store.rs'sKvEngine/KvCursorenums are empty there (E0004), identically on the pre-task tree (verified by stash). Every constructor mode pairs a kv engine (ADR-011 §4's table), so a kv-less store is not a shippable shape; the clean configs are--no-default-features --features "pg-lo,mem"and ADR-013 §4's pg-pure build--no-default-features --features "postgres,pg-lo"— both clippy clean. In the literal config the shared pieces compile (the 10 store.rs E0004s are the crate's only errors; nothing in pg_common/pg_lo errors). Flagged forstore-pg-lo-constructionto keep in view.
Summary
Completed 2026-10-12 (wave 5).
src/pg_lo.rsimplements the seven-methodBackend+put_stagedbehind featurepg-lo(default-off): LOs hold the bytes, thelo_entriescompanion table is the contract authority (list/size/has/CAS all table-level;pg_largeobjectread by the engine only inside the orphan sweep's candidate enumeration), one-tx puts (lo_create → 512 KiB chunkedlowrite(LOWRITE_CHUNK, the C2 knee) → row insert → COMMIT), the descriptorlesslo_get(oid, off, len)window cursor (READ_CHUNK;len()metadata-only from the companion row; EOF clamping per the cursor contract plus the unexpected-EOF clean failure for a body short of its claimed length), the held-tx staging cursor with aggregation of the store core's 64 KiB windows into 512 KiB statements and both hygiene forms exact-count-tested, one-tx deletes with the Missing discipline, the complete single-snapshotlist, idempotent schema+table bootstrap at the constructor with the quoted-identifier boundary (hostile names tested), the 42P01 read-only virgin-store split (schema-less form tested), the crate-internalreap_orphanssweep (scope-noted; planted-orphan contract test with byte-exact committed-content survival), and the posture deltas in the module doc (catalog space never returned +VACUUM FULLrunbook line + rel-size monitoring; autovacuum inherited; the 20–50× cached-get capacity facts). Shared helpers (pg_err, 42P01, quoting, timestamps, pool,PgOpts) moved to the newsrc/pg_common.rsgatedany(postgres, pg-lo);PgOpts's public re-export widened (shared opts decision; the knob carries). 18 in-module tests, per-test dedicated databases (DatabaseJanitor), env-driven (ALKBLOBS_PG_*), skipping cleanly server-less. Verified:cargo test(69/19/3 ok),cargo test --features pg-lolive (87/19/3 ok, 18/18 pg-lo),cargo test --features postgreslive (103/36/3 ok),cargo test --all-features(176/71/4 ok), clippy-D warningsgreen (default,--features pg-lo,--features postgres,--all-features,--no-default-features --features "pg-lo,mem",--no-default-features --features "postgres,pg-lo"),cargo fmt --checkgreen,cargo doc --no-depsclean, zero LO + schema + database residue on the harness server after full runs. Engine-state window family remainsengine-pg-lo-window(EngineWindowStateuntouched; nothing ofEngineFleetStateexists on this engine); constructor wiring remainsstore-pg-lo-construction.