symfonic.services.privacy.saga_store¶
saga_store ¶
The reference ErasureSagaStore: per-participant completion, durably.
start resumes rather than restarts, and that is the whole design. A
handler-loop erasure that dies at store four of nine leaves nothing but an
"initiated" audit row; this store leaves a row per participant saying exactly
which four went, how many attempts each took, and what the last error said.
InMemoryErasureSagaStore ¶
Reference implementation (CON-S-4); state is injectable for durability.
The lock is a thread lock, not a per-loop asyncio.Lock: an operator
retry and a scheduled resume can land in different loops (or threads), and
:meth:claim_generation_advance is only a compare-and-set if the compare
and the set are indivisible for every caller, not just for co-scheduled
ones. Nothing awaits inside a section, so a plain lock cannot deadlock.
Source code in src/symfonic/services/privacy/saga_store.py
claim_generation_advance
async
¶
Compare-and-set the completion transition; True for the winner only.
This is the only way to make the transition — there is deliberately no
plain "mark it advanced" setter to reach for. The saga cannot do it as
"read the flag, then advance": an operator retry racing a scheduled
resume — a race PrivacyService.resume explicitly invites — would
have both runs observe False across the await and both call
complete_erasure, advancing the generation twice. The decision has to
be one indivisible step in the store, which is the only place both runs
meet.
Source code in src/symfonic/services/privacy/saga_store.py
incomplete_under
async
¶
The descendant half of read suppression (see the port's docstring).
A scan, because the reference store is a mapping: a durable backend indexes the tenant segment and answers the same question with a query. Keys that are not scope keys are compared exactly rather than skipped — an unparseable key still names a saga somebody started, and dropping it would fail open for whoever recorded it.
Source code in src/symfonic/services/privacy/saga_store.py
mark_exhausted
async
¶
Bounded retry ran out. Recorded, never raised.
Raising here would abort the sweep and skip every participant sorted after the broken one — the exact failure mode the shipped legacy sweep already learned to avoid.
Source code in src/symfonic/services/privacy/saga_store.py
start
async
¶
Create, or resume and widen.
Widen, because PRIV-1's registry is live: a store registered while the saga was running still holds the subject's data, and a saga that captured its participant list at t=0 would leave it holding it.