You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Decide how the model meets soft scheduling, spread, anti-affinity and parked replicas #355
How does the model express the scheduling shapes the live estate uses beyond hard placement, and a Deployment held at zero replicas?
The inventory found weighted scheduling preferences, node or pod, in ten Deployments, which chapter 10 forbids outright (0017). It also found a topology spread constraint on home-portal, pod anti-affinity between adguard-ha's two Deployments, and agents-api, agents-api-ws, agents-ui and the knowledge platform's ingest worker parked at zero replicas, which a Process cannot express (RENDER-GAPS row K8).
Settled with the maintainer on 2026-10-09. Input: the live objects in JorisJonkers-dev/fleet-infra at 4e4a9bb (grep -rl -E "preferredDuringScheduling|topologySpreadConstraints|podAntiAffinity|replicas: 0" cluster/flux/apps) and docs/research/live-estate-needs.md.
What is live
Eight node preferences, each weight 100: arch: arm64 on app-ui, auth-ui and agents-ui; site: frankfurt on tribelt, outline and grimoire; gpu-model-t1000 on jellyfin and immich-machine-learning.
Three topology spreads (ScheduleAnyway, hostname) on app-ui, auth-ui and agents-ui, running at 1, 1 and 0 replicas, so the spreads constrain nothing.
One soft pod anti-affinity between adguard-ha-a and adguard-ha-b. These are two Deployments of one replica each, behind two Services and two VIPs, and nothing stops both landing on one node.
Four parked Deployments: agents-api, agents-api-ws and agents-ui while hermes takes over, and knowledge-ingest-worker until its Vault credentials land. The four agents CronJobs are suspended as well.
Decisions
No soft term enters the model.0017 stands unchanged. All eight node preferences are dropped and nothing replaces them. If latency to Postgres bites, the author of tribelt, outline or grimoire writes a hard site: frankfurt, which the model already allows.
Hard terms must be able to lock tightly, so gpu gains vendor and model. Both are optional, hard and set-valued, and are matched against the node contract's gpus[].vendor and gpus[].model, which the node files already declare. jellyfin writes gpu: {class: transcode, model: [t1000]}. immich-machine-learning writes vendor: [nvidia], because its image is CUDA. An unmatched value is E_PLACEMENT_UNSATISFIABLE. No other new lock is needed: site, arch, disk.media and capabilities are already hard, and there is still no node-id term.
A node dataset is bound to the node that holds it, not to a capability. This refines decision 10 of Decide how the model meets the live needs no chapter covers. The Platform document declares each dataset on the node whose disks hold it, for example media: {node: enschede-t1000-1, path: /srv/media}. Mounting the dataset derives a hard pin to that node, the same way a local-path binding already pins a claim. Any other term that contradicts the pin is E_PLACEMENT_UNSATISFIABLE. Jellyfin cannot move away from its drives, and that lock comes from its data, not from a filter that happens to match one node, as samba does today. Moving the data is a Platform edit, and its consumers follow it.
replicas stays a capacity statement, with an opt-in hard spread. As chapter 10 says, two replicas on one node are legal. outline wants exactly that on single-node Frankfurt. replicas: {count, reason, across: node} renders required pod anti-affinity on the hostname. When fewer eligible nodes exist than count, the build refuses it with E_PLACEMENT_UNSATISFIABLE. across admits node only. The three inert UI spreads are dropped.
adguard-ha becomes one Process, replicas: {count: 2, reason, across: node}, behind one L4 exposure: one VIP with the Local traffic policy. MetalLB re-announces the VIP from the node of the surviving replica. This replaces the live two-VIP design, so the router gets one DNS address during handover.
parked: {reason} is a new Process field, for any lifecycle. It renders zero replicas for a Deployment or StatefulSet, and suspend: true for a scheduled CronJob. Claims, grants, the Service and the network policy stay rendered, so parking never prunes data. A parked Process gets no PodDisruptionBudget and is left out of the Application's switchover readiness. It still runs every build check, eligibility included, so unparking is a one-field edit that cannot be the first time placement fails. An edge from a running Process to a parked provider is allowed, and the reason is the record. This settles RENDER-GAPS row K8's parked half.
Record
This answer is recorded here, on the ticket, the same way as its parent. The new terms (Parked, across, the gpuvendor and model) and the node-bound Node Dataset enter CONTEXT.md, spec/v1 (chapters 10, 14 and 20) and their ADRs with the spec rewrite. 0017 is not rewritten, because its decision holds.
The conditions flagged on this decision are settled in Decide which conditions on the settled decisions change the model. Item 4's anti-affinity term carries matchLabelKeys: [pod-template-hash], so a surging rollout places its new pod when replicas equal eligible nodes; a drill confirmed both the stall and the fix. Item 5 stands, as failover and not load sharing, with a drill of the failover gap as a condition of the utility handover. Item 6's suspended CronJob comes with a derived start deadline of 300 seconds, and a run in flight finishes.
Activity
ExtraToast commentedon Oct 9, 2026
Resolution
Settled with the maintainer on 2026-10-09. Input: the live objects in
JorisJonkers-dev/fleet-infraat4e4a9bb(grep -rl -E "preferredDuringScheduling|topologySpreadConstraints|podAntiAffinity|replicas: 0" cluster/flux/apps) and docs/research/live-estate-needs.md.What is live
arch: arm64on app-ui, auth-ui and agents-ui;site: frankfurton tribelt, outline and grimoire;gpu-model-t1000on jellyfin and immich-machine-learning.ScheduleAnyway, hostname) on app-ui, auth-ui and agents-ui, running at 1, 1 and 0 replicas, so the spreads constrain nothing.Decisions
site: frankfurt, which the model already allows.gpugainsvendorandmodel. Both are optional, hard and set-valued, and are matched against the node contract'sgpus[].vendorandgpus[].model, which the node files already declare. jellyfin writesgpu: {class: transcode, model: [t1000]}. immich-machine-learning writesvendor: [nvidia], because its image is CUDA. An unmatched value isE_PLACEMENT_UNSATISFIABLE. No other new lock is needed:site,arch,disk.mediaandcapabilitiesare already hard, and there is still no node-id term.media: {node: enschede-t1000-1, path: /srv/media}. Mounting the dataset derives a hard pin to that node, the same way alocal-pathbinding already pins a claim. Any other term that contradicts the pin isE_PLACEMENT_UNSATISFIABLE. Jellyfin cannot move away from its drives, and that lock comes from its data, not from a filter that happens to match one node, assambadoes today. Moving the data is a Platform edit, and its consumers follow it.replicasstays a capacity statement, with an opt-in hard spread. As chapter 10 says, two replicas on one node are legal. outline wants exactly that on single-node Frankfurt.replicas: {count, reason, across: node}renders required pod anti-affinity on the hostname. When fewer eligible nodes exist thancount, the build refuses it withE_PLACEMENT_UNSATISFIABLE.acrossadmitsnodeonly. The three inert UI spreads are dropped.replicas: {count: 2, reason, across: node}, behind one L4 exposure: one VIP with theLocaltraffic policy. MetalLB re-announces the VIP from the node of the surviving replica. This replaces the live two-VIP design, so the router gets one DNS address during handover.parked: {reason}is a new Process field, for any lifecycle. It renders zero replicas for a Deployment or StatefulSet, andsuspend: truefor ascheduledCronJob. Claims, grants, the Service and the network policy stay rendered, so parking never prunes data. A parked Process gets no PodDisruptionBudget and is left out of the Application's switchover readiness. It still runs every build check, eligibility included, so unparking is a one-field edit that cannot be the first time placement fails. An edge from a running Process to a parked provider is allowed, and thereasonis the record. This settles RENDER-GAPS row K8's parked half.Record
This answer is recorded here, on the ticket, the same way as its parent. The new terms (Parked,
across, thegpuvendorandmodel) and the node-bound Node Dataset enterCONTEXT.md,spec/v1(chapters 10, 14 and 20) and their ADRs with the spec rewrite. 0017 is not rewritten, because its decision holds.ExtraToast commentedon Oct 12, 2026
The conditions flagged on this decision are settled in Decide which conditions on the settled decisions change the model. Item 4's anti-affinity term carries
matchLabelKeys: [pod-template-hash], so a surging rollout places its new pod when replicas equal eligible nodes; a drill confirmed both the stall and the fix. Item 5 stands, as failover and not load sharing, with a drill of the failover gap as a condition of the utility handover. Item 6's suspended CronJob comes with a derived start deadline of 300 seconds, and a run in flight finishes.