Skip to content

symfonic.services.switching.admission

admission

Atomic bind-and-register admission (CUT-SS-4, CUT-AIR-2, SCP-FRZ-1).

Binding and registering are one operation. Split them and there is a window in which an invocation holds a generation nothing knows about — which is exactly the window a drain proof would miss, and exactly the invocation that would keep running after a freeze said stop.

AdmissionController

AdmissionController(*, source: Any, registry: ActiveInvocationRegistry | None = None, worker_id: str = 'worker', barrier: QuiescenceBarrier | None = None, clock: Callable[[], float] = time.time)

Binds a generation and registers the invocation under one lock.

Source code in src/symfonic/services/switching/admission.py
def __init__(
    self,
    *,
    source: Any,
    registry: ActiveInvocationRegistry | None = None,
    worker_id: str = "worker",
    barrier: QuiescenceBarrier | None = None,
    clock: Callable[[], float] = time.time,
) -> None:
    self._source = source
    self._registry = registry
    self._worker_id = worker_id
    self._barrier = barrier
    self._clock = clock
    self._lock = registry.admission_lock if registry is not None else asyncio.Lock()

acknowledge_freeze async

acknowledge_freeze(bundle_id: str) -> str | None

CUT-BR-6 — this worker states that it has observed the freeze.

Source code in src/symfonic/services/switching/admission.py
async def acknowledge_freeze(self, bundle_id: str) -> str | None:
    """CUT-BR-6 — this worker states that it has observed the freeze."""
    binding = await self._source.resolve(bundle_id)
    epoch_id = binding.freeze_state.freeze_epoch_id
    if epoch_id is not None and self._barrier is not None:
        self._barrier.acknowledge(self._worker_id, epoch_id)
    return epoch_id

admit async

admit(*, invocation_id: str, bundle_id: str, tenant_scope_hash: str, inherited_pin: InvocationPin | None = None) -> BoundGeneration

Resolve one snapshot and register it, atomically against the freeze.

Source code in src/symfonic/services/switching/admission.py
async def admit(
    self,
    *,
    invocation_id: str,
    bundle_id: str,
    tenant_scope_hash: str,
    inherited_pin: InvocationPin | None = None,
) -> BoundGeneration:
    """Resolve one snapshot and register it, atomically against the freeze."""
    if inherited_pin is not None:
        return await self._admit_pinned(
            inherited_pin, invocation_id=invocation_id, scope=tenant_scope_hash
        )
    async with self._lock:
        binding = await self._source.resolve(bundle_id)
        self._require_admissible(binding, binding)
        return self._register(
            binding.pin(), binding, invocation_id, tenant_scope_hash
        )

resume async

resume(pin: InvocationPin | None, *, invocation_id: str, tenant_scope_hash: str = '') -> BoundGeneration

Resume under the artifact's pin, not under this worker's binding.

Source code in src/symfonic/services/switching/admission.py
async def resume(
    self,
    pin: InvocationPin | None,
    *,
    invocation_id: str,
    tenant_scope_hash: str = "",
) -> BoundGeneration:
    """Resume under the artifact's pin, not under this worker's binding."""
    if pin is None:
        raise PinlessArtifactError(
            "this artifact carries no generation pin, and no pin-less artifact "
            "policy is configured; resuming it would silently place it on "
            "whatever generation this worker happens to run (CUT-PIN-1)."
        )
    return await self._admit_pinned(
        pin, invocation_id=invocation_id, scope=tenant_scope_hash
    )

BoundGeneration dataclass

BoundGeneration(pin: InvocationPin, binding: BundleBinding, record: InvocationRecord | None = None)

What one invocation captured: a pin, its binding, and its registry row.

record is None in library mode: CUT-AIR-5 makes the registry operated-platform-only, and a library adopter must not find themselves maintaining a drain-proof table they have no control plane to drain for.