symfonic.memory.backends.postgres_erase_jobs¶
postgres_erase_jobs ¶
Durable native subtree erase jobs and the writer fence they impose (CORE-199 S1).
A large forget erases what one transaction cannot in later chunks: claim
identities, and content past its budget (postgres_erase_content).
Between those commits a job row marks the subtree as erasing: no native
mutation may touch it, no claim reader may treat it as live, and every
content read hides it (erasing, live_paths). This module ships the
table, the fence and the hiding predicate; nothing here creates a job.
The fence is sound only after the writer holds a memory_nodes lock in the same transaction. The sweep that will commit a job takes EXCLUSIVE on memory_nodes, which conflicts with every lock a writer takes, so either the writer committed first and the sweep erases its rows, or the writer waited and its next READ COMMITTED statement sees the committed job. A check made before the lock, or under an older snapshot, misses a job committed in between, so other isolation levels refuse while the table exists.
The jobs that can fence a writer are the ones beside the memory_nodes it locked, so the table is resolved in that relation's schema and every query names it schema-qualified. A job table reachable only elsewhere on search_path, one the session cannot read, or one without the canonical prefix constraint refuses: each would otherwise skip the fence or read the wrong schema.
A prefix must be a complete-level canonical v1 key rooted at the row's own tenant. On such a prefix the writer's exact-ancestor test and the reader's starts_with agree; without the final slash a reader would also fence a sibling whose ID extends the job's, and the writer would not.
canonical_job_prefix ¶
Python mirror of PREFIX_CHECK: S2 validates a job before inserting it.
Source code in src/symfonic/memory/backends/postgres_erase_jobs.py
fence_scope
async
¶
For a writer about to touch rows owned at scope or above it.
ROW SHARE on memory_nodes first, then the fence, inside the writer's own transaction. Once content erases in chunks (CORE-199), an update or a delete of a row an erasing job still holds would act on content the forget already answered for, so these writers refuse like inserts do. A v0 scope owns no canonical key, so no job can fence it.
Source code in src/symfonic/memory/backends/postgres_erase_jobs.py
grant_read_only
async
¶
Owner side, in the caller's transaction: reset role to SELECT on jobs.
role must already be a validated plain identifier.
Source code in src/symfonic/memory/backends/postgres_erase_jobs.py
jobs_table
async
¶
The schema-qualified job table, or None; readers need no isolation guard.
schema resolves beside schema.memory_nodes for a reader whose
search_path is pinned away from it.
Source code in src/symfonic/memory/backends/postgres_erase_jobs.py
read_only_drift
async
¶
How role departs from SELECT-only on jobs, or None if it does not.
Source code in src/symfonic/memory/backends/postgres_erase_jobs.py
refuse_absent_if_erasing
async
¶
Read-only: a record a job hides is held, not absent (CORE-199).
For a caller that found nothing at scope and is about to report so:
reads hide what an erasing job holds, so the answer there is the erase
in progress, as a write would get. Nothing is mutated either way.
Source code in src/symfonic/memory/backends/postgres_erase_jobs.py
refuse_if_erasing
async
¶
Refuse a native mutation whose scope lies in an erasing subtree.
Call only after this transaction holds its memory_nodes lock. A job at an ancestor key or at the key itself fences a write; a job below the write's own scope does not, since the write lands outside that subtree.
Source code in src/symfonic/memory/backends/postgres_erase_jobs.py
share_lock_nodes
async
¶
For a writer that locks no graph row: ROW SHARE on memory_nodes, first.
ROW SHARE is the weakest mode the sweep's EXCLUSIVE conflicts with, so a
job committed with the sweep is visible to this transaction's later
refuse_if_erasing. Taken before the writer's own rows, it orders
memory_nodes first, as every forget does, so neither waits on the other
in a cycle. PostgreSQL grants this mode only to a role with INSERT,
UPDATE, DELETE or TRUNCATE on memory_nodes; any other refuses here
rather than write unfenced.