1use crate::scheduler::{CancellationCause, RequestId, WorkId};
7
8pub trait ConsensusTransport {
14 type Error: std::error::Error;
16
17 fn participant_count(&self) -> usize;
19
20 fn all_gather_words(&self, local: &[u32]) -> Result<Vec<u32>, Self::Error>;
22}
23
24#[derive(Debug, Clone, Copy, Eq, PartialEq)]
26pub struct ScheduledWork<'a> {
27 pub id: WorkId,
29 pub descriptor: &'a [u32],
31}
32
33#[derive(Debug, Clone, Copy, Eq, PartialEq)]
35pub enum CompletionObservation {
36 Incomplete,
38 Complete,
40 Failed,
42}
43
44impl CompletionObservation {
45 const fn wire(self) -> u32 {
46 match self {
47 Self::Incomplete => 0,
48 Self::Complete => 1,
49 Self::Failed => 2,
50 }
51 }
52}
53
54#[derive(Debug, Clone, Copy, Eq, PartialEq)]
56pub enum CompletionResolution {
57 Incomplete,
59 Complete,
61 FailedPending,
63 FailedComplete,
65}
66
67#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
69pub enum ConsensusError {
70 #[error("distributed scheduler consensus topology has no participants")]
72 EmptyTopology,
73 #[error("distributed scheduler {0} exceeds u32")]
75 MetadataOverflow(&'static str),
76 #[error("distributed scheduler consensus failed: {0}")]
78 Transport(String),
79 #[error(
81 "distributed scheduler consensus returned {actual} words; expected {expected} for {participants} ranks"
82 )]
83 MalformedGather {
84 expected: usize,
86 actual: usize,
88 participants: usize,
90 },
91 #[error("{context} differs at rank {rank}")]
93 Mismatch {
94 context: &'static str,
96 rank: usize,
98 },
99 #[error("distributed completion header differs at rank {rank}")]
101 CompletionHeader {
102 rank: usize,
104 },
105 #[error("distributed completion identity differs at rank {rank}")]
107 CompletionIdentity {
108 rank: usize,
110 },
111 #[error("distributed completion status is invalid at rank {rank}")]
113 CompletionStatus {
114 rank: usize,
116 },
117}
118
119pub fn validate_schedule<T: ConsensusTransport>(
121 transport: &T,
122 plan: &[ScheduledWork<'_>],
123 drain_cycle: u64,
124 protocol: u64,
125) -> Result<(), ConsensusError> {
126 let mut words = vec![
127 u32::try_from(plan.len())
128 .map_err(|_| ConsensusError::MetadataOverflow("schedule length"))?,
129 drain_cycle as u32,
130 (drain_cycle >> 32) as u32,
131 protocol as u32,
132 (protocol >> 32) as u32,
133 ];
134 for work in plan {
135 push_u64(&mut words, work.id.request().value());
136 push_u64(&mut words, work.id.sequence());
137 words.push(
138 u32::try_from(work.descriptor.len())
139 .map_err(|_| ConsensusError::MetadataOverflow("work descriptor length"))?,
140 );
141 words.extend_from_slice(work.descriptor);
142 }
143 validate_equal_words(transport, &words, "distributed work descriptors")
144}
145
146pub fn validate_disposition<T: ConsensusTransport>(
148 transport: &T,
149 protocol: u64,
150 request: RequestId,
151 cause: CancellationCause,
152) -> Result<(), ConsensusError> {
153 let mut words = vec![protocol as u32, (protocol >> 32) as u32];
154 push_u64(&mut words, request.value());
155 words.push(match cause {
156 CancellationCause::Explicit => 1,
157 CancellationCause::Deadline => 2,
158 });
159 validate_equal_words(transport, &words, "distributed cancellation disposition")
160}
161
162pub fn resolve_completions<T: ConsensusTransport>(
164 transport: &T,
165 protocol: u64,
166 local: &[(WorkId, CompletionObservation)],
167) -> Result<Vec<CompletionResolution>, ConsensusError> {
168 let participants = checked_participants(transport)?;
169 if participants == 1 {
170 return Ok(local
171 .iter()
172 .map(|(_, status)| match status {
173 CompletionObservation::Incomplete => CompletionResolution::Incomplete,
174 CompletionObservation::Complete => CompletionResolution::Complete,
175 CompletionObservation::Failed => CompletionResolution::FailedComplete,
176 })
177 .collect());
178 }
179
180 let mut words = vec![
181 protocol as u32,
182 (protocol >> 32) as u32,
183 u32::try_from(local.len())
184 .map_err(|_| ConsensusError::MetadataOverflow("completion work count"))?,
185 ];
186 for (id, status) in local {
187 push_u64(&mut words, id.request().value());
188 push_u64(&mut words, id.sequence());
189 words.push(status.wire());
190 }
191 let gathered = gather_words(transport, &words, participants)?;
192 for rank in 0..participants {
193 let candidate = &gathered[rank * words.len()..(rank + 1) * words.len()];
194 if candidate[..3] != words[..3] {
195 return Err(ConsensusError::CompletionHeader { rank });
196 }
197 for (index, (id, _)) in local.iter().enumerate() {
198 let offset = 3 + index * 5;
199 let expected = [
200 id.request().value() as u32,
201 (id.request().value() >> 32) as u32,
202 id.sequence() as u32,
203 (id.sequence() >> 32) as u32,
204 ];
205 if candidate[offset..offset + 4] != expected {
206 return Err(ConsensusError::CompletionIdentity { rank });
207 }
208 if candidate[offset + 4] > CompletionObservation::Failed.wire() {
209 return Err(ConsensusError::CompletionStatus { rank });
210 }
211 }
212 }
213
214 Ok((0..local.len())
215 .map(|index| {
216 let statuses =
217 (0..participants).map(|rank| gathered[rank * words.len() + 3 + index * 5 + 4]);
218 let statuses = statuses.collect::<Vec<_>>();
219 let failed = statuses.contains(&CompletionObservation::Failed.wire());
220 let incomplete = statuses.contains(&CompletionObservation::Incomplete.wire());
221 match (failed, incomplete) {
222 (true, true) => CompletionResolution::FailedPending,
223 (true, false) => CompletionResolution::FailedComplete,
224 (false, true) => CompletionResolution::Incomplete,
225 (false, false) => CompletionResolution::Complete,
226 }
227 })
228 .collect())
229}
230
231fn validate_equal_words<T: ConsensusTransport>(
232 transport: &T,
233 words: &[u32],
234 context: &'static str,
235) -> Result<(), ConsensusError> {
236 let participants = checked_participants(transport)?;
237 if participants == 1 {
238 return Ok(());
239 }
240 let gathered = gather_words(transport, words, participants)?;
241 for rank in 0..participants {
242 let start = rank * words.len();
243 let end = start + words.len();
244 if gathered.get(start..end) != Some(words) {
245 return Err(ConsensusError::Mismatch { context, rank });
246 }
247 }
248 Ok(())
249}
250
251fn checked_participants<T: ConsensusTransport>(transport: &T) -> Result<usize, ConsensusError> {
252 let participants = transport.participant_count();
253 if participants == 0 {
254 Err(ConsensusError::EmptyTopology)
255 } else {
256 Ok(participants)
257 }
258}
259
260fn gather_words<T: ConsensusTransport>(
261 transport: &T,
262 words: &[u32],
263 participants: usize,
264) -> Result<Vec<u32>, ConsensusError> {
265 let expected = words
266 .len()
267 .checked_mul(participants)
268 .ok_or(ConsensusError::MetadataOverflow("gathered word count"))?;
269 let gathered = transport
270 .all_gather_words(words)
271 .map_err(|error| ConsensusError::Transport(error.to_string()))?;
272 if gathered.len() != expected {
273 return Err(ConsensusError::MalformedGather {
274 expected,
275 actual: gathered.len(),
276 participants,
277 });
278 }
279 Ok(gathered)
280}
281
282fn push_u64(output: &mut Vec<u32>, value: u64) {
283 output.extend_from_slice(&[value as u32, (value >> 32) as u32]);
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289 use std::{cell::RefCell, convert::Infallible};
290
291 type GatherMutation = dyn FnMut(&mut [u32], usize);
292
293 struct MockTransport {
294 participants: usize,
295 mutate: RefCell<Option<Box<GatherMutation>>>,
296 }
297
298 impl MockTransport {
299 fn agreeing(participants: usize) -> Self {
300 Self {
301 participants,
302 mutate: RefCell::new(None),
303 }
304 }
305
306 fn mutating(participants: usize, mutate: impl FnMut(&mut [u32], usize) + 'static) -> Self {
307 Self {
308 participants,
309 mutate: RefCell::new(Some(Box::new(mutate))),
310 }
311 }
312 }
313
314 impl ConsensusTransport for MockTransport {
315 type Error = Infallible;
316
317 fn participant_count(&self) -> usize {
318 self.participants
319 }
320
321 fn all_gather_words(&self, local: &[u32]) -> Result<Vec<u32>, Self::Error> {
322 let mut gathered = Vec::with_capacity(local.len() * self.participants);
323 for rank in 0..self.participants {
324 let start = gathered.len();
325 gathered.extend_from_slice(local);
326 if let Some(mutate) = self.mutate.borrow_mut().as_mut() {
327 mutate(&mut gathered[start..], rank);
328 }
329 }
330 Ok(gathered)
331 }
332 }
333
334 #[test]
335 fn schedule_and_disposition_agree_without_backend_types() {
336 let transport = MockTransport::agreeing(3);
337 let descriptor = [7, 8, 9];
338 let work = [ScheduledWork {
339 id: WorkId::new(RequestId::new(5), 2),
340 descriptor: &descriptor,
341 }];
342 validate_schedule(&transport, &work, 11, 13).unwrap();
343 validate_disposition(
344 &transport,
345 13,
346 RequestId::new(5),
347 CancellationCause::Explicit,
348 )
349 .unwrap();
350 }
351
352 #[test]
353 fn schedule_mismatch_fails_closed() {
354 let transport = MockTransport::mutating(2, |words, rank| {
355 if rank == 1 {
356 *words.last_mut().unwrap() ^= 1;
357 }
358 });
359 let descriptor = [7];
360 let error = validate_schedule(
361 &transport,
362 &[ScheduledWork {
363 id: WorkId::new(RequestId::new(1), 0),
364 descriptor: &descriptor,
365 }],
366 0,
367 9,
368 )
369 .unwrap_err();
370 assert_eq!(
371 error,
372 ConsensusError::Mismatch {
373 context: "distributed work descriptors",
374 rank: 1,
375 }
376 );
377 }
378
379 #[test]
380 fn completion_resolution_waits_for_failed_rank_peers() {
381 let call = RefCell::new(0usize);
382 let transport = MockTransport::mutating(3, move |words, rank| {
383 if rank == 1 {
384 words[7] = CompletionObservation::Failed.wire();
385 } else if rank == 2 {
386 words[7] = CompletionObservation::Incomplete.wire();
387 }
388 *call.borrow_mut() += 1;
389 });
390 let resolutions = resolve_completions(
391 &transport,
392 22,
393 &[(
394 WorkId::new(RequestId::new(4), 3),
395 CompletionObservation::Complete,
396 )],
397 )
398 .unwrap();
399 assert_eq!(resolutions, vec![CompletionResolution::FailedPending]);
400 }
401
402 #[test]
403 fn malformed_gather_and_empty_topology_fail_closed() {
404 let empty = MockTransport::agreeing(0);
405 assert_eq!(
406 validate_disposition(&empty, 1, RequestId::new(1), CancellationCause::Deadline,)
407 .unwrap_err(),
408 ConsensusError::EmptyTopology
409 );
410
411 struct ShortGather;
412 impl ConsensusTransport for ShortGather {
413 type Error = Infallible;
414 fn participant_count(&self) -> usize {
415 2
416 }
417 fn all_gather_words(&self, local: &[u32]) -> Result<Vec<u32>, Self::Error> {
418 Ok(local.to_vec())
419 }
420 }
421 assert!(matches!(
422 validate_disposition(
423 &ShortGather,
424 1,
425 RequestId::new(1),
426 CancellationCause::Explicit,
427 ),
428 Err(ConsensusError::MalformedGather { .. })
429 ));
430 }
431}