Skip to main content

behavior_actors/workflow/
barrier.rs

1//! Fixed-membership cyclic barrier coordination.
2
3use behavior::{
4    Actions, Address, Behavior, BehaviorActed, BehaviorBase, Never, NoBirths, Protocol,
5    SendEffects, User,
6};
7#[cfg(test)]
8use behavior::{Delivery, Recipient};
9use thiserror::Error;
10
11use crate::DeliveryRoute;
12
13/// Explicit barrier generation carried by every arrival and release.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
15pub struct BarrierGeneration(pub u64);
16
17/// Release fact delivered to every participant in a completed generation.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub struct BarrierReleased {
20    /// Exact generation whose complete membership arrived.
21    pub generation: BarrierGeneration,
22}
23
24/// One keyed arrival at a [`Barrier`].
25pub struct BarrierMessage<K, Route> {
26    /// Generation the participant intends to join.
27    pub generation: BarrierGeneration,
28    /// Participant key from the barrier's fixed membership.
29    pub participant: K,
30    /// Typed recipient for this generation's release.
31    pub reply_to: Route,
32}
33
34/// One accepted arrival retained until its generation releases.
35pub struct BarrierArrival<K, Route> {
36    /// Fixed-membership key.
37    pub participant: K,
38    /// Typed release recipient.
39    pub reply_to: Route,
40}
41
42/// Complete semantic state of a cyclic [`Barrier`].
43pub enum BarrierState<K, Route> {
44    /// The current generation is accepting its fixed membership once each.
45    Gathering {
46        /// Exact accepted generation.
47        generation: BarrierGeneration,
48        /// Accepted arrivals in arrival order.
49        arrivals: Vec<BarrierArrival<K, Route>>,
50    },
51    /// The final representable generation released; no later generation exists.
52    Exhausted {
53        /// Final released generation.
54        generation: BarrierGeneration,
55    },
56}
57
58/// Invalid fixed barrier definition.
59#[derive(Debug, Error, Clone, PartialEq, Eq)]
60pub enum BarrierConfigError<K> {
61    /// A barrier without participants has no meaningful release transition.
62    #[error("barrier membership must not be empty")]
63    EmptyMembership,
64    /// One participant key appeared more than once.
65    #[error("barrier membership contains a duplicate participant")]
66    DuplicateParticipant(K),
67}
68
69/// Validated fixed barrier membership, independent of an address namespace.
70pub struct BarrierMembership<K> {
71    members: Vec<K>,
72}
73
74impl<K: Clone + Eq> BarrierMembership<K> {
75    /// Validate fixed membership before binding it to an actor protocol.
76    ///
77    /// # Errors
78    ///
79    /// Returns a typed error for empty or duplicate membership.
80    pub fn new(members: Vec<K>) -> Result<Self, BarrierConfigError<K>> {
81        if members.is_empty() {
82            return Err(BarrierConfigError::EmptyMembership);
83        }
84        for (index, participant) in members.iter().enumerate() {
85            if members[..index].contains(participant) {
86                return Err(BarrierConfigError::DuplicateParticipant(
87                    participant.clone(),
88                ));
89            }
90        }
91        Ok(Self { members })
92    }
93}
94
95/// Rejected barrier arrival.
96#[derive(Debug, Error, Clone, PartialEq, Eq)]
97pub enum BarrierError<K, Route> {
98    /// The participant is not in fixed membership.
99    #[error("barrier participant is unknown")]
100    UnknownParticipant { participant: K, reply_to: Route },
101    /// The participant already arrived in this generation.
102    #[error("barrier participant already arrived in this generation")]
103    DuplicateArrival {
104        /// Duplicate participant key.
105        participant: K,
106        /// Current barrier generation.
107        generation: BarrierGeneration,
108        /// Exact release recipient from the rejected arrival.
109        reply_to: Route,
110    },
111    /// Arrival belongs to a generation already released.
112    #[error("barrier arrival is stale")]
113    StaleGeneration {
114        /// Participant whose arrival was rejected.
115        participant: K,
116        /// Generation carried by the arrival.
117        observed: BarrierGeneration,
118        /// Current accepted generation.
119        current: BarrierGeneration,
120        /// Exact release recipient from the rejected arrival.
121        reply_to: Route,
122    },
123    /// Arrival attempts to skip the current generation.
124    #[error("barrier arrival is for a future generation")]
125    FutureGeneration {
126        /// Participant whose arrival was rejected.
127        participant: K,
128        /// Generation carried by the arrival.
129        observed: BarrierGeneration,
130        /// Current accepted generation.
131        current: BarrierGeneration,
132        /// Exact release recipient from the rejected arrival.
133        reply_to: Route,
134    },
135    /// No representable successor generation remains.
136    #[error("barrier generations are exhausted")]
137    Exhausted {
138        /// Participant whose arrival was rejected.
139        participant: K,
140        /// Final released generation.
141        generation: BarrierGeneration,
142        /// Exact release recipient from the rejected arrival.
143        reply_to: Route,
144    },
145}
146
147/// Fixed-membership cyclic barrier behavior.
148///
149/// Each generation accepts every configured key exactly once. The final
150/// arrival commits the successor generation before emitting one
151/// [`BarrierReleased`] delivery to every arrival recipient in arrival order.
152/// Stale, future, duplicate, unknown, and exhausted arrivals are distinct
153/// typed errors and preserve state. At `u64::MAX`, the final release commits
154/// [`BarrierState::Exhausted`] instead of inferring wraparound. Initialization
155/// is empty and the behavior never terminates itself. Membership, generation,
156/// and release ordering are Bombay workflow policy; delivery is interpreted
157/// by Address and Communication. Construction rejects empty or duplicate
158/// membership. No method has a semantic panic condition.
159pub struct Barrier<A, K, Route>
160where
161    A: Address,
162    Route: DeliveryRoute,
163    Route::Protocol: Protocol<Addr = A, Msg = BarrierReleased>,
164{
165    members: Vec<K>,
166    state: BarrierState<K, Route>,
167    marker: core::marker::PhantomData<fn() -> A>,
168}
169
170impl<A, K, Route> Barrier<A, K, Route>
171where
172    A: Address,
173    K: Clone + Eq,
174    Route: DeliveryRoute,
175    Route::Protocol: Protocol<Addr = A, Msg = BarrierReleased>,
176{
177    /// Bind validated membership to generation zero of a barrier actor.
178    #[must_use]
179    pub fn new(membership: BarrierMembership<K>) -> Self {
180        Self {
181            members: membership.members,
182            state: BarrierState::Gathering {
183                generation: BarrierGeneration(0),
184                arrivals: Vec::new(),
185            },
186            marker: core::marker::PhantomData,
187        }
188    }
189
190    /// Fixed participant keys in definition order.
191    #[must_use]
192    pub fn members(&self) -> &[K] {
193        &self.members
194    }
195
196    /// Borrow the complete current generation state.
197    #[must_use]
198    pub const fn state(&self) -> &BarrierState<K, Route> {
199        &self.state
200    }
201}
202
203impl<A, K, Route> BehaviorBase for Barrier<A, K, Route>
204where
205    A: Address,
206    Route: DeliveryRoute,
207    Route::Protocol: Protocol<Addr = A, Msg = BarrierReleased>,
208{
209    type Base = Self;
210
211    fn base(&self) -> &Self {
212        self
213    }
214}
215
216impl<A, K, Route> behavior::Protocol for Barrier<A, K, Route>
217where
218    A: Address,
219    Route: DeliveryRoute,
220    Route::Protocol: Protocol<Addr = A, Msg = BarrierReleased>,
221{
222    type Addr = A;
223    type Msg = BarrierMessage<K, Route>;
224}
225
226impl<A, K, Route> Behavior for Barrier<A, K, Route>
227where
228    A: Address,
229    K: Clone + Eq,
230    Route: DeliveryRoute,
231    Route::Protocol: Protocol<Addr = A, Msg = BarrierReleased>,
232    Route::Sends: behavior::SendsFor<User<A, BarrierMessage<K, Route>>>,
233{
234    type Protocol = Self;
235    type Event = User<A, behavior::BehaviorMessage<Self>>;
236    type Sends = Route::Sends;
237    type Ph = Never;
238    type Error = BarrierError<K, Route>;
239    type Birth = NoBirths;
240
241    fn transition(&mut self, _: behavior::ActiveTurn, event: Self::Event) -> BehaviorActed<Self> {
242        let BarrierMessage {
243            generation: observed,
244            participant,
245            reply_to,
246        } = event.message;
247        if !self.members.contains(&participant) {
248            return Err(BarrierError::UnknownParticipant {
249                participant,
250                reply_to,
251            });
252        }
253        let (current, arrivals) = match &mut self.state {
254            BarrierState::Gathering {
255                generation,
256                arrivals,
257            } => (generation, arrivals),
258            BarrierState::Exhausted { generation } => {
259                return Err(BarrierError::Exhausted {
260                    participant,
261                    generation: *generation,
262                    reply_to,
263                });
264            }
265        };
266        if observed < *current {
267            return Err(BarrierError::StaleGeneration {
268                participant,
269                observed,
270                current: *current,
271                reply_to,
272            });
273        }
274        if observed > *current {
275            return Err(BarrierError::FutureGeneration {
276                participant,
277                observed,
278                current: *current,
279                reply_to,
280            });
281        }
282        if arrivals
283            .iter()
284            .any(|arrival| arrival.participant == participant)
285        {
286            return Err(BarrierError::DuplicateArrival {
287                participant,
288                generation: *current,
289                reply_to,
290            });
291        }
292        arrivals.push(BarrierArrival {
293            participant,
294            reply_to,
295        });
296        if arrivals.len() < self.members.len() {
297            return Ok(Actions::cont());
298        }
299        let generation = *current;
300        let completed = core::mem::take(arrivals);
301        self.state =
302            generation
303                .0
304                .checked_add(1)
305                .map_or(BarrierState::Exhausted { generation }, |next| {
306                    BarrierState::Gathering {
307                        generation: BarrierGeneration(next),
308                        arrivals: Vec::with_capacity(self.members.len()),
309                    }
310                });
311        let mut sends = Route::Sends::empty();
312        for arrival in completed {
313            sends.append(arrival.reply_to.deliver(BarrierReleased { generation }));
314        }
315        Ok(Actions::send(sends))
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322    use crate::Activate as _;
323    use behavior::MailAddr;
324
325    #[test]
326    fn configuration_rejects_empty_and_duplicate_membership() {
327        assert!(matches!(
328            BarrierMembership::new(Vec::<u8>::new()),
329            Err(BarrierConfigError::EmptyMembership)
330        ));
331        assert!(matches!(
332            BarrierMembership::new(vec![1, 2, 1]),
333            Err(BarrierConfigError::DuplicateParticipant(1))
334        ));
335    }
336
337    #[test]
338    fn generation_releases_exact_membership_in_arrival_order() {
339        let one = Recipient::from(MailAddr(1));
340        let two = Recipient::from(MailAddr(2));
341        let mut barrier = Barrier::new(BarrierMembership::new(vec![1_u8, 2]).unwrap())
342            .initialize()
343            .unwrap()
344            .behavior;
345        let first = barrier
346            .receive(
347                MailAddr(9),
348                BarrierMessage {
349                    generation: BarrierGeneration(0),
350                    participant: 2,
351                    reply_to: two,
352                },
353            )
354            .unwrap();
355        assert!(first.sends.is_empty());
356        let rejection = barrier.receive(
357            MailAddr(9),
358            BarrierMessage {
359                generation: BarrierGeneration(0),
360                participant: 2,
361                reply_to: two,
362            },
363        );
364        assert!(matches!(
365            rejection,
366            Err(BarrierError::DuplicateArrival {
367                participant: 2,
368                generation: BarrierGeneration(0),
369                reply_to,
370            }) if reply_to == two
371        ));
372        let released = barrier
373            .receive(
374                MailAddr(9),
375                BarrierMessage {
376                    generation: BarrierGeneration(0),
377                    participant: 1,
378                    reply_to: one,
379                },
380            )
381            .unwrap();
382        assert!(
383            released.sends
384                == vec![
385                    Delivery::new(
386                        two,
387                        BarrierReleased {
388                            generation: BarrierGeneration(0),
389                        },
390                    ),
391                    Delivery::new(
392                        one,
393                        BarrierReleased {
394                            generation: BarrierGeneration(0),
395                        },
396                    ),
397                ]
398        );
399        assert!(matches!(
400            barrier.state(),
401            BarrierState::Gathering {
402                generation: BarrierGeneration(1),
403                arrivals,
404            } if arrivals.is_empty()
405        ));
406        let rejection = barrier.receive(
407            MailAddr(9),
408            BarrierMessage {
409                generation: BarrierGeneration(0),
410                participant: 1,
411                reply_to: one,
412            },
413        );
414        assert!(matches!(
415            rejection,
416            Err(BarrierError::StaleGeneration {
417                current: BarrierGeneration(1),
418                reply_to,
419                ..
420            }) if reply_to == one
421        ));
422    }
423
424    #[test]
425    fn final_generation_releases_then_exhausts_without_wraparound() {
426        let participant = Recipient::from(MailAddr(1));
427        let mut definition = Barrier::new(BarrierMembership::new(vec![1_u8]).unwrap());
428        definition.state = BarrierState::Gathering {
429            generation: BarrierGeneration(u64::MAX),
430            arrivals: Vec::new(),
431        };
432        let mut barrier = (definition).initialize().unwrap().behavior;
433        let released = barrier
434            .receive(
435                MailAddr(9),
436                BarrierMessage {
437                    generation: BarrierGeneration(u64::MAX),
438                    participant: 1,
439                    reply_to: participant,
440                },
441            )
442            .unwrap();
443        assert_eq!(released.sends.len(), 1);
444        assert!(matches!(
445            barrier.state(),
446            BarrierState::Exhausted {
447                generation: BarrierGeneration(u64::MAX),
448            }
449        ));
450    }
451}