Virtual object keying
Each Restate VO uses the narrowest key that gives the serialization it needs:DeployServiceis keyed bydeployment_id. Each deployment runs as its own isolated workflow, so multiple deployments in the same environment can build in parallel.RoutingServiceis keyed byenv_id. All routing changes for an environment (frontline route assignment + the live-deployment swap) serialize here, so concurrent deploys/rollbacks/promotes for the same env can never race onapps.current_deployment_id.BuildSlotServiceis keyed byworkspace_id. Caps how many deployments build at once across the workspace and prioritises production over preview waiters.DeploymentService(delayed desired-state transitions) is keyed bydeployment_id.
RoutingService.SwapLiveDeployment, which is per-env serialized.
Key components:
- Control API deployment service —
svc/ctrl/services/deployment DeployServiceRestate workflow —svc/ctrl/worker/deployRoutingService(route assignment + live swap) —svc/ctrl/worker/routingDeploymentServiceVO for delayed desired-state transitions —svc/ctrl/worker/deploymentBuildSlotServiceVO for per-workspace build concurrency —svc/ctrl/worker/buildslot- Dedup helper for cancelling superseded queued siblings —
svc/ctrl/dedup
Flow: create deployment
Flow: cancel deployment
Flow: promote
Flow: rollback
BuildSlotService (workspace concurrency)
BuildSlotService caps concurrent builds per workspace. It is a Restate VO keyed by workspace_id, which makes Acquire and Release race-free across concurrent deploy handlers.
State held in the VO:
active_slots— set of deployment IDs currently holding a slotprod_wait_list— FIFO of production waiterspreview_wait_list— FIFO of non-production waiters
- Deploy handler creates a Restate awakeable and calls
AcquireOrWait(deployment_id, awakeable_id, is_production). - The workspace’s
max_concurrent_buildsquota is fetched. Iflen(active_slots) < limit, the deployment is added toactive_slotsand the awakeable is resolved immediately. - Otherwise the deployment is appended to
prod_wait_list(if production) orpreview_wait_list(if not). The Deploy handler stays suspended onawakeable.Result().
- Deploy handler calls
Release(deployment_id)— from the success path explicitly, or from the compensation stack on failure/cancel. - If the deployment was in
active_slots, it is removed and a waiter is promoted:prod_wait_listfirst, thenpreview_wait_list. The promoted waiter’s awakeable is resolved. - If the deployment was in either wait list (cancelled before it ever got a slot), it is removed.
quota.max_concurrent_builds per workspace.
Commit deduplication
When a new deployment is created,dedup.CancelOlderSiblings looks for older deployments on the same (app, environment, branch) that are still in the build queue (pending or awaiting_approval) and cancels them.
Once a deployment acquires a build slot and transitions to starting, it is committed — newer commits will not supersede it. This avoids the pathological case where rapid pushes keep cancelling builds and nothing ever finishes.
Cancellation happens in three steps, all batched:
- One SELECT — list older queued sibling deployments with their invocation IDs.
- One batch UPDATE — stamp every sibling’s in-flight steps with
"Superseded by newer commit"(first-write-wins viaWHERE ended_at IS NULL). - One batch UPDATE — transition every sibling to
status=superseded. - N HTTP calls —
restateAdmin.CancelInvocationfor each sibling that has an invocation ID.
Instance readiness (awakeable-based)
AftercreateTopologies, ensureSentinelRows, and ensureCiliumNetworkPolicy, the Deploy handler fires off SentinelService.Deploy RPCs as RequestFutures (non-blocking) and then enters waitForDeployments. Both waits happen in parallel on krane’s side.
waitForDeployments flow:
- Load per-region min replicas via
FindDeploymentTopologyMinReplicas. - Count running instances per region.
- Require
numRegions - 1healthy regions (minimum 1, tolerating one regional outage). - Repeat the DB check until the threshold is met or
regionReadyTimeoutelapses.
Self-skip (belt-and-suspenders dedup)
In addition to the proactive cancel above, the Deploy handler checksHasNewerActiveDeployment at the top of its workflow. If a newer sibling on the same (app, env, branch) is already pending, starting, building, deploying, network, finalizing, ready, or awaiting_approval, the current deployment self-skips. This catches races where the proactive cancel didn’t land (e.g. the newer deployment hadn’t persisted its invocation ID yet).
State serialization (desired state)
Scheduled state changes are serialized via a Restate virtual object keyed by deployment ID insvc/ctrl/worker/deployment. The object stores a nonce for the most recent transition so older delayed requests no-op.
Retry policy
TheDeployService is registered with an exponential-backoff retry policy: 30s → 1m → 2m → 4m → 5m (capped), 10 attempts total (~30 minutes). If a deploy can’t make progress after 10 retries (persistent MySQL connection errors, Depot outage), Restate kills the invocation, the compensation stack runs, and the deployment is marked failed. This replaces an older 150-attempt policy that could leave a deploy stuck retrying for ~24 hours.
Compensation stack
The Deploy handler maintains a LIFO compensation stack registered viaCompensation.Add (for side-effects wrapped in restate.RunVoid) and Compensation.AddCtx (for raw ObjectContext operations like BuildSlotService.Release().Send). The stack fires on any error or cancellation:
- Release the build slot
- Mark the deployment as
failed(only if still in an active status — the conditionalUpdateDeploymentStatusIfActivequery prevents overwritingsupersededorready) - Undo topology inserts, route assignments, etc.

