Context
The retention graph and collector exist and are proven against an in-memory
store:
src/ceres/snapshot/retention.rs — RetentionCoordinator + RetentionStore
trait: de-duplicated (parent, child) edges, root coverage for
Lease/Pin/Prepare, mark-sweep collection that CASes LIVE → DELETING
before any reaper runs, NoopReaper as the fail-closed default.
src/jupiter/migration/m20260917_000100_add_mst2_retention.rs — tables
mst2_retention_node, mst2_retention_edge (unique (parent_id, child_id)),
mst2_retention_root (unique (node_id, root_key)).
src/ceres/snapshot/runtime.rs — a resolved view pins its metadata-root page
and chunk projection as retention roots; release_lease drops the root and
re-resolving lifts a DELETING node back to LIVE.
Missing: the durable side. The tables exist but nothing reads or writes
them; retention state lives in one process's memory and is lost on restart,
which is exactly when a partially-collected graph is most dangerous.
Scope
PostgresRetentionStore implementing the existing trait
(node, root_covers, live_incoming, retain, mark_deleting, remove,
release_root, all_live, children) against the migrated tables, with the
guarantees the in-memory version gets from its single mutex:
retain inserts the node, the unique edges and the root rows in one
transaction; re-adding an existing edge must not increment anything;
mark_deleting is a conditional update (WHERE state = 'LIVE') that
reports whether it changed a row — this is the CAS the coordinator relies
on;
- concurrent
retain against a node being collected resolves to exactly one
of "retained (stays LIVE)" or "already DELETING (caller falls back)"; it
must never leave a DELETING node with a live incoming edge.
- Durable GC operation log + crash replay. Each collection step that
changes counts (mark, edge removal, node removal, reference decrement) is
recorded so a crash mid-pass can be replayed without decrementing twice.
Replaying must be idempotent by construction, not by hope.
- Mark audit. A periodic pass comparing the root-reachable set against the
recorded reference counts; on any mismatch it must stop physical
reclamation and report, rather than deleting on a wrong count (spec 10 §6).
- DELETING is terminal. A node marked
DELETING must not be re-referenced;
completing cleanup and then explicitly rebuilding is the only path back.
Acceptance
- The 12 coordinator rules currently covered in
retention.rs unit tests run
against the Postgres implementation, not only the in-memory one.
- Crash-replay test: kill the process between mark and remove, restart, replay,
assert counts are consistent and no node is removed twice.
- Concurrent test: a collection pass racing a
retain for the same node ends in
one of the two legal outcomes, never in a DELETING node with a live edge.
- Mark-audit test: corrupt a count deliberately, assert reclamation stops and
the audit reports (physical deletion is still off by default — see the
GitRetentionPort issue).
- GC-01..10 subset that does not depend on the Git-side port.
References
- Spec 10 §§5–6, spec 16 §6 GC-01..10
src/ceres/snapshot/retention.rs (rules to port), the retention migration
Context
The retention graph and collector exist and are proven against an in-memory
store:
src/ceres/snapshot/retention.rs—RetentionCoordinator+RetentionStoretrait: de-duplicated
(parent, child)edges, root coverage forLease/Pin/Prepare, mark-sweep collection that CASesLIVE → DELETINGbefore any reaper runs,
NoopReaperas the fail-closed default.src/jupiter/migration/m20260917_000100_add_mst2_retention.rs— tablesmst2_retention_node,mst2_retention_edge(unique(parent_id, child_id)),mst2_retention_root(unique(node_id, root_key)).src/ceres/snapshot/runtime.rs— a resolved view pins its metadata-root pageand chunk projection as retention roots;
release_leasedrops the root andre-resolving lifts a
DELETINGnode back toLIVE.Missing: the durable side. The tables exist but nothing reads or writes
them; retention state lives in one process's memory and is lost on restart,
which is exactly when a partially-collected graph is most dangerous.
Scope
PostgresRetentionStoreimplementing the existing trait(
node,root_covers,live_incoming,retain,mark_deleting,remove,release_root,all_live,children) against the migrated tables, with theguarantees the in-memory version gets from its single mutex:
retaininserts the node, the unique edges and the root rows in onetransaction; re-adding an existing edge must not increment anything;
mark_deletingis a conditional update (WHERE state = 'LIVE') thatreports whether it changed a row — this is the CAS the coordinator relies
on;
retainagainst a node being collected resolves to exactly oneof "retained (stays LIVE)" or "already DELETING (caller falls back)"; it
must never leave a
DELETINGnode with a live incoming edge.changes counts (mark, edge removal, node removal, reference decrement) is
recorded so a crash mid-pass can be replayed without decrementing twice.
Replaying must be idempotent by construction, not by hope.
recorded reference counts; on any mismatch it must stop physical
reclamation and report, rather than deleting on a wrong count (spec 10 §6).
DELETINGmust not be re-referenced;completing cleanup and then explicitly rebuilding is the only path back.
Acceptance
retention.rsunit tests runagainst the Postgres implementation, not only the in-memory one.
assert counts are consistent and no node is removed twice.
retainfor the same node ends inone of the two legal outcomes, never in a
DELETINGnode with a live edge.the audit reports (physical deletion is still off by default — see the
GitRetentionPort issue).
References
src/ceres/snapshot/retention.rs(rules to port), the retention migration