Skip to main content

eredu_core/
consensus.rs

1//! Backend-neutral distributed scheduler consensus.
2//!
3//! Core defines the wire records and validates rank agreement. A backend
4//! adapter supplies only a topology-scoped all-gather of portable words.
5
6use crate::scheduler::{CancellationCause, RequestId, WorkId};
7use crate::{BoundedCompletion, BoundedCompletionWait, BoundedSubmissionOutcome, Submission};
8
9/// Topology-scoped transport for scheduler metadata.
10///
11/// Implementations must return rank-major concatenation of one equally sized
12/// word frame from every participant. The scheduler never sends tensors,
13/// caches, streams, or executable objects through this interface.
14pub trait ConsensusTransport {
15    /// Transport error.
16    type Error: std::error::Error;
17
18    /// Number of ranks in the consensus topology.
19    fn participant_count(&self) -> usize;
20
21    /// Gathers an equally sized word frame from every rank in rank order.
22    fn all_gather_words(&self, local: &[u32]) -> Result<Vec<u32>, Self::Error>;
23}
24
25/// Consensus transport that returns exact work ownership under a caller-selected bound.
26pub trait BoundedConsensusTransport: ConsensusTransport {
27    /// Completion retaining transport resources through completion or safe cancellation.
28    type Completion: BoundedCompletion;
29    /// Backend-owned gathered value that remains lazy until exact completion.
30    type GatherOutput;
31
32    /// Submits one equal-word rank-major gather without synchronizing the caller.
33    fn submit_all_gather_words(
34        &self,
35        local: &[u32],
36    ) -> Result<Submission<Self::GatherOutput, Self::Completion>, Self::Error>;
37
38    /// Resolves completed backend output into rank-major portable words.
39    fn resolve_all_gather_words(&self, output: Self::GatherOutput)
40        -> Result<Vec<u32>, Self::Error>;
41}
42
43/// One planned transition and its stable semantic descriptor.
44#[derive(Debug, Clone, Copy, Eq, PartialEq)]
45pub struct ScheduledWork<'a> {
46    /// Scheduler transition identity.
47    pub id: WorkId,
48    /// Program-specific stable descriptor words.
49    pub descriptor: &'a [u32],
50}
51
52/// Exact local completion observation before rank consensus.
53#[derive(Debug, Clone, Copy, Eq, PartialEq)]
54pub enum CompletionObservation {
55    /// Local backend work is incomplete.
56    Incomplete,
57    /// Local backend work completed successfully.
58    Complete,
59    /// Exact local completion observation failed.
60    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/// Topology-wide resolution for one submitted transition.
74#[derive(Debug, Clone, Copy, Eq, PartialEq)]
75pub enum CompletionResolution {
76    /// At least one rank is still executing and no rank failed.
77    Incomplete,
78    /// Every rank completed successfully.
79    Complete,
80    /// At least one rank failed while another remains incomplete.
81    FailedPending,
82    /// At least one rank failed and every rank reached an exact terminal state.
83    FailedComplete,
84}
85
86/// Structured consensus validation failure without backend error types.
87#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
88pub enum ConsensusError {
89    /// A consensus topology cannot be empty.
90    #[error("distributed scheduler consensus topology has no participants")]
91    EmptyTopology,
92    /// Portable metadata exceeded its wire representation.
93    #[error("distributed scheduler {0} exceeds u32")]
94    MetadataOverflow(&'static str),
95    /// The backend collective failed.
96    #[error("distributed scheduler consensus failed: {0}")]
97    Transport(String),
98    /// The transport returned a malformed rank-major gather.
99    #[error(
100        "distributed scheduler consensus returned {actual} words; expected {expected} for {participants} ranks"
101    )]
102    MalformedGather {
103        /// Expected gathered word count.
104        expected: usize,
105        /// Actual gathered word count.
106        actual: usize,
107        /// Expected participant count.
108        participants: usize,
109    },
110    /// A schedule or disposition frame differed.
111    #[error("{context} differs at rank {rank}")]
112    Mismatch {
113        /// Operation being validated.
114        context: &'static str,
115        /// First disagreeing rank.
116        rank: usize,
117    },
118    /// Completion protocol, work count, or frame size differed.
119    #[error("distributed completion header differs at rank {rank}")]
120    CompletionHeader {
121        /// First disagreeing rank.
122        rank: usize,
123    },
124    /// Completion work ordering differed.
125    #[error("distributed completion identity differs at rank {rank}")]
126    CompletionIdentity {
127        /// First disagreeing rank.
128        rank: usize,
129    },
130    /// A rank emitted an unknown completion status.
131    #[error("distributed completion status is invalid at rank {rank}")]
132    CompletionStatus {
133        /// First rank with an invalid value.
134        rank: usize,
135    },
136    /// Completed output content differed across ranks.
137    #[error("distributed completion output differs at rank {rank}")]
138    CompletionOutput {
139        /// First rank whose completed output descriptor differs.
140        rank: usize,
141    },
142}
143
144/// Validates exact work ordering and descriptors across a topology.
145pub 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
171/// Validates exact work ordering and descriptors under the selected completion bound.
172pub 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
202/// Validates a cancellation or deadline disposition across a topology.
203pub 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
218/// Resolves exact local completion observations into topology-wide outcomes.
219pub 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
287/// Resolves exact local completion observations under the selected completion bound.
288pub 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
324/// Resolves completion observations and fixed-width output identities under a bound.
325pub 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/// Agrees whether every rank submitted the exact selected schedule under the bound.
410#[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
463/// Validates one fixed-width model identity and the expected rank ordering.
464pub 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
659/// Agrees one cancellation preparation or commit-authorization status under a bound.
660pub 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
706/// Agrees the exact active request set and returns every request whose deadline
707/// has expired on at least one rank.
708pub 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}