Files
alkblobs/docs/research/poc-pglo-spec.md
T
glm-5.3-flash cee97defba docs(research): POC #7 — postgres Large Objects as the fs-tier pg-lo engine, passed
The ADR-008 pg-lo admission POC ran in a standalone crate
(/workspace/alkblobs-pglo-poc): PgLoBackend over the ADR-003/008 trait
contract (including size), 10/10 exact-count sweep-outcome contract
tests, clippy/fmt clean; dockerized postgres:16-alpine on :15432,
POC #5 driver stack (tokio-postgres + deadpool) via SQL lo_* functions,
no new dependency.

Gate verdict: passed, with named deltas.

- Performance: durable put 60-65 MB/s at >=1 MiB, within 1.5x of — and
  below 1 MiB beating — durable local fs on this fsync-slow disk;
  cached gets 70-180 MB/s single-stream, ~0.7 GB/s aggregate over 16
  readers (20-50x behind page-cache fs — the honest named delta)
- Contract: companion table is the list()/size()/CAS authority (never
  the catalogs); stage-then-commit; GC-participating lo_unlink delete
- Handles: the tx-scoped descriptor is real but pool-compatible via
  descriptorless lo_get(oid, off, len) windows — window gets keep
  handle-acquire p99 at 1-6 ms under readers <= pool; held descriptor
  is the fallback posture
- Vacuum: pg_largeobject pages churn-reused, never returned; tracked
  by autovacuum; rel-size monitoring named as an ops requirement
- Crash/orphan: LO creation is transactional — kill/terminate
  mid-write-tx leaves zero orphan pages; the only orphan class is a
  committed LO bypassing the companion table (planted, reaped by the
  ~7 ms/oid sweep; committed content survives byte-exact)
- Harness lessons: lo_lseek is int4 — the 64 variants are the
  >2 GiB discipline; shared-table parallel tests are unsound (per-test
  CREATE DATABASE isolation)

Docs: new poc-pglo-findings.md; poc-pglo-spec.md status passed;
register OQ-BL-06 #7 marked passed; ADR-008 pg-lo bullet updated
(duplicate bullet removed) + backends-and-dispatch/open-questions
cross-references.

Verification: cargo test --release (10 passed), clippy -D warnings,
fmt --check in /workspace/alkblobs-pglo-poc.
2026-10-03 04:23:06 +00:00

101 lines
5.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
status: passed
title: "POC #7 — postgres Large Objects as the fs tier's pg-lo engine"
last_updated: 2026-10-03
---
# POC: postgres Large Objects as the fs tier's pg-lo engine — spec
> **POC register #7**, added post-Phase-0 per the ADR-003/007
> substitution door (the third use of it, after postgres-on-kv and
> redb-on-kv). Code: standalone crate
> `/workspace/alkblobs-pglo-poc` (clone the POC #5 crate's harness
> conventions; add a `PgLoBackend` implementing the ADR-008 trait
> contract including `size`). Findings land here regardless, per the
> established convention. Requested by REQ-2 (requirements.md — the
> fleet-with-large-blobs consolidation option; ADR-008 names pg-lo as
> the candidate fs-tier engine). Status: **passed** — run 2026-10-03;
> findings in `poc-pglo-findings.md` (curves C2, handle anatomy C3,
> window-vs-held C4, vacuum posture C5, crash-orphan C6). Verdict: the
> gate passes — companion-table engine shape, `lo_get` window gets,
> ~60–65 MB/s durable put ≈ fs's durable put, cached gets behind
> page-cache fs by 20–50× (bounded, monitorable), orphan-recovery
> sweep proven and crash-orphan behavior clean.
## What this POC must decide
The ADR-008 question: can postgres Large Objects hold the fs tier's
contract for the fleet topology — serving the ≥128 KiB regime
(packfiles, large artifacts) from the same engine the kv tier already
rides — at acceptable performance and with an honest ops posture? The
fs tier's contract (ADR-003/008): `get` yields a pread-able handle,
stage-then-commit semantics for unknown-length puts, complete
`list()`, GC-participating `delete`, `size` without content fetch.
Large Objects' native shape differs from files in one structural way
the ADR names: **LO descriptors are transaction-scoped** (lo_open →
loread/lowrite must run inside a tx). The POC must measure what that
does to the handle pattern under a connection pool, not assume it away.
## Instruments
1. **Inherited `bench_small`-shaped harness, LO arm** — the POC #3/#5
methodology at fs-tier-relevant sizes: **128 KiB, 256 KiB, 1 MiB,
16 MiB, 128 MiB**, sequential and random-replay, p50/p99. Compare
against: local fs (the `local` engine baseline, same harness) and
kv-tier pg (bytea) at the lower sizes, as cross-checks of
methodology parity. (`loread`/`lowrite` via SQL functions through
tokio-postgres — the `postgres_large_object` crate targets
postgres 0.15 and is dead; do not use it.)
2. **Handle-cost probe** (`pgdiag7`): the tx-scoped descriptor under
deadpool — cost of open-tx → lo_open → lseek/loread per range read,
held vs. re-opened per read; pool pressure under N concurrent
readers (the ops fetch handler's real shape).
3. **Write path probe** (`pgdiag8`): lo_create + chunked lowrite
(the LOBLKSIZE ~2 KiB page layout matters here — measure write
amplification as value size grows); unknown-length put shape
(stage into one LO, companion row on commit); verify
`size`-without-fetch (`SELECT size` from the companion table).
4. **Churn/vacuum probe**: repeated put/delete cycles at 1–16 MiB;
measure `pg_largeobject` catalog growth, TOAST/vacuum behavior,
autovacuum posture (the fs engine inherits ADR-008/007's
maintenance-surface honesty requirement).
5. **Crash/orphan probe**: kill client mid-write-tx; verify no orphan
LO and no half-commit; kill after lo_create before commit; write
the orphan-recovery sweep story the engine ADR will need (compare
`lo_get`-visible OIDs against the companion table — never trust
the catalogs).
## Decision gate (mirrors ADR-007's)
pg-lo is admitted as a shipped fs-tier engine iff:
- **Performance:** LO put/get at 128 KiB–16 MiB is within an
order-of-magnitude of local fs on the same box (the tier is chosen
for fleet correctness, not solo speed — but a 10× gap would put the
fleet's packfile serving materially behind a single-node fs
deployment, and the ADR must say so if measured);
- **Contract:** complete `list()` (companion table), GC-participating
delete (`lo_unlink` under the delete window), virgin-store no-op
reads, `size` probe — all hold, proven via the exact-count
sweep-outcome test shape under `--all-features`;
- **Ops posture:** named deltas (tx-scoped handles, autovacuum/vacuum
on `pg_largeobject`, orphan recovery sweep) with measured or
documented-bounded costs.
Findings feed an engine ADR in the ADR-007 shape (measured curves,
posture deltas as requirements). If the gate fails, ADR-008's
recorded statement stands: shared-media or re-routing are REQ-2's
fleet answers, and pg-lo's named cost was paid as evidence, not
shipped blind.
## Register note (phase-0.md OQ-BL-06 convention)
The canonical register is phase-0.md OQ-BL-06: #1 trait dispatch,
#2 absorbed, #3 large-blob, #4 pooled-CAS (covered by #1), #5
postgres, #6 redb. The post-convergence POC files briefly carried
inconsistent self-numberings (the postgres file titled itself "#4");
they are renumbered to the register. This POC is **#7** — specified
2026-10-03, requested by REQ-2 (requirements.md; ADR-008 names pg-lo
as the candidate fs-tier engine), run 2026-10-03 — see
`poc-pglo-findings.md`.