Skip to content

symfonic.services.switching.store

store

CUT-SS — the switch-state store: one record per bundle, per-key CAS.

The contract is the compare-and-swap semantics, not the vendor. This module ships the in-process reference backend (CON-S-4); a Postgres/etcd/DynamoDB adapter satisfies the same protocol by conditionally writing on epoch.

BundleRecord dataclass

BundleRecord(bundle_id: str, epoch: int, generation_vector: GenerationVector, freeze_state: FreezeState = NO_FREEZE, audit_head: str = '0' * 64, rollback_vector: GenerationVector | None = None, committed_at: float = 0.0)

CUT-SS-1/CUT-SS-3 — the entire binding state of one bundle.

One record, one atomic write. There is deliberately no way to express a half-switched bundle: the vector, the freeze and the audit head move together or not at all.

InMemorySwitchStore

InMemorySwitchStore()

Per-key linearizable CAS over an in-process dict, with epoch history.

Every mutation takes the same lock and re-reads the current epoch inside it, so a lost race raises SwitchConflictError instead of overwriting. set_unreachable models the control-plane outage the availability rules in CUT-BR exist to survive.

Source code in src/symfonic/services/switching/store.py
def __init__(self) -> None:
    self._records: dict[str, BundleRecord] = {}
    self._history: dict[str, list[BundleRecord]] = {}
    self._lock = asyncio.Lock()
    self._unreachable = False

compare_and_swap async

compare_and_swap(record: BundleRecord, *, expected_epoch: int) -> BundleRecord

CUT-SS-2 — commit record iff the stored epoch is still expected.

Source code in src/symfonic/services/switching/store.py
async def compare_and_swap(
    self, record: BundleRecord, *, expected_epoch: int
) -> BundleRecord:
    """CUT-SS-2 — commit ``record`` iff the stored epoch is still expected."""
    async with self._lock:
        self._guard()
        current = self._records.get(record.bundle_id)
        if current is None:
            raise BindingUnavailableError(
                f"cannot compare-and-swap unknown bundle {record.bundle_id!r}."
            )
        if current.epoch != expected_epoch:
            raise SwitchConflictError(
                f"compare-and-swap on bundle {record.bundle_id!r} expected epoch "
                f"{expected_epoch} but the store is at {current.epoch}; the "
                "mutation was not applied."
            )
        stored = replace(record, epoch=current.epoch + 1)
        self._records[stored.bundle_id] = stored
        self._history.setdefault(stored.bundle_id, []).append(stored)
        return stored

history async

history(bundle_id: str) -> tuple[BundleRecord, ...]

CUT-SS-7 — the full epoch history, for audit reconstruction.

Source code in src/symfonic/services/switching/store.py
async def history(self, bundle_id: str) -> tuple[BundleRecord, ...]:
    """CUT-SS-7 — the full epoch history, for audit reconstruction."""
    self._guard()
    return tuple(self._history.get(bundle_id, ()))