Skip to content

symfonic.platform.fence_audit

fence_audit

EFX-ER-4 — a fence denial is an event, not a counter.

A refused write is the only externally visible sign that something is still writing to an erased subject: a stuck worker, a queue that was drained after the tombstone, a replica replaying an old log. The fence refuses it correctly and silently, and silence is the failure mode — an operator reading a dashboard sees a completed erasure and no writers, while a process on the other side of the cluster hammers the fence forever.

So the denial goes to the audit seam. This module is the adapter between the runtime-service value (:class:FenceDenial, which knows nothing about audit) and the platform seam (:class:AuditSeam, which knows nothing about fences) — which is why it lives on the platform side and the fence takes a callback.

fence_denial_recorder

fence_denial_recorder(audit: AuditSeam) -> Callable[[FenceDenial], Awaitable[None]]

A recorder a row-14 fence can call on every denial.

Not destructive: the denial prevented a mutation, so failing the already-refused write closed a second time would add nothing. The seam still degrades loudly (AUD-4) if the sink is down.

Source code in src/symfonic/platform/fence_audit.py
def fence_denial_recorder(audit: AuditSeam) -> Callable[[FenceDenial], Awaitable[None]]:
    """A recorder a row-14 fence can call on every denial.

    Not ``destructive``: the denial *prevented* a mutation, so failing the
    already-refused write closed a second time would add nothing. The seam still
    degrades loudly (AUD-4) if the sink is down.
    """

    async def record(denial: FenceDenial) -> None:
        await audit.record(
            AuditRecord(
                action=FENCE_DENIED_ACTION,
                outcome=str(denial.reason),
                principal_id="erasure-fence",
                scope_key=denial.scope_key,
                resource_type="subject",
                resource_id=denial.scope_key,
                metadata={
                    "reason": str(denial.reason),
                    "expected_generation": denial.expected_generation,
                    "observed_generation": denial.observed_generation,
                },
            )
        )

    return record