1use crate::scheduler::{CancellationCause, RequestId, WorkId};
7use crate::{BoundedCompletion, BoundedCompletionWait, BoundedSubmissionOutcome, Submission};
8
9pub trait ConsensusTransport {
15 type Error: std::error::Error;
17
18 fn participant_count(&self) -> usize;
20
21 fn all_gather_words(&self, local: &[u32]) -> Result<Vec<u32>, Self::Error>;
23}
24
25pub trait BoundedConsensusTransport: ConsensusTransport {
27 type Completion: BoundedCompletion;
29 type GatherOutput;
31
32 fn submit_all_gather_words(
34 &self,
35 local: &[u32],
36 ) -> Result<Submission<Self::GatherOutput, Self::Completion>, Self::Error>;
37
38 fn resolve_all_gather_words(&self, output: Self::GatherOutput)
40 -> Result<Vec<u32>, Self::Error>;
41}
42
43#[derive(Debug, Clone, Copy, Eq, PartialEq)]
45pub struct ScheduledWork<'a> {
46 pub id: WorkId,
48 pub descriptor: &'a [u32],
50}
51
52#[derive(Debug, Clone, Copy, Eq, PartialEq)]
54pub enum CompletionObservation {
55 Incomplete,
57 Complete,
59 Failed,
61}
62
63impl CompletionObservation {
64 const fn wire(self) -> u32 {
65 match self {
66 Self::Incomplete => 0,
67 Self::Complete => 1,
68 Self::Failed => 2,
69 }
70 }
71}
72
73#[derive(Debug, Clone, Copy, Eq, PartialEq)]
75pub enum CompletionResolution {
76 Incomplete,
78 Complete,
80 FailedPending,
82 FailedComplete,
84}
85
86#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
88pub enum ConsensusError {
89 #[error("distributed scheduler consensus topology has no participants")]
91 EmptyTopology,
92 #[error("distributed scheduler {0} exceeds u32")]
94 MetadataOverflow(&'static str),
95 #[error("distributed scheduler consensus failed: {0}")]
97 Transport(String),
98 #[error(
100 "distributed scheduler consensus returned {actual} words; expected {expected} for {participants} ranks"
101 )]
102 MalformedGather {
103 expected: usize,
105 actual: usize,
107 participants: usize,
109 },
110 #[error("{context} differs at rank {rank}")]
112 Mismatch {
113 context: &'static str,
115 rank: usize,
117 },
118 #[error("distributed completion header differs at rank {rank}")]
120 CompletionHeader {
121 rank: usize,
123 },
124 #[error("distributed completion identity differs at rank {rank}")]
126 CompletionIdentity {
127 rank: usize,
129 },
130 #[error("distributed completion status is invalid at rank {rank}")]
132 CompletionStatus {
133 rank: usize,
135 },
136 #[error("distributed completion output differs at rank {rank}")]
138 CompletionOutput {
139 rank: usize,
141 },
142}
143
144pub fn validate_schedule<T: ConsensusTransport>(
146 transport: &T,
147 plan: &[ScheduledWork<'_>],
148 drain_cycle: u64,
149 protocol: u64,
150) -> Result<(), ConsensusError> {
151 let mut words = vec![
152 u32::try_from(plan.len())
153 .map_err(|_| ConsensusError::MetadataOverflow("schedule length"))?,
154 drain_cycle as u32,
155 (drain_cycle >> 32) as u32,
156 protocol as u32,
157 (protocol >> 32) as u32,
158 ];
159 for work in plan {
160 push_u64(&mut words, work.id.request().value());
161 push_u64(&mut words, work.id.sequence());
162 words.push(
163 u32::try_from(work.descriptor.len())
164 .map_err(|_| ConsensusError::MetadataOverflow("work descriptor length"))?,
165 );
166 words.extend_from_slice(work.descriptor);
167 }
168 validate_equal_words(transport, &words, "distributed work descriptors")
169}
170
171pub fn validate_schedule_bounded<T: BoundedConsensusTransport>(
173 transport: &T,
174 plan: &[ScheduledWork<'_>],
175 drain_cycle: u64,
176 protocol: u64,
177 wait: BoundedCompletionWait,
178) -> Result<(), ConsensusError>
179where
180 <T::Completion as crate::Completion>::Error: std::fmt::Display,
181{
182 let mut words = vec![
183 u32::try_from(plan.len())
184 .map_err(|_| ConsensusError::MetadataOverflow("schedule length"))?,
185 drain_cycle as u32,
186 (drain_cycle >> 32) as u32,
187 protocol as u32,
188 (protocol >> 32) as u32,
189 ];
190 for work in plan {
191 push_u64(&mut words, work.id.request().value());
192 push_u64(&mut words, work.id.sequence());
193 words.push(
194 u32::try_from(work.descriptor.len())
195 .map_err(|_| ConsensusError::MetadataOverflow("work descriptor length"))?,
196 );
197 words.extend_from_slice(work.descriptor);
198 }
199 validate_equal_words_bounded(transport, &words, "distributed work descriptors", wait)
200}
201
202pub fn validate_disposition<T: ConsensusTransport>(
204 transport: &T,
205 protocol: u64,
206 request: RequestId,
207 cause: CancellationCause,
208) -> Result<(), ConsensusError> {
209 let mut words = vec![protocol as u32, (protocol >> 32) as u32];
210 push_u64(&mut words, request.value());
211 words.push(match cause {
212 CancellationCause::Explicit => 1,
213 CancellationCause::Deadline => 2,
214 });
215 validate_equal_words(transport, &words, "distributed cancellation disposition")
216}
217
218pub fn resolve_completions<T: ConsensusTransport>(
220 transport: &T,
221 protocol: u64,
222 local: &[(WorkId, CompletionObservation)],
223) -> Result<Vec<CompletionResolution>, ConsensusError> {
224 let participants = checked_participants(transport)?;
225 if participants == 1 {
226 return Ok(local
227 .iter()
228 .map(|(_, status)| match status {
229 CompletionObservation::Incomplete => CompletionResolution::Incomplete,
230 CompletionObservation::Complete => CompletionResolution::Complete,
231 CompletionObservation::Failed => CompletionResolution::FailedComplete,
232 })
233 .collect());
234 }
235
236 let mut words = vec![
237 protocol as u32,
238 (protocol >> 32) as u32,
239 u32::try_from(local.len())
240 .map_err(|_| ConsensusError::MetadataOverflow("completion work count"))?,
241 ];
242 for (id, status) in local {
243 push_u64(&mut words, id.request().value());
244 push_u64(&mut words, id.sequence());
245 words.push(status.wire());
246 }
247 let gathered = gather_words(transport, &words, participants)?;
248 for rank in 0..participants {
249 let candidate = &gathered[rank * words.len()..(rank + 1) * words.len()];
250 if candidate[..3] != words[..3] {
251 return Err(ConsensusError::CompletionHeader { rank });
252 }
253 for (index, (id, _)) in local.iter().enumerate() {
254 let offset = 3 + index * 5;
255 let expected = [
256 id.request().value() as u32,
257 (id.request().value() >> 32) as u32,
258 id.sequence() as u32,
259 (id.sequence() >> 32) as u32,
260 ];
261 if candidate[offset..offset + 4] != expected {
262 return Err(ConsensusError::CompletionIdentity { rank });
263 }
264 if candidate[offset + 4] > CompletionObservation::Failed.wire() {
265 return Err(ConsensusError::CompletionStatus { rank });
266 }
267 }
268 }
269
270 Ok((0..local.len())
271 .map(|index| {
272 let statuses =
273 (0..participants).map(|rank| gathered[rank * words.len() + 3 + index * 5 + 4]);
274 let statuses = statuses.collect::<Vec<_>>();
275 let failed = statuses.contains(&CompletionObservation::Failed.wire());
276 let incomplete = statuses.contains(&CompletionObservation::Incomplete.wire());
277 match (failed, incomplete) {
278 (true, true) => CompletionResolution::FailedPending,
279 (true, false) => CompletionResolution::FailedComplete,
280 (false, true) => CompletionResolution::Incomplete,
281 (false, false) => CompletionResolution::Complete,
282 }
283 })
284 .collect())
285}
286
287pub fn resolve_completions_bounded<T: BoundedConsensusTransport>(
289 transport: &T,
290 protocol: u64,
291 local: &[(WorkId, CompletionObservation)],
292 wait: BoundedCompletionWait,
293) -> Result<Vec<CompletionResolution>, ConsensusError>
294where
295 <T::Completion as crate::Completion>::Error: std::fmt::Display,
296{
297 let participants = checked_participants(transport)?;
298 if participants == 1 {
299 return Ok(local
300 .iter()
301 .map(|(_, status)| match status {
302 CompletionObservation::Incomplete => CompletionResolution::Incomplete,
303 CompletionObservation::Complete => CompletionResolution::Complete,
304 CompletionObservation::Failed => CompletionResolution::FailedComplete,
305 })
306 .collect());
307 }
308
309 let mut words = vec![
310 protocol as u32,
311 (protocol >> 32) as u32,
312 u32::try_from(local.len())
313 .map_err(|_| ConsensusError::MetadataOverflow("completion work count"))?,
314 ];
315 for (id, status) in local {
316 push_u64(&mut words, id.request().value());
317 push_u64(&mut words, id.sequence());
318 words.push(status.wire());
319 }
320 let gathered = gather_words_bounded(transport, &words, participants, wait)?;
321 resolve_gathered_completions(&words, &gathered, participants, local)
322}
323
324pub fn resolve_output_completions_bounded<T: BoundedConsensusTransport>(
326 transport: &T,
327 protocol: u64,
328 local: &[(WorkId, CompletionObservation, [u32; 8])],
329 wait: BoundedCompletionWait,
330) -> Result<Vec<CompletionResolution>, ConsensusError>
331where
332 <T::Completion as crate::Completion>::Error: std::fmt::Display,
333{
334 let participants = checked_participants(transport)?;
335 if participants == 1 {
336 return Ok(local
337 .iter()
338 .map(|(_, status, _)| match status {
339 CompletionObservation::Incomplete => CompletionResolution::Incomplete,
340 CompletionObservation::Complete => CompletionResolution::Complete,
341 CompletionObservation::Failed => CompletionResolution::FailedComplete,
342 })
343 .collect());
344 }
345
346 let mut words = vec![
347 protocol as u32,
348 (protocol >> 32) as u32,
349 u32::try_from(local.len())
350 .map_err(|_| ConsensusError::MetadataOverflow("completion work count"))?,
351 ];
352 for (id, status, output) in local {
353 push_u64(&mut words, id.request().value());
354 push_u64(&mut words, id.sequence());
355 words.push(status.wire());
356 words.extend_from_slice(output);
357 }
358 let gathered = gather_words_bounded(transport, &words, participants, wait)?;
359 let stride = 13;
360 for rank in 0..participants {
361 let candidate = &gathered[rank * words.len()..(rank + 1) * words.len()];
362 if candidate[..3] != words[..3] {
363 return Err(ConsensusError::CompletionHeader { rank });
364 }
365 for (index, (id, _, _)) in local.iter().enumerate() {
366 let offset = 3 + index * stride;
367 let expected = [
368 id.request().value() as u32,
369 (id.request().value() >> 32) as u32,
370 id.sequence() as u32,
371 (id.sequence() >> 32) as u32,
372 ];
373 if candidate[offset..offset + 4] != expected {
374 return Err(ConsensusError::CompletionIdentity { rank });
375 }
376 if candidate[offset + 4] > CompletionObservation::Failed.wire() {
377 return Err(ConsensusError::CompletionStatus { rank });
378 }
379 }
380 }
381
382 (0..local.len())
383 .map(|index| {
384 let offset = 3 + index * stride;
385 let statuses = (0..participants)
386 .map(|rank| gathered[rank * words.len() + offset + 4])
387 .collect::<Vec<_>>();
388 let failed = statuses.contains(&CompletionObservation::Failed.wire());
389 let incomplete = statuses.contains(&CompletionObservation::Incomplete.wire());
390 if !failed && !incomplete {
391 let expected = &gathered[offset + 5..offset + stride];
392 for rank in 1..participants {
393 let start = rank * words.len() + offset + 5;
394 if &gathered[start..start + 8] != expected {
395 return Err(ConsensusError::CompletionOutput { rank });
396 }
397 }
398 }
399 Ok(match (failed, incomplete) {
400 (true, true) => CompletionResolution::FailedPending,
401 (true, false) => CompletionResolution::FailedComplete,
402 (false, true) => CompletionResolution::Incomplete,
403 (false, false) => CompletionResolution::Complete,
404 })
405 })
406 .collect()
407}
408
409#[allow(clippy::too_many_arguments)]
411pub fn agree_submission_status_bounded<T: BoundedConsensusTransport>(
412 transport: &T,
413 protocol: u64,
414 drain_cycle: u64,
415 expected: usize,
416 locally_submitted: usize,
417 local_success: bool,
418 wait: BoundedCompletionWait,
419) -> Result<bool, ConsensusError>
420where
421 <T::Completion as crate::Completion>::Error: std::fmt::Display,
422{
423 let participants = checked_participants(transport)?;
424 let words = vec![
425 protocol as u32,
426 (protocol >> 32) as u32,
427 drain_cycle as u32,
428 (drain_cycle >> 32) as u32,
429 u32::try_from(expected)
430 .map_err(|_| ConsensusError::MetadataOverflow("expected submission count"))?,
431 u32::try_from(locally_submitted)
432 .map_err(|_| ConsensusError::MetadataOverflow("local submission count"))?,
433 u32::from(local_success),
434 ];
435 let gathered = gather_words_bounded(transport, &words, participants, wait)?;
436 let semantic = &words[..5];
437 let mut all_submitted = true;
438 for rank in 0..participants {
439 let frame = &gathered[rank * words.len()..(rank + 1) * words.len()];
440 if &frame[..semantic.len()] != semantic {
441 return Err(ConsensusError::Mismatch {
442 context: "distributed submission transaction",
443 rank,
444 });
445 }
446 let submitted = usize::try_from(frame[5])
447 .map_err(|_| ConsensusError::MetadataOverflow("remote submission count"))?;
448 match frame[6] {
449 0 => all_submitted = false,
450 1 if submitted == expected => {}
451 1 => all_submitted = false,
452 _ => {
453 return Err(ConsensusError::Mismatch {
454 context: "distributed submission status",
455 rank,
456 })
457 }
458 }
459 }
460 Ok(all_submitted)
461}
462
463pub fn validate_ranked_identity_bounded<T: BoundedConsensusTransport>(
465 transport: &T,
466 protocol: u64,
467 identity: &[u32; 8],
468 local_rank: usize,
469 wait: BoundedCompletionWait,
470) -> Result<(), ConsensusError>
471where
472 <T::Completion as crate::Completion>::Error: std::fmt::Display,
473{
474 let participants = checked_participants(transport)?;
475 let mut words = vec![protocol as u32, (protocol >> 32) as u32];
476 words.extend_from_slice(identity);
477 words.push(
478 u32::try_from(local_rank)
479 .map_err(|_| ConsensusError::MetadataOverflow("distributed model rank"))?,
480 );
481 let gathered = gather_words_bounded(transport, &words, participants, wait)?;
482 let common = &words[..words.len() - 1];
483 for rank in 0..participants {
484 let frame = &gathered[rank * words.len()..(rank + 1) * words.len()];
485 if &frame[..common.len()] != common {
486 return Err(ConsensusError::Mismatch {
487 context: "distributed model identity",
488 rank,
489 });
490 }
491 if usize::try_from(frame[common.len()]).ok() != Some(rank) {
492 return Err(ConsensusError::Mismatch {
493 context: "distributed model rank ordering",
494 rank,
495 });
496 }
497 }
498 Ok(())
499}
500
501fn resolve_gathered_completions(
502 words: &[u32],
503 gathered: &[u32],
504 participants: usize,
505 local: &[(WorkId, CompletionObservation)],
506) -> Result<Vec<CompletionResolution>, ConsensusError> {
507 for rank in 0..participants {
508 let candidate = &gathered[rank * words.len()..(rank + 1) * words.len()];
509 if candidate[..3] != words[..3] {
510 return Err(ConsensusError::CompletionHeader { rank });
511 }
512 for (index, (id, _)) in local.iter().enumerate() {
513 let offset = 3 + index * 5;
514 let expected = [
515 id.request().value() as u32,
516 (id.request().value() >> 32) as u32,
517 id.sequence() as u32,
518 (id.sequence() >> 32) as u32,
519 ];
520 if candidate[offset..offset + 4] != expected {
521 return Err(ConsensusError::CompletionIdentity { rank });
522 }
523 if candidate[offset + 4] > CompletionObservation::Failed.wire() {
524 return Err(ConsensusError::CompletionStatus { rank });
525 }
526 }
527 }
528
529 Ok((0..local.len())
530 .map(|index| {
531 let statuses = (0..participants)
532 .map(|rank| gathered[rank * words.len() + 3 + index * 5 + 4])
533 .collect::<Vec<_>>();
534 let failed = statuses.contains(&CompletionObservation::Failed.wire());
535 let incomplete = statuses.contains(&CompletionObservation::Incomplete.wire());
536 match (failed, incomplete) {
537 (true, true) => CompletionResolution::FailedPending,
538 (true, false) => CompletionResolution::FailedComplete,
539 (false, true) => CompletionResolution::Incomplete,
540 (false, false) => CompletionResolution::Complete,
541 }
542 })
543 .collect())
544}
545
546fn validate_equal_words<T: ConsensusTransport>(
547 transport: &T,
548 words: &[u32],
549 context: &'static str,
550) -> Result<(), ConsensusError> {
551 let participants = checked_participants(transport)?;
552 if participants == 1 {
553 return Ok(());
554 }
555 let gathered = gather_words(transport, words, participants)?;
556 for rank in 0..participants {
557 let start = rank * words.len();
558 let end = start + words.len();
559 if gathered.get(start..end) != Some(words) {
560 return Err(ConsensusError::Mismatch { context, rank });
561 }
562 }
563 Ok(())
564}
565
566fn validate_equal_words_bounded<T: BoundedConsensusTransport>(
567 transport: &T,
568 words: &[u32],
569 context: &'static str,
570 wait: BoundedCompletionWait,
571) -> Result<(), ConsensusError>
572where
573 <T::Completion as crate::Completion>::Error: std::fmt::Display,
574{
575 let participants = checked_participants(transport)?;
576 if participants == 1 {
577 return Ok(());
578 }
579 let gathered = gather_words_bounded(transport, words, participants, wait)?;
580 for rank in 0..participants {
581 let start = rank * words.len();
582 let end = start + words.len();
583 if gathered.get(start..end) != Some(words) {
584 return Err(ConsensusError::Mismatch { context, rank });
585 }
586 }
587 Ok(())
588}
589
590fn checked_participants<T: ConsensusTransport>(transport: &T) -> Result<usize, ConsensusError> {
591 let participants = transport.participant_count();
592 if participants == 0 {
593 Err(ConsensusError::EmptyTopology)
594 } else {
595 Ok(participants)
596 }
597}
598
599fn gather_words<T: ConsensusTransport>(
600 transport: &T,
601 words: &[u32],
602 participants: usize,
603) -> Result<Vec<u32>, ConsensusError> {
604 let expected = words
605 .len()
606 .checked_mul(participants)
607 .ok_or(ConsensusError::MetadataOverflow("gathered word count"))?;
608 let gathered = transport
609 .all_gather_words(words)
610 .map_err(|error| ConsensusError::Transport(error.to_string()))?;
611 if gathered.len() != expected {
612 return Err(ConsensusError::MalformedGather {
613 expected,
614 actual: gathered.len(),
615 participants,
616 });
617 }
618 Ok(gathered)
619}
620
621fn gather_words_bounded<T: BoundedConsensusTransport>(
622 transport: &T,
623 words: &[u32],
624 participants: usize,
625 wait: BoundedCompletionWait,
626) -> Result<Vec<u32>, ConsensusError>
627where
628 <T::Completion as crate::Completion>::Error: std::fmt::Display,
629{
630 let expected = words
631 .len()
632 .checked_mul(participants)
633 .ok_or(ConsensusError::MetadataOverflow("gathered word count"))?;
634 let gathered = transport
635 .submit_all_gather_words(words)
636 .map_err(|error| ConsensusError::Transport(error.to_string()))?
637 .wait_bounded(wait)
638 .map_err(|error| ConsensusError::Transport(error.to_string()))?;
639 let gathered = match gathered {
640 BoundedSubmissionOutcome::Completed(gathered) => transport
641 .resolve_all_gather_words(gathered)
642 .map_err(|error| ConsensusError::Transport(error.to_string()))?,
643 BoundedSubmissionOutcome::DeadlineExceeded { cancellation } => {
644 return Err(ConsensusError::Transport(format!(
645 "bounded consensus deadline exceeded ({cancellation:?})"
646 )))
647 }
648 };
649 if gathered.len() != expected {
650 return Err(ConsensusError::MalformedGather {
651 expected,
652 actual: gathered.len(),
653 participants,
654 });
655 }
656 Ok(gathered)
657}
658
659pub fn agree_disposition_status_bounded<T: BoundedConsensusTransport>(
661 transport: &T,
662 protocol: u64,
663 request: RequestId,
664 cause: CancellationCause,
665 phase: u32,
666 local_ready: bool,
667 wait: BoundedCompletionWait,
668) -> Result<bool, ConsensusError>
669where
670 <T::Completion as crate::Completion>::Error: std::fmt::Display,
671{
672 let participants = checked_participants(transport)?;
673 let mut words = vec![protocol as u32, (protocol >> 32) as u32];
674 push_u64(&mut words, request.value());
675 words.push(match cause {
676 CancellationCause::Explicit => 1,
677 CancellationCause::Deadline => 2,
678 });
679 words.push(phase);
680 words.push(u32::from(local_ready));
681 let gathered = gather_words_bounded(transport, &words, participants, wait)?;
682 let semantic = &words[..words.len() - 1];
683 let mut all_ready = true;
684 for rank in 0..participants {
685 let frame = &gathered[rank * words.len()..(rank + 1) * words.len()];
686 if &frame[..semantic.len()] != semantic {
687 return Err(ConsensusError::Mismatch {
688 context: "distributed cancellation transaction",
689 rank,
690 });
691 }
692 match frame[semantic.len()] {
693 0 => all_ready = false,
694 1 => {}
695 _ => {
696 return Err(ConsensusError::Mismatch {
697 context: "distributed cancellation readiness",
698 rank,
699 })
700 }
701 }
702 }
703 Ok(all_ready)
704}
705
706pub fn agree_deadline_candidates_bounded<T: BoundedConsensusTransport>(
709 transport: &T,
710 protocol: u64,
711 local: &[(RequestId, bool)],
712 max_requests: usize,
713 wait: BoundedCompletionWait,
714) -> Result<Vec<RequestId>, ConsensusError>
715where
716 <T::Completion as crate::Completion>::Error: std::fmt::Display,
717{
718 let participants = checked_participants(transport)?;
719 if local.len() > max_requests {
720 return Err(ConsensusError::MetadataOverflow(
721 "active deadline request count",
722 ));
723 }
724 let count = u32::try_from(local.len())
725 .map_err(|_| ConsensusError::MetadataOverflow("active deadline request count"))?;
726 let slots = u32::try_from(max_requests)
727 .map_err(|_| ConsensusError::MetadataOverflow("deadline request slots"))?;
728 let mut words = vec![protocol as u32, (protocol >> 32) as u32, count, slots];
729 let mut previous = None;
730 for &(request, expired) in local {
731 if previous.is_some_and(|previous| previous >= request) {
732 return Err(ConsensusError::Mismatch {
733 context: "local deadline request ordering",
734 rank: 0,
735 });
736 }
737 previous = Some(request);
738 push_u64(&mut words, request.value());
739 words.push(u32::from(expired));
740 }
741 words.resize(4 + max_requests.saturating_mul(3), 0);
742 let gathered = gather_words_bounded(transport, &words, participants, wait)?;
743 let mut expired = vec![false; local.len()];
744 for rank in 0..participants {
745 let frame = &gathered[rank * words.len()..(rank + 1) * words.len()];
746 if frame[..4] != words[..4] {
747 return Err(ConsensusError::Mismatch {
748 context: "distributed deadline request set header",
749 rank,
750 });
751 }
752 for (index, &(request, _)) in local.iter().enumerate() {
753 let offset = 4 + index * 3;
754 let expected = [request.value() as u32, (request.value() >> 32) as u32];
755 if frame[offset..offset + 2] != expected {
756 return Err(ConsensusError::Mismatch {
757 context: "distributed deadline request identity",
758 rank,
759 });
760 }
761 match frame[offset + 2] {
762 0 => {}
763 1 => expired[index] = true,
764 _ => {
765 return Err(ConsensusError::Mismatch {
766 context: "distributed deadline request status",
767 rank,
768 })
769 }
770 }
771 }
772 if frame[4 + local.len() * 3..].iter().any(|word| *word != 0) {
773 return Err(ConsensusError::Mismatch {
774 context: "distributed deadline request padding",
775 rank,
776 });
777 }
778 }
779 Ok(local
780 .iter()
781 .zip(expired)
782 .filter_map(|(&(request, _), expired)| expired.then_some(request))
783 .collect())
784}
785
786fn push_u64(output: &mut Vec<u32>, value: u64) {
787 output.extend_from_slice(&[value as u32, (value >> 32) as u32]);
788}
789
790#[cfg(test)]
791mod tests {
792 use super::*;
793 use std::{cell::RefCell, convert::Infallible};
794
795 type GatherMutation = dyn FnMut(&mut [u32], usize);
796
797 struct MockTransport {
798 participants: usize,
799 mutate: RefCell<Option<Box<GatherMutation>>>,
800 }
801
802 impl MockTransport {
803 fn agreeing(participants: usize) -> Self {
804 Self {
805 participants,
806 mutate: RefCell::new(None),
807 }
808 }
809
810 fn mutating(participants: usize, mutate: impl FnMut(&mut [u32], usize) + 'static) -> Self {
811 Self {
812 participants,
813 mutate: RefCell::new(Some(Box::new(mutate))),
814 }
815 }
816 }
817
818 impl ConsensusTransport for MockTransport {
819 type Error = Infallible;
820
821 fn participant_count(&self) -> usize {
822 self.participants
823 }
824
825 fn all_gather_words(&self, local: &[u32]) -> Result<Vec<u32>, Self::Error> {
826 let mut gathered = Vec::with_capacity(local.len() * self.participants);
827 for rank in 0..self.participants {
828 let start = gathered.len();
829 gathered.extend_from_slice(local);
830 if let Some(mutate) = self.mutate.borrow_mut().as_mut() {
831 mutate(&mut gathered[start..], rank);
832 }
833 }
834 Ok(gathered)
835 }
836 }
837
838 struct DeadlineTransport;
839
840 impl ConsensusTransport for DeadlineTransport {
841 type Error = Infallible;
842
843 fn participant_count(&self) -> usize {
844 2
845 }
846
847 fn all_gather_words(&self, _local: &[u32]) -> Result<Vec<u32>, Self::Error> {
848 panic!("bounded consensus called the unbounded transport path")
849 }
850 }
851
852 struct DeadlineCompletion;
853
854 impl crate::Completion for DeadlineCompletion {
855 type Error = Infallible;
856
857 fn is_complete(&self) -> Result<bool, Self::Error> {
858 Ok(false)
859 }
860
861 fn wait(&self) -> Result<(), Self::Error> {
862 panic!("bounded consensus called an unbounded completion wait")
863 }
864 }
865
866 impl crate::BoundedCompletion for DeadlineCompletion {
867 fn wait_bounded(
868 self,
869 wait: BoundedCompletionWait,
870 ) -> Result<crate::BoundedCompletionOutcome, Self::Error> {
871 Ok(crate::BoundedCompletionOutcome::DeadlineExceeded {
872 cancellation: wait.cancellation(),
873 })
874 }
875 }
876
877 impl BoundedConsensusTransport for DeadlineTransport {
878 type Completion = DeadlineCompletion;
879 type GatherOutput = Vec<u32>;
880
881 fn submit_all_gather_words(
882 &self,
883 local: &[u32],
884 ) -> Result<Submission<Self::GatherOutput, Self::Completion>, Self::Error> {
885 Ok(Submission {
886 output: local.to_vec(),
887 completion: DeadlineCompletion,
888 })
889 }
890
891 fn resolve_all_gather_words(
892 &self,
893 _output: Self::GatherOutput,
894 ) -> Result<Vec<u32>, Self::Error> {
895 panic!("a timed-out bounded gather cannot be resolved")
896 }
897 }
898
899 #[test]
900 fn schedule_and_disposition_agree_without_backend_types() {
901 let transport = MockTransport::agreeing(3);
902 let descriptor = [7, 8, 9];
903 let work = [ScheduledWork {
904 id: WorkId::new(RequestId::new(5), 2),
905 descriptor: &descriptor,
906 }];
907 validate_schedule(&transport, &work, 11, 13).unwrap();
908 validate_disposition(
909 &transport,
910 13,
911 RequestId::new(5),
912 CancellationCause::Explicit,
913 )
914 .unwrap();
915 }
916
917 #[test]
918 fn schedule_mismatch_fails_closed() {
919 let transport = MockTransport::mutating(2, |words, rank| {
920 if rank == 1 {
921 *words.last_mut().unwrap() ^= 1;
922 }
923 });
924 let descriptor = [7];
925 let error = validate_schedule(
926 &transport,
927 &[ScheduledWork {
928 id: WorkId::new(RequestId::new(1), 0),
929 descriptor: &descriptor,
930 }],
931 0,
932 9,
933 )
934 .unwrap_err();
935 assert_eq!(
936 error,
937 ConsensusError::Mismatch {
938 context: "distributed work descriptors",
939 rank: 1,
940 }
941 );
942 }
943
944 #[test]
945 fn completion_resolution_waits_for_failed_rank_peers() {
946 let call = RefCell::new(0usize);
947 let transport = MockTransport::mutating(3, move |words, rank| {
948 if rank == 1 {
949 words[7] = CompletionObservation::Failed.wire();
950 } else if rank == 2 {
951 words[7] = CompletionObservation::Incomplete.wire();
952 }
953 *call.borrow_mut() += 1;
954 });
955 let resolutions = resolve_completions(
956 &transport,
957 22,
958 &[(
959 WorkId::new(RequestId::new(4), 3),
960 CompletionObservation::Complete,
961 )],
962 )
963 .unwrap();
964 assert_eq!(resolutions, vec![CompletionResolution::FailedPending]);
965 }
966
967 #[test]
968 fn malformed_gather_and_empty_topology_fail_closed() {
969 let empty = MockTransport::agreeing(0);
970 assert_eq!(
971 validate_disposition(&empty, 1, RequestId::new(1), CancellationCause::Deadline,)
972 .unwrap_err(),
973 ConsensusError::EmptyTopology
974 );
975
976 struct ShortGather;
977 impl ConsensusTransport for ShortGather {
978 type Error = Infallible;
979 fn participant_count(&self) -> usize {
980 2
981 }
982 fn all_gather_words(&self, local: &[u32]) -> Result<Vec<u32>, Self::Error> {
983 Ok(local.to_vec())
984 }
985 }
986 assert!(matches!(
987 validate_disposition(
988 &ShortGather,
989 1,
990 RequestId::new(1),
991 CancellationCause::Explicit,
992 ),
993 Err(ConsensusError::MalformedGather { .. })
994 ));
995 }
996
997 #[test]
998 fn bounded_schedule_and_completion_consensus_timeout_without_unbounded_waits() {
999 let wait = BoundedCompletionWait::new(
1000 std::time::Duration::from_millis(1),
1001 crate::CompletionCancellationMode::QuarantineUntilComplete,
1002 )
1003 .unwrap();
1004 let work = WorkId::new(RequestId::new(4), 3);
1005 let schedule = [ScheduledWork {
1006 id: work,
1007 descriptor: &[7],
1008 }];
1009 for result in [
1010 validate_schedule_bounded(&DeadlineTransport, &schedule, 0, 22, wait),
1011 resolve_completions_bounded(
1012 &DeadlineTransport,
1013 22,
1014 &[(work, CompletionObservation::Incomplete)],
1015 wait,
1016 )
1017 .map(|_| ()),
1018 ] {
1019 assert!(
1020 matches!(result, Err(ConsensusError::Transport(message)) if message.contains("deadline exceeded"))
1021 );
1022 }
1023 }
1024}