symfonic.memory.backends.postgres_memory_generation¶
postgres_memory_generation ¶
Durable memory erase generations, in the memory database itself (CORE-199).
A turn that recalled memory before an admin forget holds erased content and may write it back after the forget returned. Every native subtree erase -- the runtime's inline forget, or the eraser's first transaction of a large job -- therefore advances a row for the erased key in the transaction that deletes the content, under the tenant lock and the sweep's EXCLUSIVE lock. A memory-only forget advances it as a joint forget does.
Every raw PostgreSQL v1 mutation re-reads the rows covering its own scope on
its own connection after the tenant lock: the scope's key and each ancestor
key, by primary key. A turn's capture, and every re-proof of it, also reads
every key below its scope by a primary-key range: a turn recalls its whole
subtree, so a forget of a descendant erased something it may hold. That read
is bounded: past SUBTREE_BUDGET keys it refuses rather than prove less. The
rows live beside memory_nodes, so the proof needs no process-local
registry and holds across pools, processes and restarts. Rows only
advance: a trigger admits an INSERT only at generation 1 and an UPDATE
only to exactly the old generation plus one, and refuses every DELETE
or TRUNCATE, so a role holding INSERT and UPDATE (the runtime and the
eraser) can neither reopen a stale turn nor exhaust a counter. The
triggers are ENABLE ALWAYS, so session_replication_role=replica still
fires them. A turn's token also binds the table's install witness and the
server (INCARNATION, SERVER; postgres_memory_generation_witness): a
reinstalled, restored or copied table, or a restored cluster, must not readmit
a turn captured before, even if its rows match. Every read
refuses a table whose triggers are not enabled
always, carry another event mask, a WHEN clause or an UPDATE OF column
list, or call any function but this module's body beside the table, a
table with row security, a rewrite rule, any other trigger or an inheritance
child or parent, an UNLOGGED table, and a table or trigger function that memory_nodes'
owner does not own (postgres_memory_generation_schema). The table owner,
or a superuser, can still tamper: a persistent change of those kinds makes
every reader refuse, but a trigger disabled and re-enabled inside one
transaction passes that transaction's own writes (afterwards its row no
longer matches the witness, UNWITNESSED), and nothing here can prevent it.
Startup never creates the table;
scripts/migrations/core199_memory_erase_generations.sql does. Until then
a turn refuses to capture and a native erase refuses with UNMIGRATED
before it deletes anything (CORE-171): the eraser does not attest, the
runtime's forget refuses before its first delete and a joint forget before
its transcript intent (a check at its start, not a lock). An erase that could
not record would leave a turn free to write back what it erased.
advance
async
¶
Invalidate every turn that observed scope's subtree; return its log seq.
Inside the erase's own transaction, after the graph lock and before it
commits, whether or not the erase matched a row. Callers refuse earlier;
a table or log gone since then refuses here and rolls the delete back.
The generation and its erase log row commit together
(postgres_memory_erase_log).
Source code in src/symfonic/memory/backends/postgres_memory_generation.py
capture
async
¶
The rows covering path now; a turn takes this before it reads.
ROW SHARE on memory_nodes waits out an erase holding the sweep's
EXCLUSIVE lock, so a table installed during that erase cannot hide it.
strict (every turn) refuses without the table; otherwise it reads ().
Source code in src/symfonic/memory/backends/postgres_memory_generation.py
checked
async
¶
:func:covering in the pool's (joined) transaction, failing closed.
Source code in src/symfonic/memory/backends/postgres_memory_generation.py
covering
async
¶
Rows at path's key and each ancestor key, by primary key; None if absent.
subtree adds every key below path's: what a turn that recalled
path read, at most SUBTREE_BUDGET of them, or it refuses with
OVER_BUDGET. That form is a turn's token, so it leads with the
table's INCARNATION and the SERVER: neither a reinstalled or
restored table nor a restored cluster readmits a turn captured before.
Every row must come from the relation RESOLVE attested, and the name
must still resolve to it after the reads, or it refuses REBOUND.
The caller's transaction holds the tenant lock; a joined caller must be
READ COMMITTED, or its snapshot could predate an advance it waited for.
Source code in src/symfonic/memory/backends/postgres_memory_generation.py
generations_table
async
¶
grant_runtime
async
¶
Owner side, in the caller's transaction; role is a validated identifier.
Source code in src/symfonic/memory/backends/postgres_memory_generation_schema.py
require_installed
async
¶
:func:require_table on pool before an erase touches another store.
Source code in src/symfonic/memory/backends/postgres_memory_generation.py
require_table
async
¶
The table, or refuse UNMIGRATED: no native erase runs without it.
Source code in src/symfonic/memory/backends/postgres_memory_generation.py
runtime_drift
async
¶
How role departs from read-and-advance on table, or None.