1pub const MIXED_WORK_PROTOCOL_SCHEMA_VERSION: u32 = 1;
11
12const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
13const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum MixedWorkQueueClass {
18 Scan,
20 Graph,
22 Parser,
24 Flow,
26 Control,
28}
29
30impl MixedWorkQueueClass {
31 #[must_use]
33 pub const fn as_str(self) -> &'static str {
34 match self {
35 Self::Scan => "scan",
36 Self::Graph => "graph",
37 Self::Parser => "parser",
38 Self::Flow => "flow",
39 Self::Control => "control",
40 }
41 }
42
43 const fn tag(self) -> u64 {
44 match self {
45 Self::Scan => 1,
46 Self::Graph => 2,
47 Self::Parser => 3,
48 Self::Flow => 4,
49 Self::Control => 5,
50 }
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
56pub enum MixedWorkUnitType {
57 ScanChunk,
59 ScanVerifier,
61 GraphFrontier,
63 GraphCompaction,
65 ParserShard,
67 ParserChangedRange,
69 FlowRelationDelta,
71 FlowFixpointStep,
73 DrainSentinel,
75}
76
77impl MixedWorkUnitType {
78 #[must_use]
80 pub const fn as_str(self) -> &'static str {
81 match self {
82 Self::ScanChunk => "scan_chunk",
83 Self::ScanVerifier => "scan_verifier",
84 Self::GraphFrontier => "graph_frontier",
85 Self::GraphCompaction => "graph_compaction",
86 Self::ParserShard => "parser_shard",
87 Self::ParserChangedRange => "parser_changed_range",
88 Self::FlowRelationDelta => "flow_relation_delta",
89 Self::FlowFixpointStep => "flow_fixpoint_step",
90 Self::DrainSentinel => "drain_sentinel",
91 }
92 }
93
94 const fn tag(self) -> u64 {
95 match self {
96 Self::ScanChunk => 11,
97 Self::ScanVerifier => 12,
98 Self::GraphFrontier => 21,
99 Self::GraphCompaction => 22,
100 Self::ParserShard => 31,
101 Self::ParserChangedRange => 32,
102 Self::FlowRelationDelta => 41,
103 Self::FlowFixpointStep => 42,
104 Self::DrainSentinel => 51,
105 }
106 }
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
111pub struct ResidentArtifactId(pub u32);
112
113impl ResidentArtifactId {
114 #[must_use]
116 pub const fn is_valid(self) -> bool {
117 self.0 != 0
118 }
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
123pub struct OutputSlabId(pub u32);
124
125impl OutputSlabId {
126 #[must_use]
128 pub const fn is_valid(self) -> bool {
129 self.0 != 0
130 }
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
135pub struct MixedWorkUnit {
136 pub sequence: u64,
138 pub queue_class: MixedWorkQueueClass,
140 pub unit_type: MixedWorkUnitType,
142 pub resident_artifact_id: ResidentArtifactId,
144 pub output_slab_id: OutputSlabId,
146 pub watchdog_budget_ticks: u32,
148 pub payload_digest: u64,
150}
151
152impl MixedWorkUnit {
153 #[must_use]
155 pub const fn new(
156 sequence: u64,
157 queue_class: MixedWorkQueueClass,
158 unit_type: MixedWorkUnitType,
159 resident_artifact_id: ResidentArtifactId,
160 output_slab_id: OutputSlabId,
161 watchdog_budget_ticks: u32,
162 payload_digest: u64,
163 ) -> Self {
164 Self {
165 sequence,
166 queue_class,
167 unit_type,
168 resident_artifact_id,
169 output_slab_id,
170 watchdog_budget_ticks,
171 payload_digest,
172 }
173 }
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178pub struct MixedWorkProtocolPlan<'a> {
179 pub units: &'a [MixedWorkUnit],
181 pub drain_watchdog_budget_ticks: u64,
183}
184
185impl<'a> MixedWorkProtocolPlan<'a> {
186 #[must_use]
188 pub const fn new(units: &'a [MixedWorkUnit], drain_watchdog_budget_ticks: u64) -> Self {
189 Self {
190 units,
191 drain_watchdog_budget_ticks,
192 }
193 }
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
198pub struct MixedWorkProtocolEvidence {
199 pub schema_version: u32,
201 pub unit_count: u32,
203 pub scan_units: u32,
205 pub graph_units: u32,
207 pub parser_units: u32,
209 pub flow_units: u32,
211 pub control_units: u32,
213 pub total_watchdog_budget_ticks: u64,
215 pub max_watchdog_budget_ticks: u32,
217 pub drain_watchdog_budget_ticks: u64,
219 pub bounded_drain: bool,
221 pub hidden_host_loop_count: u32,
223 pub deterministic_output_digest: u64,
225}
226
227impl MixedWorkProtocolEvidence {
228 #[must_use]
230 pub const fn covers_scan_graph_parser_flow(self) -> bool {
231 self.scan_units != 0
232 && self.graph_units != 0
233 && self.parser_units != 0
234 && self.flow_units != 0
235 }
236
237 #[must_use]
239 pub const fn is_complete(self) -> bool {
240 self.schema_version == MIXED_WORK_PROTOCOL_SCHEMA_VERSION
241 && self.unit_count != 0
242 && self.bounded_drain
243 && self.hidden_host_loop_count == 0
244 && self.deterministic_output_digest != 0
245 }
246}
247
248#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
250#[non_exhaustive]
251pub enum MixedWorkProtocolError {
252 #[error(
254 "mixed-work plan is empty. Fix: publish at least one resident work unit before scheduling."
255 )]
256 EmptyPlan,
257 #[error(
259 "mixed-work drain watchdog budget is zero. Fix: provide a positive resident drain budget."
260 )]
261 ZeroDrainWatchdogBudget,
262 #[error("mixed-work unit {sequence} has zero watchdog budget. Fix: assign a positive per-unit watchdog budget.")]
264 ZeroUnitWatchdogBudget {
265 sequence: u64,
267 },
268 #[error("mixed-work unit {sequence} has resident artifact id 0. Fix: publish a resident artifact before queueing work.")]
270 ZeroResidentArtifactId {
271 sequence: u64,
273 },
274 #[error("mixed-work unit {sequence} has output slab id 0. Fix: allocate a resident output slab before queueing work.")]
276 ZeroOutputSlabId {
277 sequence: u64,
279 },
280 #[error(
282 "mixed-work unit {sequence} routes {unit_type} through {queue_class}. Fix: use a unit type owned by the queue class."
283 )]
284 QueueClassMismatch {
285 sequence: u64,
287 queue_class: &'static str,
289 unit_type: &'static str,
291 },
292 #[error(
294 "mixed-work unit count {unit_count} overflows u32 evidence. Fix: shard the resident batch."
295 )]
296 UnitCountOverflow {
297 unit_count: usize,
299 },
300 #[error(
302 "mixed-work {queue_class} unit count overflowed u32 evidence. Fix: shard that queue class."
303 )]
304 ClassCountOverflow {
305 queue_class: &'static str,
307 },
308 #[error("mixed-work watchdog budget sum overflowed u64. Fix: shard the resident batch.")]
310 WatchdogBudgetOverflow,
311 #[error(
313 "mixed-work watchdog budget {total_watchdog_budget_ticks} exceeds drain budget {drain_watchdog_budget_ticks}. Fix: increase the drain budget or shard the resident batch."
314 )]
315 WatchdogBudgetExceeded {
316 total_watchdog_budget_ticks: u64,
318 drain_watchdog_budget_ticks: u64,
320 },
321}
322
323pub fn mixed_work_protocol_evidence(
331 plan: &MixedWorkProtocolPlan<'_>,
332) -> Result<MixedWorkProtocolEvidence, MixedWorkProtocolError> {
333 validate_mixed_work_protocol(plan)
334}
335
336pub fn validate_mixed_work_protocol(
343 plan: &MixedWorkProtocolPlan<'_>,
344) -> Result<MixedWorkProtocolEvidence, MixedWorkProtocolError> {
345 if plan.units.is_empty() {
346 return Err(MixedWorkProtocolError::EmptyPlan);
347 }
348 if plan.drain_watchdog_budget_ticks == 0 {
349 return Err(MixedWorkProtocolError::ZeroDrainWatchdogBudget);
350 }
351 if plan.units.len() > u32::MAX as usize {
352 return Err(MixedWorkProtocolError::UnitCountOverflow {
353 unit_count: plan.units.len(),
354 });
355 }
356
357 let mut counts = [0_u32; 5];
358 let mut total_watchdog_budget_ticks = 0_u64;
359 let mut max_watchdog_budget_ticks = 0_u32;
360 let mut digest = FNV_OFFSET;
361
362 for unit in plan.units {
363 validate_unit(*unit)?;
364 bump_class_count(&mut counts, unit.queue_class)?;
365 total_watchdog_budget_ticks = total_watchdog_budget_ticks
366 .checked_add(u64::from(unit.watchdog_budget_ticks))
367 .ok_or(MixedWorkProtocolError::WatchdogBudgetOverflow)?;
368 max_watchdog_budget_ticks = max_watchdog_budget_ticks.max(unit.watchdog_budget_ticks);
369 digest = mix_unit_digest(digest, *unit);
370 }
371
372 if total_watchdog_budget_ticks > plan.drain_watchdog_budget_ticks {
373 return Err(MixedWorkProtocolError::WatchdogBudgetExceeded {
374 total_watchdog_budget_ticks,
375 drain_watchdog_budget_ticks: plan.drain_watchdog_budget_ticks,
376 });
377 }
378
379 Ok(MixedWorkProtocolEvidence {
380 schema_version: MIXED_WORK_PROTOCOL_SCHEMA_VERSION,
381 unit_count: plan.units.len() as u32,
382 scan_units: counts[0],
383 graph_units: counts[1],
384 parser_units: counts[2],
385 flow_units: counts[3],
386 control_units: counts[4],
387 total_watchdog_budget_ticks,
388 max_watchdog_budget_ticks,
389 drain_watchdog_budget_ticks: plan.drain_watchdog_budget_ticks,
390 bounded_drain: true,
391 hidden_host_loop_count: 0,
392 deterministic_output_digest: digest,
393 })
394}
395
396fn validate_unit(unit: MixedWorkUnit) -> Result<(), MixedWorkProtocolError> {
397 if unit.watchdog_budget_ticks == 0 {
398 return Err(MixedWorkProtocolError::ZeroUnitWatchdogBudget {
399 sequence: unit.sequence,
400 });
401 }
402 if !unit.resident_artifact_id.is_valid() {
403 return Err(MixedWorkProtocolError::ZeroResidentArtifactId {
404 sequence: unit.sequence,
405 });
406 }
407 if !unit.output_slab_id.is_valid() {
408 return Err(MixedWorkProtocolError::ZeroOutputSlabId {
409 sequence: unit.sequence,
410 });
411 }
412 if !unit_type_matches_queue(unit.queue_class, unit.unit_type) {
413 return Err(MixedWorkProtocolError::QueueClassMismatch {
414 sequence: unit.sequence,
415 queue_class: unit.queue_class.as_str(),
416 unit_type: unit.unit_type.as_str(),
417 });
418 }
419 Ok(())
420}
421
422const fn unit_type_matches_queue(
423 queue_class: MixedWorkQueueClass,
424 unit_type: MixedWorkUnitType,
425) -> bool {
426 matches!(
427 (queue_class, unit_type),
428 (MixedWorkQueueClass::Scan, MixedWorkUnitType::ScanChunk)
429 | (MixedWorkQueueClass::Scan, MixedWorkUnitType::ScanVerifier)
430 | (MixedWorkQueueClass::Graph, MixedWorkUnitType::GraphFrontier)
431 | (
432 MixedWorkQueueClass::Graph,
433 MixedWorkUnitType::GraphCompaction
434 )
435 | (MixedWorkQueueClass::Parser, MixedWorkUnitType::ParserShard)
436 | (
437 MixedWorkQueueClass::Parser,
438 MixedWorkUnitType::ParserChangedRange
439 )
440 | (
441 MixedWorkQueueClass::Flow,
442 MixedWorkUnitType::FlowRelationDelta
443 )
444 | (
445 MixedWorkQueueClass::Flow,
446 MixedWorkUnitType::FlowFixpointStep
447 )
448 | (
449 MixedWorkQueueClass::Control,
450 MixedWorkUnitType::DrainSentinel
451 )
452 )
453}
454
455fn bump_class_count(
456 counts: &mut [u32; 5],
457 queue_class: MixedWorkQueueClass,
458) -> Result<(), MixedWorkProtocolError> {
459 let index = match queue_class {
460 MixedWorkQueueClass::Scan => 0,
461 MixedWorkQueueClass::Graph => 1,
462 MixedWorkQueueClass::Parser => 2,
463 MixedWorkQueueClass::Flow => 3,
464 MixedWorkQueueClass::Control => 4,
465 };
466 counts[index] =
467 counts[index]
468 .checked_add(1)
469 .ok_or(MixedWorkProtocolError::ClassCountOverflow {
470 queue_class: queue_class.as_str(),
471 })?;
472 Ok(())
473}
474
475fn mix_unit_digest(mut digest: u64, unit: MixedWorkUnit) -> u64 {
476 digest = fnv_mix(digest, unit.sequence);
477 digest = fnv_mix(digest, unit.queue_class.tag());
478 digest = fnv_mix(digest, unit.unit_type.tag());
479 digest = fnv_mix(digest, u64::from(unit.resident_artifact_id.0));
480 digest = fnv_mix(digest, u64::from(unit.output_slab_id.0));
481 digest = fnv_mix(digest, u64::from(unit.watchdog_budget_ticks));
482 fnv_mix(digest, unit.payload_digest)
483}
484
485fn fnv_mix(mut digest: u64, value: u64) -> u64 {
486 for byte in value.to_le_bytes() {
487 digest ^= u64::from(byte);
488 digest = digest.wrapping_mul(FNV_PRIME);
489 }
490 digest
491}
492
493#[cfg(test)]
494mod tests {
495 use super::{
496 mixed_work_protocol_evidence, validate_mixed_work_protocol, MixedWorkProtocolError,
497 MixedWorkProtocolPlan, MixedWorkQueueClass, MixedWorkUnit, MixedWorkUnitType, OutputSlabId,
498 ResidentArtifactId, MIXED_WORK_PROTOCOL_SCHEMA_VERSION,
499 };
500
501 fn unit(
502 sequence: u64,
503 queue_class: MixedWorkQueueClass,
504 unit_type: MixedWorkUnitType,
505 ) -> MixedWorkUnit {
506 MixedWorkUnit::new(
507 sequence,
508 queue_class,
509 unit_type,
510 ResidentArtifactId(100 + sequence as u32),
511 OutputSlabId(200 + sequence as u32),
512 10,
513 0xfeed_0000 + sequence,
514 )
515 }
516
517 #[test]
518 fn mixed_scan_graph_parser_flow_work_emits_deterministic_bounded_drain_evidence() {
519 let units = [
520 unit(1, MixedWorkQueueClass::Scan, MixedWorkUnitType::ScanChunk),
521 unit(
522 2,
523 MixedWorkQueueClass::Graph,
524 MixedWorkUnitType::GraphFrontier,
525 ),
526 unit(
527 3,
528 MixedWorkQueueClass::Parser,
529 MixedWorkUnitType::ParserShard,
530 ),
531 unit(
532 4,
533 MixedWorkQueueClass::Flow,
534 MixedWorkUnitType::FlowRelationDelta,
535 ),
536 unit(
537 5,
538 MixedWorkQueueClass::Control,
539 MixedWorkUnitType::DrainSentinel,
540 ),
541 ];
542 let plan = MixedWorkProtocolPlan::new(&units, 64);
543
544 let first = mixed_work_protocol_evidence(&plan)
545 .expect("Fix: valid mixed-work plan should emit evidence");
546 let second = validate_mixed_work_protocol(&plan)
547 .expect("Fix: valid mixed-work plan should emit stable evidence");
548
549 assert_eq!(first, second);
550 assert_eq!(first.schema_version, MIXED_WORK_PROTOCOL_SCHEMA_VERSION);
551 assert!(first.is_complete());
552 assert!(first.covers_scan_graph_parser_flow());
553 assert!(first.bounded_drain);
554 assert_eq!(first.hidden_host_loop_count, 0);
555 assert_eq!(first.unit_count, 5);
556 assert_eq!(first.total_watchdog_budget_ticks, 50);
557 assert_eq!(first.max_watchdog_budget_ticks, 10);
558 assert_ne!(first.deterministic_output_digest, 0);
559 }
560
561 #[test]
562 fn zero_watchdog_budget_is_rejected() {
563 let units = [MixedWorkUnit::new(
564 7,
565 MixedWorkQueueClass::Scan,
566 MixedWorkUnitType::ScanChunk,
567 ResidentArtifactId(1),
568 OutputSlabId(1),
569 0,
570 9,
571 )];
572 let plan = MixedWorkProtocolPlan::new(&units, 1);
573
574 assert!(matches!(
575 validate_mixed_work_protocol(&plan),
576 Err(MixedWorkProtocolError::ZeroUnitWatchdogBudget { sequence: 7 })
577 ));
578 }
579
580 #[test]
581 fn class_unit_mismatch_is_rejected() {
582 let units = [MixedWorkUnit::new(
583 9,
584 MixedWorkQueueClass::Parser,
585 MixedWorkUnitType::FlowFixpointStep,
586 ResidentArtifactId(1),
587 OutputSlabId(1),
588 1,
589 9,
590 )];
591 let plan = MixedWorkProtocolPlan::new(&units, 1);
592
593 assert!(matches!(
594 validate_mixed_work_protocol(&plan),
595 Err(MixedWorkProtocolError::QueueClassMismatch {
596 sequence: 9,
597 queue_class: "parser",
598 unit_type: "flow_fixpoint_step"
599 })
600 ));
601 }
602
603 #[test]
604 fn drain_budget_must_bound_all_units() {
605 let units = [
606 unit(1, MixedWorkQueueClass::Scan, MixedWorkUnitType::ScanChunk),
607 unit(
608 2,
609 MixedWorkQueueClass::Flow,
610 MixedWorkUnitType::FlowRelationDelta,
611 ),
612 ];
613 let plan = MixedWorkProtocolPlan::new(&units, 19);
614
615 assert!(matches!(
616 validate_mixed_work_protocol(&plan),
617 Err(MixedWorkProtocolError::WatchdogBudgetExceeded {
618 total_watchdog_budget_ticks: 20,
619 drain_watchdog_budget_ticks: 19
620 })
621 ));
622 }
623
624 #[test]
625 fn resident_artifact_and_output_slab_ids_are_required() {
626 let bad_artifact = [MixedWorkUnit::new(
627 1,
628 MixedWorkQueueClass::Scan,
629 MixedWorkUnitType::ScanChunk,
630 ResidentArtifactId(0),
631 OutputSlabId(1),
632 1,
633 1,
634 )];
635 assert!(matches!(
636 validate_mixed_work_protocol(&MixedWorkProtocolPlan::new(&bad_artifact, 1)),
637 Err(MixedWorkProtocolError::ZeroResidentArtifactId { sequence: 1 })
638 ));
639
640 let bad_slab = [MixedWorkUnit::new(
641 2,
642 MixedWorkQueueClass::Scan,
643 MixedWorkUnitType::ScanChunk,
644 ResidentArtifactId(1),
645 OutputSlabId(0),
646 1,
647 1,
648 )];
649 assert!(matches!(
650 validate_mixed_work_protocol(&MixedWorkProtocolPlan::new(&bad_slab, 1)),
651 Err(MixedWorkProtocolError::ZeroOutputSlabId { sequence: 2 })
652 ));
653 }
654}