pub struct RequiredSpec {
pub regions: Vec<String>,
pub zones: Vec<String>,
pub providers: Vec<String>,
pub mesh_tags: Vec<String>,
pub nodes: Vec<String>,
pub memory_mb: u32,
pub cpu_millis: u32,
pub tolerates: Vec<String>,
pub requires_taint: Option<String>,
pub replicas: Option<u32>,
}Expand description
F16 placement constraints declared on a MirrorProviderSlot, lives under
[providers.<role>] required = { regions = [...], mesh_tags = [...] } in
mirrors/<env>.toml.
Hard (must-satisfy) axes, all AND-ed together:
regions/zones/providers— membership: the machine’sregion/zone/providermust be one of the listed values.mesh_tags— superset: the machine’smesh_tagsmust contain every listed tag.memory_mb/cpu_millis— capacity floor (R572-F5): the machine’sallocatablebudget must cover the demand.0= no constraint.- taint repulsion — unconditional (R876-B7): the machine must not
carry any taint that
taint_effectclassifies asTaintEffect::Repels, unless that exact key is listed inSelf::tolerates. This axis is not declared; it applies to every spec. requires_taint— taint affinity (R572-F5): the machine must carry this taint key (intaintsormesh_tags).None= no affinity.
These are the only readers of MachineConfig::taints, which is
what makes taint_effect’s closed vocabulary well-founded.
MachineConfig::sovereign_group is deliberately not an axis here and
must not become one (W305/R742-F1). A sovereign group is a blast radius,
not a filter: which quorum a box votes in says nothing about whether a
workload may run on it, and a dev-group node exists precisely so dev-mode
services — stateful ones included — can be scheduled onto it. Filtering on
it would re-make the mistake W305 exists to undo, where one mechanism
silently carried three unrelated properties.
An empty / zero / None on every axis means “no constraint on that axis”.
A fully-unconstrained RequiredSpec matches every machine (see
RequiredSpec::is_unconstrained).
Fields§
§regions: Vec<String>§zones: Vec<String>§providers: Vec<String>§nodes: Vec<String>R833-F8: imperative placement — the machine must be one of these by
name. Empty (the default) = no constraint, which is every pre-R833-F8
caller.
This is the one axis that is not a capability the scheduler infers.
The operator typed --where=node:us-west-003, so it composes with the
other axes exactly like the rest — a named node that fails the capacity
floor or carries a repelling taint still does not match, and the refusal
names why rather than silently placing the work somewhere else.
memory_mb: u32R572-F5: minimum memory (MiB) the target node must have in its
declared allocatable budget. 0 = no constraint. Filled by
CloudConfig::admit_workload from the workload’s
memory_request_mb() — its placement request, which is not the
same number as the resources.memory_mb cgroup ceiling.
cpu_millis: u32R572-F5: minimum CPU (millicores) the target node must have in its
declared allocatable budget. 0 = no constraint. Filled by
CloudConfig::admit_workload from the workload’s resources.cpu_millis.
tolerates: Vec<String>R876-B7: repelling node taints this placement opts back in to.
Each entry is a machine taint key spelled exactly as it appears in
MachineConfig::taints — "no-appliance", not "appliance" — so the
node side and the workload side share one vocabulary and nothing has to
translate between them.
§Why this replaced repel_archetypes
Repulsion used to be opt-in-to-be-repelled: the spec named the
archetypes it was, and only a no-<that archetype> taint blocked it.
That field was #[serde(skip)], so a slot declared as
required = { ... } in a mirror TOML always deserialized with it empty
and Self::matches never read MachineConfig::taints at all. Node
taints were therefore structurally inert for every mirror-declared
placement, and inert silently — no-server is a legal key, so
yah cloud validate passed and an operator draining a node before
maintenance got a green run and a workload that never moved (R876-S2’s
drill measured exactly this against the real tree).
The sense is now inverted, which is the only shape that can survive a
field the wire does not carry: repulsion is unconditional and
toleration is declared. A spec that says nothing is repelled by every
repelling taint — the reading an operator writing taints = ["no-server"]
on a machine already assumed they were getting.
Toleration is per-key and absolute; there is no wildcard. Listing a key no machine declares is harmless and matches nothing.
[admission_spec] fills this from the placement group’s archetypes —
every repelling key that is not the group’s own class — which is what
makes the admit_workload path behave identically across this change
(R860-T4 / W338 §Placement consequences 2 still hold: the group’s
archetypes are the union over local requirement edges, so a Server
bound to an Appliance tolerates neither no-server nor
no-appliance).
requires_taint: Option<String>R572-F5: taint the workload requires the target node to carry
(annotation yah.placement.requires-taint). The node must have the
key in its taints list or mesh_tags. None = no affinity constraint.
replicas: Option<u32>R844-F8: how many machines this constraint places onto. None — the
only shape on disk before this field — means one, so every mirror in the
tree resolves byte-identically across the change.
This is not a match axis: it never appears in Self::matches and never
changes whether a given machine qualifies. It is the cardinality of the
answer, which is why it lives here rather than as another filter — the
operator declares what is required and how many of it, and the scheduler
picks which.
Declared, never inferred. The count is emphatically not “how many machines happen to match”: deriving it that way would make adding a box to the fleet silently scale a production front door. A constraint that matches four machines and asks for two places on two.
Fewer matches than asked is an error ([select_matching]), not a
partial placement. Placing one of two and reporting success is the
subset-that-looks-like-it-worked failure R844 exists to close.
Deliberately absent from Self::is_unconstrained, which answers “does
every machine match” — a question about the predicate, not the count. A
required = { replicas = 2 } with no axis is therefore still
unconstrained, and the deploy side still refuses it as an
underspecified placement.
Implementations§
Source§impl RequiredSpec
impl RequiredSpec
Sourcepub fn replica_count(&self) -> usize
pub fn replica_count(&self) -> usize
How many machines this constraint places onto — Self::replicas,
resolving the absent case to the pre-R844-F8 answer of one.
The single place that default is spelled, so the ingress planner and the deploy resolver cannot disagree about what “no replica count” means.
Sourcepub fn is_unconstrained(&self) -> bool
pub fn is_unconstrained(&self) -> bool
True when no declared axis carries a constraint — every untainted machine matches.
R876-B7: taint repulsion is deliberately absent from this conjunction,
unlike the repel_archetypes it replaced. Repulsion is no longer an axis
a spec declares — it applies to every spec — so including it would make
the answer a property of the fleet rather than of the constraint. Nor
does Self::tolerates belong here: a toleration widens the candidate
set, and the callers of this predicate ask “did the operator narrow
anything” in order to refuse an underspecified placement. A slot that
declares only a toleration has still narrowed nothing.
Sourcepub fn matches(&self, machine: &MachineConfig) -> bool
pub fn matches(&self, machine: &MachineConfig) -> bool
Whether machine satisfies every hard axis.
- Membership axes (region/zone/provider): machine must carry the field and it must appear in the constraint list.
mesh_tags: machine tags must be a superset of the required set.- R572-F5 capacity floor:
machine.allocatable.{memory,cpu}must coverself.{memory,cpu}. A machine with noallocatableblock passes unconditionally (capacity unknown → no constraint enforced). - Taint repulsion (R876-B7): machine must not carry any taint that
taint_effectclassifies asTaintEffect::Repels, unless that key is listed inSelf::tolerates. Applied unconditionally — this is the axis no spec has to declare, and the one that makes a node drainable. - R572-F5 taint affinity: if
requires_taintis set, the machine must carry that key in itstaintslist ormesh_tags.
A TaintEffect::Attracts key (today just public-ip) does not
repel: it is the affinity vocabulary, so reading it as repulsion would
evict every workload from the three nodes that carry it. Only the
no-<archetype> class repels, and taint_effect is the single
authority on which is which — which is why
crate::validate::check_inert_taints refuses to let an unclassifiable
key be declared: it would read as a constraint and be none.
Trait Implementations§
Source§impl Clone for RequiredSpec
impl Clone for RequiredSpec
Source§fn clone(&self) -> RequiredSpec
fn clone(&self) -> RequiredSpec
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreSource§impl Debug for RequiredSpec
impl Debug for RequiredSpec
Source§impl Default for RequiredSpec
impl Default for RequiredSpec
Source§fn default() -> RequiredSpec
fn default() -> RequiredSpec
Source§impl<'de> Deserialize<'de> for RequiredSpec
impl<'de> Deserialize<'de> for RequiredSpec
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
Auto Trait Implementations§
impl Freeze for RequiredSpec
impl RefUnwindSafe for RequiredSpec
impl Send for RequiredSpec
impl Sync for RequiredSpec
impl Unpin for RequiredSpec
impl UnsafeUnpin for RequiredSpec
impl UnwindSafe for RequiredSpec
Blanket Implementations§
impl<T> Allocation for T
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> DeserializeOwned for Twhere
T: for<'de> Deserialize<'de>,
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can
then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.Source§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be
further downcast into Rc<ConcreteType> where ConcreteType implements Trait.Source§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.Source§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>, which can then be
downcast into Box<dyn ConcreteType> where ConcreteType implements Trait.Source§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Rc<Trait> (where Trait: Downcast) to Rc<Any>, which can then be further
downcast into Rc<ConcreteType> where ConcreteType implements Trait.Source§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.Source§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.Source§impl<T> DowncastSend for T
impl<T> DowncastSend for T
Source§impl<T> DowncastSync for T
impl<T> DowncastSync for T
Source§impl<T> DowncastSync for T
impl<T> DowncastSync for T
impl<T> ErasedDestructor for Twhere
T: 'static,
impl<T> Fruit for T
impl<A, B, T> HttpServerConnExec<A, B> for Twhere
B: Body,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more