Files
poimen/tasks/T7.1-postgres-eventlog.md

4.6 KiB

T7.1 — Postgres EventLog

Field Value
Phase P7 — Distribution
Size L — over 3 days
Status Not started
Flags
Spec inlined below
Blocks

Goal

The same EventLog port over Postgres, partitioned by tenant and time. Not a second port — the same one.

Facts (inlined — no spec read needed)

  • Two deployment modes, one set of ports:
Mode Log + state Blobs Coordination
Embedded — single binary, no services redb redb table in-process
Distributed — multi-node, multi-tenant Postgres S3-compatible Postgres advisory locks, or Redis if leases dominate
  • Postgres transactions supply crash-atomicity exactly as redb's shadow paging does; commit's four writes stay in one transaction.
  • This is why the port was async from the first line: a sync signature over a network call is impossible, and the local implementation paid only a negligible poll for the compatibility.
  • Every port test runs against both backends or the abstraction is not real. The conformance suite from T0.5/T0.7 is the deliverable being reused here.
  • Open at this scale: partitioning by tenant and time, index strategy for branch scans, and whether the outbox is a table or a logical replication slot. Decide and record the decision; it is not pre-settled.

Steps

  1. Schema: log table keyed (tenant, run, branch, lsn); state, consumer position and outbox tables mirroring the embedded key shapes.
  2. Partition the log by tenant and by time. Choose declarative partitioning and document the retention interaction with T8.4's tiering.
  3. Index for the access patterns that exist: branch scans from an LSN, recent runs by RunId prefix (ULIDs sort by creation time), outbox drain per BranchKey ascending Lsn.
  4. Implement commit as one transaction with all four writes. LSN allocation stays inside the transaction — a sequence is not sufficient, since LSNs are per branch and must be gap-free.
  5. Implement the checkpoint and outbox methods against the same schema.
  6. Run the unmodified P0 conformance suite against this backend in CI.
  7. Record the outbox decision (table vs replication slot) with its reasoning.

Acceptance

  • The P0 conformance suite passes unmodified against Postgres.
  • Every port test runs against both backends.

Verify

Harness: the same conformance function from T0.5, instantiated with a Postgres factory. A real Postgres in CI (testcontainers or a service container) — not an in-memory fake, which would test nothing about the backend.

Integration testtests/it_postgres_conformance.rs:

  1. Call conformance(|| PostgresEventLog::new(...)) with zero modifications to the suite. Any needed change is a finding about the port, not the test.
  2. Run the identical suite against redb in the same CI job, so a divergence between backends fails immediately rather than months later.
  3. Concurrent LSN allocation across separate connections — the redb test used tasks; here contention is across processes and is the real case. Assert 1..=n with no gaps or repeats.
  4. Assert commit's four writes are in one transaction: kill the connection mid-commit and assert all-or-nothing on reconnect.
  5. Partitioning: insert across several tenants and time ranges; assert a branch scan hits the expected partitions (check the query plan, not just the result).
  6. Outbox: assert drain_outbox returns (BranchKey, Lsn) ascending across partition boundaries.
  7. Record the outbox decision (table vs replication slot) and assert the chosen one is the one under test.

Command: cargo test -p storage-postgres --test it_postgres_conformance

False pass:

  • A modified copy of the conformance suite. The moment it is edited "just for Postgres", the two backends are no longer proven equivalent — which is the entire acceptance criterion.
  • Step 3 with a single connection, where the sequence is uncontended.
  • A SERIAL/sequence-backed LSN. It passes casual reads and is neither per-branch nor gap-free under rollback.
  • Running against SQLite or an in-process fake for CI speed.

Traps

  • A Postgres-only method added to the trait "temporarily". The embedded backend then has a stub and the abstraction is fiction.
  • A global sequence for LSNs. It serializes every run through one atomic and breaks per-branch gap-freedom.

Background (not required to do this task): rust-agentic-sys.md §7, §8.3, §18 · rust-agentic-task.md