Skip to main content

aion_server/worker/
envelope.rs

1//! Activity task-envelope generation and completion fencing.
2
3use std::collections::HashMap;
4use std::collections::hash_map::Entry;
5use std::sync::{Arc, Mutex, MutexGuard};
6
7use aion_core::{ActivityId, RunId, WorkflowId};
8use sha2::{Digest, Sha256};
9use uuid::Uuid;
10
11use crate::error::{CompletionRejectionReason, ServerError};
12
13type ExecutionKey = (WorkflowId, ActivityId);
14
15/// Opaque proof that a worker owns one dispatched execution generation.
16#[derive(Clone, Debug, Eq, PartialEq)]
17pub struct CompletionToken(String);
18
19impl CompletionToken {
20    /// Parse a worker-echoed token without assigning meaning to its contents.
21    ///
22    /// # Errors
23    ///
24    /// Returns a typed compatibility refusal when a pre-fencing worker omits it.
25    pub fn from_wire(
26        workflow_id: &WorkflowId,
27        activity_id: &ActivityId,
28        value: String,
29    ) -> Result<Self, ServerError> {
30        if value.is_empty() {
31            return Err(rejection(
32                workflow_id,
33                activity_id,
34                CompletionRejectionReason::MissingCompletionToken,
35            ));
36        }
37        Ok(Self(value))
38    }
39
40    /// Return the opaque wire representation.
41    #[must_use]
42    pub fn as_str(&self) -> &str {
43        &self.0
44    }
45
46    /// Build a non-wire token for crate-local unit fixtures.
47    #[cfg(test)]
48    #[must_use]
49    pub(crate) fn for_test() -> Self {
50        Self("test-generation".to_owned())
51    }
52}
53
54/// Every completion token authorized for ONE attempt of ONE execution
55/// generation of one execution site.
56///
57/// The vector holds more than one token in exactly one situation: the SAME
58/// attempt was delivered more than once, so two live workers each genuinely
59/// hold a token minted for the same work. Delivery is at-least-once, so that
60/// situation is ordinary rather than exceptional. Both tokens are acceptable
61/// candidates; the FIRST accepted completion consumes the whole generation, so
62/// the second worker's completion is the duplicate and is refused.
63#[derive(Clone, Debug, Eq, PartialEq)]
64struct SiteGeneration {
65    /// Run-scoped external-effect key of the generation these tokens belong to,
66    /// and the execution-generation discriminator. Not a second discriminator
67    /// invented here: it is the same [`idempotency_key`] the wire already
68    /// carries to the worker, so a reset or continue-as-new — which mints a new
69    /// [`RunId`] — is a different generation by construction.
70    idempotency_key: String,
71    /// One-based delivery attempt these tokens were issued for. A higher
72    /// attempt supersedes; an equal attempt is a redelivery of the same work.
73    attempt: u32,
74    /// Tokens issued for `attempt` that have been neither consumed by an
75    /// accepted completion nor revoked by the dispatch that issued them.
76    outstanding: Vec<CompletionToken>,
77}
78
79/// The generation one accepted completion consumed, handed back so a settlement
80/// that fails afterwards can restore exactly what was taken.
81///
82/// Restoring needs the `idempotency_key` and `attempt` that were consumed;
83/// re-deriving either at the restore site would be inventing a second
84/// execution-generation discriminator, so the consumed generation travels out
85/// with the acceptance instead.
86#[derive(Clone, Debug, Eq, PartialEq)]
87pub struct AcceptedGeneration(SiteGeneration);
88
89impl AcceptedGeneration {
90    /// One-based delivery attempt whose completion was accepted.
91    #[must_use]
92    pub const fn attempt(&self) -> u32 {
93        self.0.attempt
94    }
95}
96
97/// Process-incarnation registry of the only generations allowed to complete.
98///
99/// Recovery deliberately starts with an empty registry. Re-dispatch issues a
100/// fresh token, so a result carrying any pre-recovery token is refused whether
101/// it arrives before or after the new dispatch.
102#[derive(Clone, Debug, Default)]
103pub struct CompletionFences {
104    current: Arc<Mutex<HashMap<ExecutionKey, SiteGeneration>>>,
105}
106
107impl CompletionFences {
108    /// Authorize one more delivery of one attempt and return its token.
109    ///
110    /// One generation is held per execution site, and what a call is decides
111    /// what happens to it:
112    ///
113    /// - a different `idempotency_key` is a different EXECUTION GENERATION
114    ///   (reset / continue-as-new): the prior generation is superseded whole;
115    /// - a higher `attempt` within the same generation is a genuine RETRY: the
116    ///   prior attempt's tokens are superseded;
117    /// - the SAME attempt within the same generation is a REDELIVERY of work a
118    ///   worker may still be executing, so the new token JOINS the outstanding
119    ///   set rather than replacing it. Both workers hold a token that can be
120    ///   accepted, and the first accepted completion consumes both.
121    ///
122    /// A re-dispatch of an attempt that has already been superseded registers
123    /// nothing: it is logged with both attempt numbers, and the token it returns
124    /// is refused as a stale generation when presented, which is the truthful
125    /// answer for a worker whose attempt no longer exists.
126    ///
127    /// # Errors
128    ///
129    /// Returns [`ServerError::LockPoisoned`] when fence state cannot be trusted.
130    pub fn issue(
131        &self,
132        workflow_id: &WorkflowId,
133        run_id: &RunId,
134        activity_id: &ActivityId,
135        attempt: u32,
136    ) -> Result<CompletionToken, ServerError> {
137        let token = CompletionToken(Uuid::new_v4().to_string());
138        let issued_key = idempotency_key(workflow_id, run_id, activity_id);
139        let mut state = self.state()?;
140        match state.entry((workflow_id.clone(), activity_id.clone())) {
141            Entry::Vacant(slot) => {
142                slot.insert(SiteGeneration {
143                    idempotency_key: issued_key,
144                    attempt,
145                    outstanding: vec![token.clone()],
146                });
147            }
148            Entry::Occupied(mut slot) => {
149                let current = slot.get_mut();
150                if current.idempotency_key != issued_key || attempt > current.attempt {
151                    *current = SiteGeneration {
152                        idempotency_key: issued_key,
153                        attempt,
154                        outstanding: vec![token.clone()],
155                    };
156                } else if attempt == current.attempt {
157                    current.outstanding.push(token.clone());
158                } else {
159                    tracing::warn!(
160                        workflow_id = %workflow_id,
161                        activity_id = %activity_id,
162                        superseded_attempt = attempt,
163                        current_attempt = current.attempt,
164                        "re-dispatch of an already superseded attempt registers no generation; \
165                         its completion will be refused as a stale generation"
166                    );
167                }
168            }
169        }
170        Ok(token)
171    }
172
173    /// Consume the current generation when `submitted` is one of the tokens it
174    /// still has outstanding.
175    ///
176    /// Comparison and consumption share one mutex critical section, so two
177    /// concurrent submissions cannot both become truth: the whole generation is
178    /// removed by the first, and every later presentation of any of its tokens
179    /// finds no generation at all.
180    ///
181    /// # Errors
182    ///
183    /// Returns a typed rejection for a missing generation or a token that is
184    /// not outstanding in it, or [`ServerError::LockPoisoned`] when fence state
185    /// cannot be trusted.
186    pub fn accept(
187        &self,
188        workflow_id: &WorkflowId,
189        activity_id: &ActivityId,
190        submitted: &CompletionToken,
191    ) -> Result<AcceptedGeneration, ServerError> {
192        let mut state = self.state()?;
193        let Entry::Occupied(slot) = state.entry((workflow_id.clone(), activity_id.clone())) else {
194            return Err(rejection(
195                workflow_id,
196                activity_id,
197                CompletionRejectionReason::NoCurrentGeneration,
198            ));
199        };
200        if !slot.get().outstanding.contains(submitted) {
201            return Err(rejection(
202                workflow_id,
203                activity_id,
204                CompletionRejectionReason::StaleGeneration,
205            ));
206        }
207        Ok(AcceptedGeneration(slot.remove()))
208    }
209
210    /// Restore a consumed generation after accepted-path settlement fails.
211    ///
212    /// A concurrently issued newer generation always wins; the accepted
213    /// generation is restored only while the execution site has no current one.
214    ///
215    /// # Errors
216    ///
217    /// Returns [`ServerError::LockPoisoned`] when fence state cannot be trusted.
218    pub fn restore_if_absent(
219        &self,
220        workflow_id: &WorkflowId,
221        activity_id: &ActivityId,
222        accepted: &AcceptedGeneration,
223    ) -> Result<(), ServerError> {
224        self.state()?
225            .entry((workflow_id.clone(), activity_id.clone()))
226            .or_insert_with(|| accepted.0.clone());
227        Ok(())
228    }
229
230    /// Revoke exactly the token this caller issued, and nothing else.
231    ///
232    /// A dispatch that could not place its task withdraws its OWN authorization.
233    /// It never withdraws a sibling token still held by a live worker executing
234    /// the same attempt, and it never disturbs a newer retry — a newer attempt
235    /// already replaced the outstanding set, so the old token is simply absent
236    /// and the removal is a no-op. The generation is dropped once its last
237    /// outstanding token is gone.
238    ///
239    /// # Errors
240    ///
241    /// Returns [`ServerError::LockPoisoned`] when fence state cannot be trusted.
242    pub fn revoke(
243        &self,
244        workflow_id: &WorkflowId,
245        activity_id: &ActivityId,
246        token: &CompletionToken,
247    ) -> Result<(), ServerError> {
248        let mut state = self.state()?;
249        let Entry::Occupied(mut slot) = state.entry((workflow_id.clone(), activity_id.clone()))
250        else {
251            return Ok(());
252        };
253        slot.get_mut().outstanding.retain(|issued| issued != token);
254        if slot.get().outstanding.is_empty() {
255            slot.remove();
256        }
257        Ok(())
258    }
259
260    /// Revoke whichever generation is current while parking for recovery.
261    ///
262    /// A park retires the execution site deliberately, so it takes every token
263    /// of the current generation with it — including a redelivery's sibling.
264    ///
265    /// # Errors
266    ///
267    /// Returns [`ServerError::LockPoisoned`] when fence state cannot be trusted.
268    pub fn revoke_current(
269        &self,
270        workflow_id: &WorkflowId,
271        activity_id: &ActivityId,
272    ) -> Result<(), ServerError> {
273        self.state()?
274            .remove(&(workflow_id.clone(), activity_id.clone()));
275        Ok(())
276    }
277
278    fn state(&self) -> Result<MutexGuard<'_, HashMap<ExecutionKey, SiteGeneration>>, ServerError> {
279        self.current
280            .lock()
281            .map_err(|_| ServerError::lock_poisoned("activity completion fences"))
282    }
283}
284
285/// Derive the stable external-effect key for one action site in one workflow run.
286///
287/// Attempts and execution generations are intentionally absent. The domain tag,
288/// workflow id, run id, and activity ordinal are length-unambiguous fixed-width
289/// inputs to SHA-256.
290#[must_use]
291pub fn idempotency_key(
292    workflow_id: &WorkflowId,
293    run_id: &RunId,
294    activity_id: &ActivityId,
295) -> String {
296    let mut hasher = Sha256::new();
297    hasher.update(b"aion.activity.idempotency.v1\0");
298    hasher.update(workflow_id.as_uuid().as_bytes());
299    hasher.update(run_id.as_uuid().as_bytes());
300    hasher.update(activity_id.sequence_position().to_be_bytes());
301    encode_hex(&hasher.finalize())
302}
303
304fn encode_hex(bytes: &[u8]) -> String {
305    const DIGITS: &[u8; 16] = b"0123456789abcdef";
306    let mut encoded = String::with_capacity(bytes.len() * 2);
307    for byte in bytes {
308        encoded.push(char::from(DIGITS[usize::from(byte >> 4)]));
309        encoded.push(char::from(DIGITS[usize::from(byte & 0x0f)]));
310    }
311    encoded
312}
313
314fn rejection(
315    workflow_id: &WorkflowId,
316    activity_id: &ActivityId,
317    reason: CompletionRejectionReason,
318) -> ServerError {
319    ServerError::ActivityCompletionRejected {
320        workflow_id: workflow_id.clone(),
321        activity_id: activity_id.clone(),
322        reason,
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use super::{CompletionFences, CompletionToken, idempotency_key};
329    use crate::error::{CompletionRejectionReason, ServerError};
330    use aion_core::{ActivityId, RunId, WorkflowId};
331
332    type TestResult = Result<(), Box<dyn std::error::Error>>;
333
334    #[test]
335    fn idempotency_key_is_attempt_independent_and_site_run_scoped() {
336        let workflow = WorkflowId::new_v4();
337        let run_a = RunId::new_v4();
338        let run_b = RunId::new_v4();
339        let site_a = ActivityId::from_sequence_position(7);
340        let site_b = ActivityId::from_sequence_position(8);
341
342        let first_attempt = idempotency_key(&workflow, &run_a, &site_a);
343        let fifth_attempt = idempotency_key(&workflow, &run_a, &site_a);
344        assert_eq!(first_attempt, fifth_attempt);
345        assert_ne!(first_attempt, idempotency_key(&workflow, &run_a, &site_b));
346        assert_ne!(first_attempt, idempotency_key(&workflow, &run_b, &site_a));
347    }
348
349    /// The FIRST attempt's token is refused once a genuine RETRY attempt has
350    /// been issued: c4e1412d7's invariant, by the attempt discriminator it put
351    /// on the envelope.
352    #[test]
353    fn issuing_a_retry_rejects_the_stale_generation() -> TestResult {
354        let fences = CompletionFences::default();
355        let workflow = WorkflowId::new_v4();
356        let run = RunId::new_v4();
357        let activity = ActivityId::from_sequence_position(3);
358        let stale = fences.issue(&workflow, &run, &activity, 1)?;
359        let current = fences.issue(&workflow, &run, &activity, 2)?;
360
361        let rejected = fences.accept(&workflow, &activity, &stale);
362        assert!(matches!(
363            rejected,
364            Err(ServerError::ActivityCompletionRejected {
365                reason: CompletionRejectionReason::StaleGeneration,
366                ..
367            })
368        ));
369        fences.accept(&workflow, &activity, &current)?;
370        Ok(())
371    }
372
373    #[test]
374    fn accepted_generation_is_consumed_exactly_once() -> TestResult {
375        let fences = CompletionFences::default();
376        let workflow = WorkflowId::new_v4();
377        let run = RunId::new_v4();
378        let activity = ActivityId::from_sequence_position(4);
379        let token = fences.issue(&workflow, &run, &activity, 1)?;
380
381        fences.accept(&workflow, &activity, &token)?;
382        let duplicate = fences.accept(&workflow, &activity, &token);
383        assert!(matches!(
384            duplicate,
385            Err(ServerError::ActivityCompletionRejected {
386                reason: CompletionRejectionReason::NoCurrentGeneration,
387                ..
388            })
389        ));
390        Ok(())
391    }
392
393    /// Revoking a superseded ATTEMPT's token never disturbs the retry that
394    /// replaced it. The same-attempt sibling case this name never claimed is
395    /// owned by
396    /// [`the_all_streams_closed_revoke_cannot_orphan_a_redelivered_sibling`].
397    #[test]
398    fn revoking_an_old_generation_does_not_remove_its_replacement() -> TestResult {
399        let fences = CompletionFences::default();
400        let workflow = WorkflowId::new_v4();
401        let run = RunId::new_v4();
402        let activity = ActivityId::from_sequence_position(6);
403        let old = fences.issue(&workflow, &run, &activity, 1)?;
404        let replacement = fences.issue(&workflow, &run, &activity, 2)?;
405
406        fences.revoke(&workflow, &activity, &old)?;
407        fences.accept(&workflow, &activity, &replacement)?;
408        Ok(())
409    }
410
411    #[test]
412    fn an_empty_wire_token_is_a_typed_compatibility_refusal() {
413        let workflow = WorkflowId::new_v4();
414        let activity = ActivityId::from_sequence_position(9);
415        let rejected = CompletionToken::from_wire(&workflow, &activity, String::new());
416        assert!(matches!(
417            rejected,
418            Err(ServerError::ActivityCompletionRejected {
419                reason: CompletionRejectionReason::MissingCompletionToken,
420                ..
421            })
422        ));
423    }
424
425    #[test]
426    fn a_pre_recovery_generation_is_rejected_after_recovery() -> TestResult {
427        let before_recovery = CompletionFences::default();
428        let workflow = WorkflowId::new_v4();
429        let run = RunId::new_v4();
430        let activity = ActivityId::from_sequence_position(5);
431        let stale = before_recovery.issue(&workflow, &run, &activity, 1)?;
432
433        let after_recovery = CompletionFences::default();
434        let current = after_recovery.issue(&workflow, &run, &activity, 1)?;
435        let rejected = after_recovery.accept(&workflow, &activity, &stale);
436        assert!(matches!(
437            rejected,
438            Err(ServerError::ActivityCompletionRejected {
439                reason: CompletionRejectionReason::StaleGeneration,
440                ..
441            })
442        ));
443        after_recovery.accept(&workflow, &activity, &current)?;
444        Ok(())
445    }
446
447    /// FENCE-1 T1: a redelivery between delivery and completion does not orphan
448    /// the finished result.
449    ///
450    /// Delivery is at-least-once (`transport_loss.rs`: worker loss is
451    /// attempt-neutral and re-dispatches the SAME attempt), so a worker that is
452    /// alive and finishing can have its work delivered a second time. The first
453    /// worker still holds the first token and its result is real: it is
454    /// accepted. The redelivery's token is then the duplicate, and it is refused
455    /// because the first acceptance consumed the whole generation.
456    ///
457    /// On the base this reads `StaleGeneration` for the FIRST worker — the
458    /// finished result is thrown away, and nothing retries because the outbox
459    /// row settled `Done` at dispatch.
460    #[test]
461    fn a_redelivery_of_the_same_attempt_does_not_orphan_the_first_worker() -> TestResult {
462        let fences = CompletionFences::default();
463        let workflow = WorkflowId::new_v4();
464        let run = RunId::new_v4();
465        let activity = ActivityId::from_sequence_position(11);
466
467        let first = fences.issue(&workflow, &run, &activity, 1)?;
468        // The redelivery: the SAME attempt of the SAME run, dispatched again.
469        let second = fences.issue(&workflow, &run, &activity, 1)?;
470
471        let accepted = fences.accept(&workflow, &activity, &first);
472        assert!(
473            accepted.is_ok(),
474            "the worker that genuinely held the FIRST delivery finished the work; its result \
475             must be accepted, not thrown away: {accepted:?}"
476        );
477
478        let duplicate = fences.accept(&workflow, &activity, &second);
479        assert!(
480            matches!(
481                duplicate,
482                Err(ServerError::ActivityCompletionRejected {
483                    reason: CompletionRejectionReason::NoCurrentGeneration,
484                    ..
485                })
486            ),
487            "the first accepted completion must consume EVERY outstanding token for the site, \
488             so the redelivered worker's completion is the duplicate: {duplicate:?}"
489        );
490        Ok(())
491    }
492
493    /// FENCE-1 T1b: the all-streams-closed revoke
494    /// (`dispatch.rs` `send_to_candidates`, immediately before `Ok(None)`)
495    /// cannot orphan a sibling still held by a live worker.
496    ///
497    /// This is the exact rocketfish 09:57:54Z interleaving: attempt A is
498    /// delivered and held, the redelivery B is issued for the same attempt,
499    /// every candidate stream is then found closed so B revokes its OWN token —
500    /// and on the base that removed the site's only key, so A's completion read
501    /// `NoCurrentGeneration` rather than `StaleGeneration`.
502    #[test]
503    fn the_all_streams_closed_revoke_cannot_orphan_a_redelivered_sibling() -> TestResult {
504        let fences = CompletionFences::default();
505        let workflow = WorkflowId::new_v4();
506        let run = RunId::new_v4();
507        let activity = ActivityId::from_sequence_position(12);
508
509        let delivered = fences.issue(&workflow, &run, &activity, 1)?;
510        let redelivery = fences.issue(&workflow, &run, &activity, 1)?;
511
512        // Every candidate stream was closed, so the redelivery withdraws the
513        // authorization IT minted — and only that one.
514        fences.revoke(&workflow, &activity, &redelivery)?;
515
516        let accepted = fences.accept(&workflow, &activity, &delivered);
517        assert!(
518            accepted.is_ok(),
519            "the sibling token the first worker still holds must survive the redelivery's own \
520             revoke: {accepted:?}"
521        );
522
523        let withdrawn = fences.accept(&workflow, &activity, &redelivery);
524        assert!(
525            matches!(
526                withdrawn,
527                Err(ServerError::ActivityCompletionRejected {
528                    reason: CompletionRejectionReason::NoCurrentGeneration,
529                    ..
530                })
531            ),
532            "a revoked token must never become truth: {withdrawn:?}"
533        );
534        Ok(())
535    }
536
537    /// FENCE-1 T2, half one: a superseded ATTEMPT is still refused.
538    ///
539    /// Regression pin, not a red-first test: it is green on the base too, for
540    /// the wrong reason (the base replaced the single slot on every issue). Its
541    /// value is that it stays green through the change, which is what says
542    /// c4e1412d7's invariant kept its name.
543    #[test]
544    fn a_superseded_attempt_is_still_refused_after_a_retry_is_issued() -> TestResult {
545        let fences = CompletionFences::default();
546        let workflow = WorkflowId::new_v4();
547        let run = RunId::new_v4();
548        let activity = ActivityId::from_sequence_position(13);
549
550        let earlier = fences.issue(&workflow, &run, &activity, 1)?;
551        let current = fences.issue(&workflow, &run, &activity, 2)?;
552
553        let refused = fences.accept(&workflow, &activity, &earlier);
554        assert!(
555            matches!(
556                refused,
557                Err(ServerError::ActivityCompletionRejected {
558                    reason: CompletionRejectionReason::StaleGeneration,
559                    ..
560                })
561            ),
562            "a stale worker can never be recorded as truth: attempt 1's worker was superseded by \
563             attempt 2 and its completion must stay refused: {refused:?}"
564        );
565        fences.accept(&workflow, &activity, &current)?;
566        Ok(())
567    }
568
569    /// FENCE-1 T2, half two: the EXECUTION-GENERATION boundary is still
570    /// refused across, with identical attempt numbers on both sides.
571    ///
572    /// A reset or continue-as-new mints a new [`RunId`], so the run-scoped
573    /// idempotency key c4e1412d7 already derives is the only discriminator that
574    /// separates these two attempt-1 tokens. This is the case
575    /// acceptance-by-held-generation would be blind to if the key were not used.
576    #[test]
577    fn a_superseded_execution_generation_is_still_refused_at_the_same_attempt() -> TestResult {
578        let fences = CompletionFences::default();
579        let workflow = WorkflowId::new_v4();
580        let run_a = RunId::new_v4();
581        let run_b = RunId::new_v4();
582        let activity = ActivityId::from_sequence_position(14);
583
584        let old_run = fences.issue(&workflow, &run_a, &activity, 1)?;
585        let new_run = fences.issue(&workflow, &run_b, &activity, 1)?;
586
587        let refused = fences.accept(&workflow, &activity, &old_run);
588        assert!(
589            matches!(
590                refused,
591                Err(ServerError::ActivityCompletionRejected {
592                    reason: CompletionRejectionReason::StaleGeneration,
593                    ..
594                })
595            ),
596            "a stale worker can never be recorded as truth: the superseded RUN's worker holds \
597             attempt 1 of a generation that no longer exists: {refused:?}"
598        );
599        fences.accept(&workflow, &activity, &new_run)?;
600        Ok(())
601    }
602
603    /// A settlement that fails after acceptance restores the WHOLE generation,
604    /// siblings included, so the true resolver — whichever token it holds — is
605    /// not refused with `NoCurrentGeneration`.
606    #[test]
607    fn a_restored_generation_carries_its_redelivered_sibling_back() -> TestResult {
608        let fences = CompletionFences::default();
609        let workflow = WorkflowId::new_v4();
610        let run = RunId::new_v4();
611        let activity = ActivityId::from_sequence_position(15);
612
613        let delivered = fences.issue(&workflow, &run, &activity, 1)?;
614        let redelivery = fences.issue(&workflow, &run, &activity, 1)?;
615
616        let accepted = fences.accept(&workflow, &activity, &delivered)?;
617        assert_eq!(accepted.attempt(), 1);
618        fences.restore_if_absent(&workflow, &activity, &accepted)?;
619
620        // The true resolver presents the sibling token and is accepted.
621        fences.accept(&workflow, &activity, &redelivery)?;
622        Ok(())
623    }
624
625    /// A restore never overwrites a generation issued while the settlement was
626    /// in flight: a concurrently issued newer generation always wins.
627    #[test]
628    fn a_restore_never_displaces_a_newer_generation() -> TestResult {
629        let fences = CompletionFences::default();
630        let workflow = WorkflowId::new_v4();
631        let run = RunId::new_v4();
632        let activity = ActivityId::from_sequence_position(16);
633
634        let first = fences.issue(&workflow, &run, &activity, 1)?;
635        let accepted = fences.accept(&workflow, &activity, &first)?;
636        let retry = fences.issue(&workflow, &run, &activity, 2)?;
637
638        fences.restore_if_absent(&workflow, &activity, &accepted)?;
639
640        let refused = fences.accept(&workflow, &activity, &first);
641        assert!(
642            matches!(
643                refused,
644                Err(ServerError::ActivityCompletionRejected {
645                    reason: CompletionRejectionReason::StaleGeneration,
646                    ..
647                })
648            ),
649            "the restore must not displace the retry that was issued meanwhile: {refused:?}"
650        );
651        fences.accept(&workflow, &activity, &retry)?;
652        Ok(())
653    }
654
655    /// A re-dispatch of an ALREADY superseded attempt registers nothing: it
656    /// cannot resurrect the attempt the engine has moved past, and the token it
657    /// hands back is refused when presented.
658    #[test]
659    fn re_issuing_a_superseded_attempt_registers_no_generation() -> TestResult {
660        let fences = CompletionFences::default();
661        let workflow = WorkflowId::new_v4();
662        let run = RunId::new_v4();
663        let activity = ActivityId::from_sequence_position(17);
664
665        let current = fences.issue(&workflow, &run, &activity, 2)?;
666        let out_of_order = fences.issue(&workflow, &run, &activity, 1)?;
667
668        let refused = fences.accept(&workflow, &activity, &out_of_order);
669        assert!(
670            matches!(
671                refused,
672                Err(ServerError::ActivityCompletionRejected {
673                    reason: CompletionRejectionReason::StaleGeneration,
674                    ..
675                })
676            ),
677            "an out-of-order re-dispatch of a superseded attempt must not become acceptable: \
678             {refused:?}"
679        );
680        fences.accept(&workflow, &activity, &current)?;
681        Ok(())
682    }
683
684    /// Two workers holding sibling tokens for one redelivered attempt: exactly
685    /// one becomes truth. The set of tokens that MAY be first is wider; how
686    /// many may be first is not.
687    #[test]
688    fn only_one_of_two_sibling_tokens_can_ever_become_truth() -> TestResult {
689        let fences = CompletionFences::default();
690        let workflow = WorkflowId::new_v4();
691        let run = RunId::new_v4();
692        let activity = ActivityId::from_sequence_position(18);
693
694        let first = fences.issue(&workflow, &run, &activity, 1)?;
695        let second = fences.issue(&workflow, &run, &activity, 1)?;
696
697        let accepted = [
698            fences.accept(&workflow, &activity, &second).is_ok(),
699            fences.accept(&workflow, &activity, &first).is_ok(),
700        ];
701        assert_eq!(
702            accepted.iter().filter(|ok| **ok).count(),
703            1,
704            "exactly one completion for a redelivered attempt may become truth"
705        );
706        Ok(())
707    }
708}