symfonic.platform.erasure¶
erasure ¶
PRIV-3 — the erasure saga: durable, resumable, bounded, fail-closed.
What this replaces is a for loop in a request handler. If the process dies
at store four of nine, the shipped path leaves the subject's data half gone,
records nothing about which half, and produces an "initiated" audit row as its
only artefact. The saga writes per-participant state before and after every
attempt, so a resumed run knows exactly what is outstanding — and a read of the
subject stays suppressed until nothing is.
Three rules keep it honest:
- Enumerate, never capture. The participant list is read from the registry on every pass (PRIV-1), so a store registered while the saga was running is swept rather than skipped.
- Confirm, never assume. A participant is done when
confirm_absentreturns true (SEC-PRIV-3), not whenerase_subjectreturned a number. A store that erases confidently and still holds rows is exactly what verification-by-absence exists to catch. - Survive one bad backend. A participant that raises is recorded and the sweep continues. Aborting would skip every participant sorted after the broken one and lose the completion trail with it.
ErasureOutcome
dataclass
¶
ErasureOutcome(scope_key: str, complete: bool, counts: dict[str, int] = dict(), unconfirmed: tuple[str, ...] = (), errors: dict[str, str] = dict(), generation: int = 0)
What one pass of the saga achieved. Partial success is a real answer.
ErasureSaga ¶
ErasureSaga(*, registry: SubjectDataStoreRegistry, fence: ErasureFence, saga_store: ErasureSagaStore, audit: AuditSeam | None = None, max_attempts: int = 3)
Drives every registered participant to a confirmed absence.
Source code in src/symfonic/platform/erasure.py
run
async
¶
One bounded pass. Idempotent, and safe to call again after a crash.