1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum CordonClass {
13 Never,
16 SimPool,
19 Deferrable,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
29pub enum WorkerRole {
30 Solver,
33 SimDriver,
37 Resolve,
40 Merge,
43 PoolStateUpdater,
47 Registrar,
49 Verifier,
51 Submitter,
54}
55
56pub const ALL_ROLES: [WorkerRole; 8] = [
60 WorkerRole::Solver,
61 WorkerRole::SimDriver,
62 WorkerRole::Resolve,
63 WorkerRole::Merge,
64 WorkerRole::PoolStateUpdater,
65 WorkerRole::Registrar,
66 WorkerRole::Verifier,
67 WorkerRole::Submitter,
68];
69
70pub const V1_ACTIVE_ROLES: [WorkerRole; 5] = [
74 WorkerRole::Solver,
75 WorkerRole::SimDriver,
76 WorkerRole::Resolve,
77 WorkerRole::Merge,
78 WorkerRole::PoolStateUpdater,
79];
80
81impl WorkerRole {
82 #[must_use]
86 pub const fn cordon_class(self) -> CordonClass {
87 match self {
88 Self::SimDriver => CordonClass::SimPool,
89 Self::PoolStateUpdater | Self::Registrar | Self::Verifier => CordonClass::Deferrable,
90 Self::Solver | Self::Resolve | Self::Merge | Self::Submitter => CordonClass::Never,
91 }
92 }
93
94 #[must_use]
97 pub const fn v1_active(self) -> bool {
98 matches!(
99 self,
100 Self::Solver | Self::SimDriver | Self::Resolve | Self::Merge | Self::PoolStateUpdater
101 )
102 }
103
104 #[must_use]
106 pub const fn is_pinnable(self) -> bool {
107 matches!(self, Self::Solver | Self::Merge)
108 }
109
110 #[must_use]
113 pub const fn census_resource(self) -> &'static str {
114 match self {
115 Self::Solver => "fleet_solver_slots",
116 Self::SimDriver => "fleet_simdriver_slots",
117 Self::Resolve => "fleet_resolve_slots",
118 Self::Merge => "fleet_merge_slots",
119 Self::PoolStateUpdater => "fleet_pool_state_updater_slots",
120 Self::Registrar => "fleet_registrar_slots",
121 Self::Verifier => "fleet_verifier_slots",
122 Self::Submitter => "fleet_submitter_slots",
123 }
124 }
125
126 #[must_use]
129 pub const fn thread_name(self) -> &'static str {
130 match self {
131 Self::Solver => "work-fleet-solver-{n}",
132 Self::SimDriver => "work-fleet-sim-{n}",
133 Self::Resolve => "work-fleet-resolve-{n}",
134 Self::Merge => "work-fleet-merge-{n}",
135 Self::PoolStateUpdater => "work-fleet-poolupd-{n}",
136 Self::Registrar => "work-fleet-registrar-{n}",
137 Self::Verifier => "work-fleet-verifier-{n}",
138 Self::Submitter => "work-fleet-submit-{n}",
139 }
140 }
141
142 #[must_use]
144 pub const fn census_kind(self) -> &'static str {
145 match self {
146 Self::Solver => "fleet worker slot: LPT-bin pin (RAYPAR T3)",
147 Self::SimDriver => "fleet worker slot: pooled sim driver",
148 Self::Resolve => "fleet worker slot: resolve chunk",
149 Self::Merge => "fleet worker slot: merge sidecar pin",
150 Self::PoolStateUpdater => "fleet worker slot: pool-state updater",
151 Self::Registrar => "fleet worker slot: registrar",
152 Self::Verifier => "fleet worker slot: verifier",
153 Self::Submitter => "fleet worker slot: submitter",
154 }
155 }
156
157 #[must_use]
159 pub const fn census_sizing(self) -> &'static str {
160 match self {
161 Self::Solver => "pins = one seat per LPT bin (floor(Q) minus the solve headroom, the structural bin count; walk admission stays the Solver share)",
162 Self::SimDriver => "slot cap = 4 (today's SimSlots cap), duty-counted; fractional-quota remainder spendable here",
163 Self::Resolve => "fixed v1 (1 slot, 12.4 ms/cycle measured)",
164 Self::Merge => "exactly one sidecar",
165 Self::PoolStateUpdater => {
166 "slot cap = fleet.pool_state_updater_slots (default 4), duty-counted; \
167 fractional-quota remainder spendable here; Deferrable cordon class"
168 }
169 Self::Registrar | Self::Verifier | Self::Submitter => "declared, unhosted in v1",
170 }
171 }
172
173 #[must_use]
175 pub const fn label(self) -> &'static str {
176 match self {
177 Self::Solver => "solver",
178 Self::SimDriver => "sim_driver",
179 Self::Resolve => "resolve",
180 Self::Merge => "merge",
181 Self::PoolStateUpdater => "pool_state_updater",
182 Self::Registrar => "registrar",
183 Self::Verifier => "verifier",
184 Self::Submitter => "submitter",
185 }
186 }
187
188 #[must_use]
190 pub const fn index_in_all_roles(self) -> Option<u8> {
191 match self {
192 Self::Solver => Some(0),
193 Self::SimDriver => Some(1),
194 Self::Resolve => Some(2),
195 Self::Merge => Some(3),
196 Self::PoolStateUpdater => Some(4),
197 Self::Registrar => Some(5),
198 Self::Verifier => Some(6),
199 Self::Submitter => Some(7),
200 }
201 }
202}
203
204#[cfg(test)]
205#[expect(clippy::expect_used)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn all_roles_is_the_sized_8_role_table() {
211 assert_eq!(ALL_ROLES.len(), 8, "ADR-042 Q2: exactly 8 declared roles");
212 assert_eq!(
213 ALL_ROLES,
214 [
215 WorkerRole::Solver,
216 WorkerRole::SimDriver,
217 WorkerRole::Resolve,
218 WorkerRole::Merge,
219 WorkerRole::PoolStateUpdater,
220 WorkerRole::Registrar,
221 WorkerRole::Verifier,
222 WorkerRole::Submitter,
223 ],
224 "ALL_ROLES must list every variant exactly once, positions are the conformance script"
225 );
226 }
227
228 #[test]
229 fn u8_index_round_trips_through_the_table() {
230 for (i, role) in ALL_ROLES.iter().enumerate() {
231 let idx = usize::from(role.index_in_all_roles().expect("all roles indexed"));
232 assert_eq!(idx, i, "index_in_all_roles must match ALL_ROLES position");
233 assert_eq!(ALL_ROLES[idx], *role, "round trip");
234 }
235 }
236
237 #[test]
238 fn v1_active_is_exactly_the_hosted_prefix() {
239 for (i, role) in ALL_ROLES.iter().enumerate() {
242 assert_eq!(
243 role.v1_active(),
244 i < V1_ACTIVE_ROLES.len(),
245 "v1-active must be exactly {V1_ACTIVE_ROLES:?}"
246 );
247 }
248 assert_eq!(V1_ACTIVE_ROLES, ALL_ROLES[..V1_ACTIVE_ROLES.len()]);
249 }
250
251 #[test]
252 fn cordon_classes_match_the_sign_off_table() {
253 let class = |r: WorkerRole| match r.cordon_class() {
257 CordonClass::Never => "never",
258 CordonClass::SimPool => "sim-pool",
259 CordonClass::Deferrable => "deferrable",
260 };
261 assert_eq!(class(WorkerRole::SimDriver), "sim-pool");
262 for r in [
263 WorkerRole::PoolStateUpdater,
264 WorkerRole::Registrar,
265 WorkerRole::Verifier,
266 ] {
267 assert_eq!(class(r), "deferrable", "declared background set: {r:?}");
268 }
269 for r in [
270 WorkerRole::Solver,
271 WorkerRole::Resolve,
272 WorkerRole::Merge,
273 WorkerRole::Submitter,
274 ] {
275 assert_eq!(class(r), "never", "cordon-invariant role: {r:?}");
276 }
277 }
278
279 #[test]
280 fn only_solver_and_merge_are_pinnable() {
281 for role in ALL_ROLES {
282 assert_eq!(
283 role.is_pinnable(),
284 matches!(role, WorkerRole::Solver | WorkerRole::Merge)
285 );
286 }
287 }
288
289 #[test]
290 fn census_and_thread_names_are_distinct_and_greppable() {
291 let mut resources = Vec::new();
292 let mut threads = Vec::new();
293 for role in ALL_ROLES {
294 resources.push(role.census_resource());
295 threads.push(role.thread_name());
296 assert!(role.thread_name().starts_with("work-fleet-"));
297 assert!(role.census_kind().starts_with("fleet worker slot"));
298 assert!(!role.census_sizing().is_empty());
299 }
300 let n = resources.len();
301 resources.sort_unstable();
302 threads.sort_unstable();
303 resources.dedup();
304 threads.dedup();
305 assert_eq!(resources.len(), n, "census resource ids must be unique");
306 assert_eq!(
307 threads.len(),
308 n,
309 "thread-name patterns must be unique (GOQWCL)"
310 );
311 }
312
313 #[test]
314 fn gauge_labels_are_snake_case() {
315 for role in ALL_ROLES {
316 let label = role.label();
317 assert!(!label.is_empty());
318 assert!(label.chars().all(|c| c.is_ascii_lowercase() || c == '_'));
319 }
320 }
321}