Skip to content

symfonic.memory.backends.postgres_erase_routing

postgres_erase_routing

Decide, before any delete, whether a native forget needs the eraser (CORE-199 S2).

The runtime's own forget transaction asks this under the sweep's EXCLUSIVE graph lock and mutates nothing when the answer is yes. Three conditions hand the forget to the eraser principal:

  • an erasing job overlaps the subtree: the identical prefix resumes it, an ancestor or descendant refuses there. The runtime must not erase those claims inline, since it cannot lift the job that fences them.
  • the claim history exceeds what one transaction may erase.
  • the content exceeds what one transaction may erase, once content jobs are installed (route_content, from the inline erase's own bounded reads).

Without the job table (S1 not migrated) no job can exist, and an oversized history refuses as it always did, now before the content delete rather than after it. Without the generation table every native forget refuses UNMIGRATED here, before either answer (CORE-171).

LargeEraseRequired

LargeEraseRequired(message: str, runtime_login: str, binding: Any)

Bases: MemoryContractError

A forget the runtime may not finish; nothing has been mutated.

Subclasses the refusal so that any caller other than the forget bridge sees exactly the refusal it saw before S2. binding names the memory_nodes relation the runtime routed against; the eraser refuses to act on any other.

Source code in src/symfonic/memory/backends/postgres_erase_routing.py
def __init__(self, message: str, runtime_login: str, binding: Any) -> None:
    super().__init__(message)
    self.runtime_login = runtime_login
    self.binding = binding

overlap_sql

overlap_sql(jobs: str) -> str

Erasing jobs at an ancestor key, the key itself, or any descendant.

Source code in src/symfonic/memory/backends/postgres_erase_routing.py
def overlap_sql(jobs: str) -> str:
    """Erasing jobs at an ancestor key, the key itself, or any descendant."""
    return (
        f"SELECT id, prefix FROM {jobs} WHERE tenant_id=$1 AND state='erasing' "
        "AND (prefix=ANY($2::text[]) OR (prefix>=$3 AND prefix<$4))"
    )

route_content async

route_content(conn: Any, error: MemoryContractError) -> None

Hand an oversized content forget to the eraser once content jobs exist.

error is the inline erase's refusal, raised by its bounded reads before any delete. Without content jobs it stands, as it always did.

Source code in src/symfonic/memory/backends/postgres_erase_routing.py
async def route_content(conn: Any, error: MemoryContractError) -> None:
    """Hand an oversized content forget to the eraser once content jobs exist.

    ``error`` is the inline erase's refusal, raised by its bounded reads
    before any delete. Without content jobs it stands, as it always did.
    """
    from symfonic.memory.backends.postgres_erase_binding import resolve
    from symfonic.memory.backends.postgres_erase_content import CONTENT_EXCEEDED, content_ready
    from symfonic.memory.backends.postgres_erase_jobs import jobs_table

    if str(error) != CONTENT_EXCEEDED:
        return
    jobs = await jobs_table(conn)
    if jobs is not None and await content_ready(conn, jobs):
        login = await conn.fetchval("SELECT session_user")
        raise LargeEraseRequired(CONTENT_EXCEEDED, login, await resolve(conn)) from error

route_forget async

route_forget(conn: Any, scope: Any) -> None

Raise LargeEraseRequired, or return to let the runtime erase inline.

Source code in src/symfonic/memory/backends/postgres_erase_routing.py
async def route_forget(conn: Any, scope: Any) -> None:
    """Raise LargeEraseRequired, or return to let the runtime erase inline."""
    from symfonic.memory.backends.postgres_erase_binding import resolve
    from symfonic.memory.backends.postgres_erase_jobs import ERASING, jobs_table
    from symfonic.memory.backends.postgres_memory_generation import require_table
    from symfonic.memory.backends.postgres_native_subtree import GRAPH_LOCK
    from symfonic.memory.backends.postgres_pending_claims import over_budget

    await conn.execute(GRAPH_LOCK)
    await require_table(conn)
    jobs = await jobs_table(conn)
    login = await conn.fetchval("SELECT session_user")
    if jobs is not None:
        from symfonic.memory.backends.postgres_erase_retirement import refuse_retired

        await refuse_retired(conn, jobs, scope.tenant_id)
    if jobs is not None and await conn.fetch(overlap_sql(jobs), *overlap_args(scope)):
        raise LargeEraseRequired(ERASING, login, await resolve(conn))
    if await over_budget(conn, scope):
        if jobs is None:
            raise MemoryContractError(BUDGET_EXCEEDED)
        raise LargeEraseRequired(BUDGET_EXCEEDED, login, await resolve(conn))