Skip to main content

behavior/
pool.rs

1//! A bounded, FIFO worker pool expressed entirely as a pure behavior.
2//!
3//! Pool scheduling is a derived Bombay construction, not an actor-model
4//! primitive. Runtime installation, delivery, and observation remain effects
5//! for an interpreter; this module owns only their typed protocol and fold.
6
7use core::convert::Infallible;
8use core::marker::PhantomData;
9use std::collections::{BTreeMap, VecDeque};
10use std::time::Duration;
11
12use crate::{
13    Actions, Address, Behavior, Births, Crash, CreationRejection, Delivery, Exit, FleetError,
14    Never, Own, Proxy, ProxyCommand, Recipient, RestartPolicy, SendAlgebra, SendInput, Strategy,
15    SupervisionEvent, Supervisor, SupervisorError, SupervisorSends, User, WorkerCreationResolved,
16    WorkerStopped,
17};
18
19/// Caller-chosen identity used to correlate pool responses.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub struct JobId(pub u64);
22
23/// Pool-owned correlation token for one exact dispatch attempt.
24///
25/// This is not an actor identity or evidence that delivery occurred.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub struct AssignmentId(pub u64);
28
29/// One assignment accepted by a worker behavior.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct PoolAssignment<J> {
32    pub assignment: AssignmentId,
33    pub job: JobId,
34    pub payload: J,
35}
36
37/// Messages accepted by a pool coordinator.
38#[derive(Clone, PartialEq, Eq)]
39pub enum PoolMessage<A, D, J, R>
40where
41    A: Address,
42    D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>,
43{
44    Submit {
45        job: JobId,
46        payload: J,
47        reply_to: Recipient<D>,
48    },
49    Completed {
50        worker: <D::Addr as Address>::Nonce,
51        assignment: AssignmentId,
52        result: R,
53    },
54}
55
56/// Messages accepted by a key-persistent pool coordinator.
57///
58/// `Rebalance` is the only input that can change an established key binding.
59/// It affects later submissions; jobs already accepted retain their selected
60/// stable worker slot.
61#[derive(Clone, PartialEq, Eq)]
62pub enum KeyedPoolMessage<A, D, K, J, R>
63where
64    A: Address,
65    D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>,
66{
67    Submit {
68        key: K,
69        job: JobId,
70        payload: J,
71        reply_to: Recipient<D>,
72    },
73    Completed {
74        worker: <D::Addr as Address>::Nonce,
75        assignment: AssignmentId,
76        result: R,
77    },
78    Rebalance {
79        key: K,
80        worker: <D::Addr as Address>::Nonce,
81    },
82}
83
84/// Why a submitted job was not accepted by the pool.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum PoolRejection {
87    BacklogFull,
88    /// The key's selected stable slot is unknown or permanently retired.
89    AffinityUnavailable,
90}
91
92/// Why an accepted assignment ended without a worker completion.
93#[derive(Clone, PartialEq, Eq)]
94pub enum PoolInterruption<A: Address> {
95    WorkerStopped {
96        worker: A::Nonce,
97        outcome: Result<Exit<A>, Crash>,
98    },
99    NoRecoverableWorkers,
100    /// The job's selected stable slot retired while the job was queued.
101    AffinityRetired {
102        worker: A::Nonce,
103        reason: WorkerRetirement,
104    },
105}
106
107/// Complete response protocol for one submitted job.
108#[derive(Clone, PartialEq, Eq)]
109pub enum PoolResponse<J, R, A: Address> {
110    Accepted {
111        job: JobId,
112    },
113    Rejected {
114        job: JobId,
115        payload: J,
116        reason: PoolRejection,
117    },
118    Completed {
119        job: JobId,
120        result: R,
121    },
122    Interrupted {
123        job: JobId,
124        payload: J,
125        reason: PoolInterruption<A>,
126    },
127}
128
129/// Bombay policy for an assigned job whose worker incarnation stops.
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum InterruptionPolicy {
132    /// End pool ownership and report the still-owned job to its submitter.
133    Fail,
134    /// Put the job at the front of the backlog for at-least-once assignment.
135    Retry,
136}
137
138/// Public, payload-free view of one stable worker slot.
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub enum WorkerPhase {
141    Installing,
142    Idle,
143    Assigned {
144        assignment: AssignmentId,
145        job: JobId,
146    },
147    Retired {
148        reason: WorkerRetirement,
149    },
150}
151
152/// Why a stable worker slot is no longer eligible for dispatch.
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub enum WorkerRetirement {
155    CreationRejected(CreationRejection),
156    ReplacementUnavailable,
157}
158
159/// Invalid static pool topology.
160#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
161pub enum PoolConfigError<N> {
162    /// No stable worker slot exists, so accepted ownership could never end.
163    #[error("a worker pool requires at least one worker")]
164    NoWorkers,
165    /// Two configured positions selected the same stable worker nonce.
166    #[error("two configured worker positions selected the same nonce")]
167    DuplicateWorker(N),
168    #[error("the configured worker nonce sequence is exhausted")]
169    SequenceExhausted,
170}
171
172/// Pure, statically dispatched policy for a previously unseen affinity key.
173pub trait AffinitySelector<K, N> {
174    /// Select the stable worker nonce for a key that has no binding yet.
175    fn select(&self, key: &K) -> N;
176}
177
178impl<K, N, F> AffinitySelector<K, N> for F
179where
180    F: Fn(&K) -> N,
181{
182    fn select(&self, key: &K) -> N {
183        self(key)
184    }
185}
186
187/// Typed rejection of an event that cannot apply to the current pool state.
188#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
189pub enum PoolError<N> {
190    #[error("the event names an unknown worker")]
191    UnknownWorker(N),
192    #[error("the worker nonce is already configured")]
193    DuplicateWorker(N),
194    #[error("a pool-owned sequence is exhausted")]
195    SequenceExhausted,
196    #[error("the worker factory rejected a configured index")]
197    WorkerFactoryIndex { index: usize },
198    #[error("a completion targeted a worker that cannot complete work")]
199    CompletionForUnavailableWorker { worker: N, phase: WorkerPhase },
200    #[error("the completion carries a stale assignment identifier")]
201    StaleCompletion {
202        worker: N,
203        expected: AssignmentId,
204        received: AssignmentId,
205    },
206    #[error("a stop observation targeted an unavailable worker")]
207    WorkerStoppedWhileUnavailable { worker: N, phase: WorkerPhase },
208    #[error("a creation result targeted a worker that is not installing")]
209    CreationResolvedWhileUnavailable { worker: N, phase: WorkerPhase },
210    #[error("an affinity rebalance targeted a retired worker")]
211    RebalanceToRetiredWorker { worker: N, reason: WorkerRetirement },
212}
213
214struct AcceptedJob<A: Address, D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>, J, R> {
215    id: JobId,
216    payload: J,
217    reply_to: Recipient<D>,
218    interruption: Option<PoolInterruption<A>>,
219    target: Option<A::Nonce>,
220}
221
222struct QueuedJob<A: Address, D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>, J, R> {
223    accepted: AcceptedJob<A, D, J, R>,
224    dispatch_payload: J,
225}
226
227enum SlotState<A: Address, D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>, J, R> {
228    Installing,
229    Idle,
230    Assigned {
231        assignment: AssignmentId,
232        job: AcceptedJob<A, D, J, R>,
233    },
234    Retired {
235        reason: WorkerRetirement,
236    },
237}
238
239struct Slot<A: Address, D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>, J, R> {
240    nonce: A::Nonce,
241    state: SlotState<A, D, J, R>,
242}
243
244impl<A, D, J, R> Slot<A, D, J, R>
245where
246    A: Address,
247    D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>,
248{
249    fn phase(&self) -> WorkerPhase {
250        match &self.state {
251            SlotState::Installing => WorkerPhase::Installing,
252            SlotState::Idle => WorkerPhase::Idle,
253            SlotState::Assigned { assignment, job } => WorkerPhase::Assigned {
254                assignment: *assignment,
255                job: job.id,
256            },
257            SlotState::Retired { reason } => WorkerPhase::Retired { reason: *reason },
258        }
259    }
260}
261
262struct PlannedDispatch {
263    slot_position: usize,
264    job_position: usize,
265}
266
267enum Admission {
268    Accepted,
269    Rejected,
270}
271
272/// The pool's concrete event sum, including existing supervision facts.
273pub type PoolEvent<A, D, J, R> = SupervisionEvent<User<A, PoolMessage<A, D, J, R>>>;
274
275/// Concrete event sum for a [`KeyedWorkerPool`].
276pub type KeyedPoolEvent<A, D, K, J, R> = SupervisionEvent<User<A, KeyedPoolMessage<A, D, K, J, R>>>;
277
278/// Named pool-owned delivery lanes.
279pub struct PoolBehaviorSends<A, D, J, R, C>
280where
281    A: Address,
282    A::Nonce: From<u64>,
283    D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>,
284    C: Behavior<Addr = A, Ph = Never>,
285{
286    /// Admission and terminal responses addressed to submitters.
287    pub responses: Vec<Delivery<D>>,
288    /// Assignments addressed to the selected stable worker proxies.
289    pub assignments: Vec<Delivery<Proxy<C>>>,
290}
291
292impl<A, D, J, R, C> SendAlgebra for PoolBehaviorSends<A, D, J, R, C>
293where
294    A: Address,
295    A::Nonce: From<u64>,
296    D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>,
297    C: Behavior<Addr = A, Ph = Never>,
298{
299    fn empty() -> Self {
300        Self {
301            responses: Vec::new(),
302            assignments: Vec::new(),
303        }
304    }
305
306    fn append(&mut self, mut other: Self) {
307        self.responses.append(&mut other.responses);
308        self.assignments.append(&mut other.assignments);
309    }
310}
311
312impl<A, D, J, R, C> SendInput<Delivery<D>, Own> for PoolBehaviorSends<A, D, J, R, C>
313where
314    A: Address,
315    A::Nonce: From<u64>,
316    D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>,
317    C: Behavior<Addr = A, Ph = Never>,
318{
319    fn emit(&mut self, input: Delivery<D>) {
320        self.responses.push(input);
321    }
322}
323
324impl<A, D, J, R, C> SendInput<Delivery<Proxy<C>>, Own> for PoolBehaviorSends<A, D, J, R, C>
325where
326    A: Address,
327    A::Nonce: From<u64>,
328    D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>,
329    C: Behavior<Addr = A, Ph = Never>,
330{
331    fn emit(&mut self, input: Delivery<Proxy<C>>) {
332        self.assignments.push(input);
333    }
334}
335
336type KernelSends<A, D, J, R, C> = PoolBehaviorSends<A, D, J, R, C>;
337
338/// Pool effects keep responses and assignments in named, independently
339/// appendable lanes within the supervised behavior send product.
340pub type PoolSends<A, D, J, R, C> = SupervisorSends<A, KernelSends<A, D, J, R, C>, C>;
341
342/// Complete action type returned by a [`WorkerPool`] transition.
343pub type PoolActions<A, D, J, R, C> = Actions<A, Never, PoolSends<A, D, J, R, C>, Births<Proxy<C>>>;
344
345#[allow(
346    clippy::type_complexity,
347    reason = "the marker retains the complete pool topology signature"
348)]
349struct PoolKernel<A: Address, D, J, R, C>(PhantomData<fn(A, D, J, R, C)>);
350
351impl<A: Address, D, J, R, C> PoolKernel<A, D, J, R, C> {
352    const fn new() -> Self {
353        Self(PhantomData)
354    }
355}
356
357impl<A, D, J, R, C> Behavior for PoolKernel<A, D, J, R, C>
358where
359    A: Address,
360    A::Nonce: From<u64>,
361    D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>,
362    C: Behavior<Addr = A, Msg = PoolAssignment<J>, Ph = Never>,
363{
364    type Addr = A;
365    type Msg = PoolMessage<A, D, J, R>;
366    type Event = User<A, PoolMessage<A, D, J, R>>;
367    type Sends = KernelSends<A, D, J, R, C>;
368    type Ph = Never;
369    type Error = Infallible;
370    type Birth = Births<C>;
371
372    fn init(&mut self, _: crate::InitializationTurn) -> crate::BehaviorActed<Self> {
373        Ok(Actions::cont())
374    }
375
376    fn transition(
377        &mut self,
378        _: crate::ActiveTurn,
379        _event: Self::Event,
380    ) -> crate::BehaviorActed<Self> {
381        Ok(Actions::cont())
382    }
383}
384
385type PoolSupervisor<A, D, J, R, C> = Supervisor<PoolKernel<A, D, J, R, C>, C>;
386
387/// A fixed, homogeneous, bounded FIFO worker pool.
388///
389/// Each configured nonce names one stable supervised proxy. Jobs are assigned
390/// only after a successful worker-creation result makes that slot idle. The
391/// retained state records an assignment before the corresponding delivery is
392/// returned, and a completion must carry the exact assignment token.
393///
394/// Assignment-sequence exhaustion is a typed [`PoolError::SequenceExhausted`]
395/// rejection selected before backlog or slot state is changed. As with any
396/// generic Rust API, a user-provided `Clone` implementation can itself unwind;
397/// the pool introduces no panic path of its own.
398///
399/// A worker with any other message protocol cannot form a pool:
400///
401/// ```compile_fail
402/// use behavior::{Actions, Behavior, MailAddr, Never, NoBirths, PoolResponse, User, WorkerPool};
403///
404/// struct Reply;
405/// struct WrongWorker;
406/// impl Behavior for Reply {
407///     type Addr = MailAddr;
408///     type Msg = PoolResponse<String, (), MailAddr>;
409///     type Event = User<MailAddr, Self::Msg>;
410///     type Sends = Vec<Never>;
411///     type Ph = Never;
412///     type Error = Never;
413///     type Birth = NoBirths;
414///     fn init(&mut self, _: crate::InitializationTurn) -> behavior::BehaviorActed<Self> { Ok(Actions::cont()) }
415///     fn transition(&mut self, _: crate::ActiveTurn, _: Self::Event) -> behavior::BehaviorActed<Self> { Ok(Actions::cont()) }
416/// }
417/// impl Behavior for WrongWorker {
418///     type Addr = MailAddr;
419///     type Msg = u8;
420///     type Event = User<MailAddr, u8>;
421///     type Sends = Vec<behavior::Never>;
422///     type Ph = Never;
423///     type Error = Never;
424///     type Birth = NoBirths;
425///     fn init(&mut self, _: crate::InitializationTurn) -> behavior::BehaviorActed<Self> { Ok(Actions::cont()) }
426///     fn transition(&mut self, _: crate::ActiveTurn, _: Self::Event) -> behavior::BehaviorActed<Self> { Ok(Actions::cont()) }
427/// }
428///
429/// // `WrongWorker::Msg` is not `PoolAssignment<String>`.
430/// let _: Option<WorkerPool<MailAddr, Reply, String, (), WrongWorker>> = None;
431/// ```
432pub struct WorkerPool<A: Address, D, J, R, C>
433where
434    D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>,
435    A::Nonce: From<u64>,
436    C: Behavior<Addr = A, Msg = PoolAssignment<J>, Ph = Never>,
437{
438    supervisor: PoolSupervisor<A, D, J, R, C>,
439    slots: Vec<Slot<A, D, J, R>>,
440    backlog: VecDeque<QueuedJob<A, D, J, R>>,
441    backlog_capacity: usize,
442    next_assignment: u64,
443    interruption: InterruptionPolicy,
444}
445
446impl<A, D, J, R, C> WorkerPool<A, D, J, R, C>
447where
448    A: Address,
449    A::Nonce: From<u64>,
450    D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>,
451    C: Behavior<Addr = A, Msg = PoolAssignment<J>, Ph = Never>,
452{
453    /// Construct a pool after proving that every configured child route is
454    /// unique.
455    ///
456    /// # Errors
457    ///
458    /// Returns [`PoolConfigError::NoWorkers`] for an empty topology or
459    /// [`PoolConfigError::DuplicateWorker`] for the first repeated
460    /// creator-local nonce. No behavior or creation request is produced.
461    #[allow(
462        clippy::too_many_arguments,
463        reason = "the arguments expose the complete pool policy"
464    )]
465    pub fn new(
466        nonces: fn(usize) -> A::Nonce,
467        count: usize,
468        build: fn(usize) -> Option<C>,
469        backlog_capacity: usize,
470        interruption: InterruptionPolicy,
471        restart_policy: RestartPolicy,
472        max_restarts: u32,
473        restart_window: Duration,
474    ) -> Result<Self, PoolConfigError<A::Nonce>> {
475        if count == 0 {
476            return Err(PoolConfigError::NoWorkers);
477        }
478        let mut slots = Vec::with_capacity(count);
479        for index in 0..count {
480            let nonce = nonces(index);
481            if slots
482                .iter()
483                .any(|slot: &Slot<A, D, J, R>| slot.nonce == nonce)
484            {
485                return Err(PoolConfigError::DuplicateWorker(nonce));
486            }
487            slots.push(Slot {
488                nonce,
489                state: SlotState::Installing,
490            });
491        }
492        Ok(Self {
493            supervisor: Supervisor::new(
494                PoolKernel::new(),
495                nonces,
496                count,
497                build,
498                Strategy::OneForOne,
499                restart_policy,
500                max_restarts,
501                restart_window,
502            )
503            .map_err(|error| match error {
504                FleetError::UnknownChild(nonce) | FleetError::DuplicateChild(nonce) => {
505                    PoolConfigError::DuplicateWorker(nonce)
506                }
507                FleetError::SequenceExhausted => PoolConfigError::SequenceExhausted,
508            })?,
509            slots,
510            backlog: VecDeque::new(),
511            backlog_capacity,
512            next_assignment: 0,
513            interruption,
514        })
515    }
516
517    #[must_use]
518    pub fn backlog_len(&self) -> usize {
519        self.backlog.len()
520    }
521
522    #[must_use]
523    pub fn worker_phase(&self, worker: A::Nonce) -> Option<WorkerPhase> {
524        self.slots
525            .iter()
526            .find(|slot| slot.nonce == worker)
527            .map(Slot::phase)
528    }
529
530    fn slot_position(&self, worker: A::Nonce) -> Result<usize, PoolError<A::Nonce>> {
531        self.slots
532            .iter()
533            .position(|slot| slot.nonce == worker)
534            .ok_or(PoolError::UnknownWorker(worker))
535    }
536}
537
538impl<A, D, J, R, C> WorkerPool<A, D, J, R, C>
539where
540    A: Address,
541    A::Nonce: From<u64>,
542    D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>,
543    J: Clone,
544    C: Behavior<Addr = A, Msg = PoolAssignment<J>, Ph = Never>,
545{
546    fn supervisor_transition(
547        &mut self,
548        event: PoolEvent<A, D, J, R>,
549    ) -> Result<PoolActions<A, D, J, R, C>, PoolError<A::Nonce>> {
550        crate::calculus::delegate_transition(&mut self.supervisor, event).map_err(|error| {
551            match error {
552                SupervisorError::Behavior(never) => match never {},
553                SupervisorError::Fleet(FleetError::UnknownChild(nonce)) => {
554                    PoolError::UnknownWorker(nonce)
555                }
556                SupervisorError::Fleet(FleetError::DuplicateChild(nonce)) => {
557                    PoolError::DuplicateWorker(nonce)
558                }
559                SupervisorError::Fleet(FleetError::SequenceExhausted) => {
560                    PoolError::SequenceExhausted
561                }
562                SupervisorError::FactoryIndex { index } => PoolError::WorkerFactoryIndex { index },
563            }
564        })
565    }
566
567    fn submit(
568        &mut self,
569        job: JobId,
570        payload: J,
571        reply_to: Recipient<D>,
572        actions: &mut PoolActions<A, D, J, R, C>,
573    ) {
574        let can_dispatch = self
575            .slots
576            .iter()
577            .any(|slot| matches!(slot.state, SlotState::Idle));
578        if !can_dispatch && self.backlog.len() == self.backlog_capacity {
579            actions
580                .sends
581                .behavior
582                .send::<Delivery<D>, Own>(Delivery::new(
583                    reply_to,
584                    PoolResponse::Rejected {
585                        job,
586                        payload,
587                        reason: PoolRejection::BacklogFull,
588                    },
589                ));
590            return;
591        }
592        let dispatch_payload = payload.clone();
593        self.backlog.push_back(QueuedJob {
594            accepted: AcceptedJob {
595                id: job,
596                payload,
597                reply_to,
598                interruption: None,
599                target: None,
600            },
601            dispatch_payload,
602        });
603        actions
604            .sends
605            .behavior
606            .send::<_, Own>(Delivery::new(reply_to, PoolResponse::Accepted { job }));
607    }
608
609    fn submit_to(
610        &mut self,
611        target: A::Nonce,
612        job: JobId,
613        payload: J,
614        reply_to: Recipient<D>,
615        actions: &mut PoolActions<A, D, J, R, C>,
616    ) -> Admission {
617        let Some(slot) = self.slots.iter().find(|slot| slot.nonce == target) else {
618            actions
619                .sends
620                .behavior
621                .send::<Delivery<D>, Own>(Delivery::new(
622                    reply_to,
623                    PoolResponse::Rejected {
624                        job,
625                        payload,
626                        reason: PoolRejection::AffinityUnavailable,
627                    },
628                ));
629            return Admission::Rejected;
630        };
631        if matches!(slot.state, SlotState::Retired { .. }) {
632            actions.sends.behavior.send::<_, Own>(Delivery::new(
633                reply_to,
634                PoolResponse::Rejected {
635                    job,
636                    payload,
637                    reason: PoolRejection::AffinityUnavailable,
638                },
639            ));
640            return Admission::Rejected;
641        }
642        let can_dispatch = matches!(slot.state, SlotState::Idle);
643        if !can_dispatch && self.backlog.len() == self.backlog_capacity {
644            actions.sends.behavior.send::<_, Own>(Delivery::new(
645                reply_to,
646                PoolResponse::Rejected {
647                    job,
648                    payload,
649                    reason: PoolRejection::BacklogFull,
650                },
651            ));
652            return Admission::Rejected;
653        }
654        let dispatch_payload = payload.clone();
655        self.backlog.push_back(QueuedJob {
656            accepted: AcceptedJob {
657                id: job,
658                payload,
659                reply_to,
660                interruption: None,
661                target: Some(target),
662            },
663            dispatch_payload,
664        });
665        actions
666            .sends
667            .behavior
668            .send::<_, Own>(Delivery::new(reply_to, PoolResponse::Accepted { job }));
669        Admission::Accepted
670    }
671
672    fn complete(
673        &mut self,
674        worker: A::Nonce,
675        assignment: AssignmentId,
676        result: R,
677        actions: &mut PoolActions<A, D, J, R, C>,
678    ) -> Result<(), PoolError<A::Nonce>> {
679        let position = self.slot_position(worker)?;
680        let previous = core::mem::replace(&mut self.slots[position].state, SlotState::Idle);
681        let job = match previous {
682            SlotState::Assigned {
683                assignment: expected,
684                job,
685            } if expected == assignment => job,
686            SlotState::Assigned {
687                assignment: expected,
688                job,
689            } => {
690                self.slots[position].state = SlotState::Assigned {
691                    assignment: expected,
692                    job,
693                };
694                return Err(PoolError::StaleCompletion {
695                    worker,
696                    expected,
697                    received: assignment,
698                });
699            }
700            state => {
701                let phase = match &state {
702                    SlotState::Installing => WorkerPhase::Installing,
703                    SlotState::Idle => WorkerPhase::Idle,
704                    SlotState::Assigned { assignment, job } => WorkerPhase::Assigned {
705                        assignment: *assignment,
706                        job: job.id,
707                    },
708                    SlotState::Retired { reason } => WorkerPhase::Retired { reason: *reason },
709                };
710                self.slots[position].state = state;
711                return Err(PoolError::CompletionForUnavailableWorker { worker, phase });
712            }
713        };
714        actions.sends.behavior.send::<_, Own>(Delivery::new(
715            job.reply_to,
716            PoolResponse::Completed {
717                job: job.id,
718                result,
719            },
720        ));
721        Ok(())
722    }
723
724    fn worker_stopped(
725        &mut self,
726        stopped: &WorkerStopped<A>,
727        responses: &mut Vec<Delivery<D>>,
728    ) -> Result<(), PoolError<A::Nonce>> {
729        let position = self.slot_position(stopped.proxy)?;
730        let phase = self.slots[position].phase();
731        if matches!(phase, WorkerPhase::Installing | WorkerPhase::Retired { .. }) {
732            return Err(PoolError::WorkerStoppedWhileUnavailable {
733                worker: stopped.proxy,
734                phase,
735            });
736        }
737        if self.interruption == InterruptionPolicy::Retry {
738            // Cloning a user payload can execute arbitrary `Clone` code. Prepare it
739            // before committing the slot transition so unwinding cannot leave the
740            // worker marked as installing while its accepted job is lost.
741            let dispatch_payload = match &self.slots[position].state {
742                SlotState::Assigned { job, .. } => Some(job.payload.clone()),
743                SlotState::Idle => None,
744                SlotState::Installing | SlotState::Retired { .. } => return Ok(()),
745            };
746            let previous =
747                core::mem::replace(&mut self.slots[position].state, SlotState::Installing);
748            if let (SlotState::Assigned { mut job, .. }, Some(dispatch_payload)) =
749                (previous, dispatch_payload)
750            {
751                job.interruption = Some(PoolInterruption::WorkerStopped {
752                    worker: stopped.proxy,
753                    outcome: stopped.outcome,
754                });
755                self.backlog.push_front(QueuedJob {
756                    accepted: job,
757                    dispatch_payload,
758                });
759            }
760            return Ok(());
761        }
762
763        let previous = core::mem::replace(&mut self.slots[position].state, SlotState::Installing);
764        if let SlotState::Assigned { job, .. } = previous {
765            responses.push(Delivery::new(
766                job.reply_to,
767                PoolResponse::Interrupted {
768                    job: job.id,
769                    payload: job.payload,
770                    reason: PoolInterruption::WorkerStopped {
771                        worker: stopped.proxy,
772                        outcome: stopped.outcome,
773                    },
774                },
775            ));
776        }
777        Ok(())
778    }
779
780    fn fail_backlog_if_irrecoverable(&mut self, actions: &mut PoolActions<A, D, J, R, C>) {
781        if self
782            .slots
783            .iter()
784            .any(|slot| !matches!(slot.state, SlotState::Retired { .. }))
785        {
786            return;
787        }
788        for queued in self.backlog.drain(..) {
789            let job = queued.accepted;
790            actions.sends.behavior.send::<_, Own>(Delivery::new(
791                job.reply_to,
792                PoolResponse::Interrupted {
793                    job: job.id,
794                    payload: job.payload,
795                    reason: job
796                        .interruption
797                        .unwrap_or(PoolInterruption::NoRecoverableWorkers),
798                },
799            ));
800        }
801    }
802
803    fn fail_jobs_for_retired_slot(
804        &mut self,
805        worker: A::Nonce,
806        reason: WorkerRetirement,
807        actions: &mut PoolActions<A, D, J, R, C>,
808    ) {
809        let mut retained = VecDeque::with_capacity(self.backlog.len());
810        while let Some(queued) = self.backlog.pop_front() {
811            if queued.accepted.target == Some(worker) {
812                let job = queued.accepted;
813                actions.sends.behavior.send::<_, Own>(Delivery::new(
814                    job.reply_to,
815                    PoolResponse::Interrupted {
816                        job: job.id,
817                        payload: job.payload,
818                        reason: job
819                            .interruption
820                            .unwrap_or(PoolInterruption::AffinityRetired { worker, reason }),
821                    },
822                ));
823            } else {
824                retained.push_back(queued);
825            }
826        }
827        self.backlog = retained;
828    }
829
830    fn creation_resolved(
831        &mut self,
832        resolved: &WorkerCreationResolved<A::Nonce>,
833    ) -> Result<(), PoolError<A::Nonce>> {
834        let position = self.slot_position(resolved.proxy)?;
835        let phase = self.slots[position].phase();
836        if !matches!(phase, WorkerPhase::Installing) {
837            return Err(PoolError::CreationResolvedWhileUnavailable {
838                worker: resolved.proxy,
839                phase,
840            });
841        }
842        self.slots[position].state = match resolved.result {
843            Ok(()) => SlotState::Idle,
844            Err(rejection) => SlotState::Retired {
845                reason: WorkerRetirement::CreationRejected(rejection),
846            },
847        };
848        Ok(())
849    }
850
851    fn dispatch(
852        &mut self,
853        actions: &mut PoolActions<A, D, J, R, C>,
854    ) -> Result<(), PoolError<A::Nonce>> {
855        let mut selected_jobs = Vec::new();
856        let mut plan = Vec::new();
857        for (slot_position, slot) in self.slots.iter().enumerate() {
858            if !matches!(slot.state, SlotState::Idle) {
859                continue;
860            }
861            let Some(job_position) = self.backlog.iter().enumerate().find_map(|(position, job)| {
862                (!selected_jobs.contains(&position)
863                    && job
864                        .accepted
865                        .target
866                        .is_none_or(|target| target == slot.nonce))
867                .then_some(position)
868            }) else {
869                continue;
870            };
871            selected_jobs.push(job_position);
872            plan.push(PlannedDispatch {
873                slot_position,
874                job_position,
875            });
876        }
877        let count = u64::try_from(plan.len()).map_err(|_| PoolError::SequenceExhausted)?;
878        let next_assignment = self
879            .next_assignment
880            .checked_add(count)
881            .ok_or(PoolError::SequenceExhausted)?;
882
883        let mut selected_by_position = BTreeMap::new();
884        for planned in plan {
885            selected_by_position.insert(planned.job_position, planned.slot_position);
886        }
887        let mut selected_by_slot: Vec<Option<QueuedJob<A, D, J, R>>> =
888            std::iter::repeat_with(|| None)
889                .take(self.slots.len())
890                .collect();
891        let mut remaining = VecDeque::new();
892        for (position, queued) in self.backlog.drain(..).enumerate() {
893            if let Some(slot_position) = selected_by_position.remove(&position) {
894                selected_by_slot[slot_position] = Some(queued);
895            } else {
896                remaining.push_back(queued);
897            }
898        }
899        self.backlog = remaining;
900
901        for (assignment, (slot_position, queued)) in (self.next_assignment..next_assignment).zip(
902            selected_by_slot
903                .into_iter()
904                .enumerate()
905                .filter_map(|(slot_position, queued)| queued.map(|queued| (slot_position, queued))),
906        ) {
907            let payload = queued.dispatch_payload;
908            let job = queued.accepted;
909            let assignment = AssignmentId(assignment);
910            let nonce = self.slots[slot_position].nonce;
911            let job_id = job.id;
912            self.slots[slot_position].state = SlotState::Assigned { assignment, job };
913            actions
914                .sends
915                .behavior
916                .send::<Delivery<Proxy<C>>, Own>(Delivery::new(
917                    Recipient::child(nonce),
918                    ProxyCommand::Forward(PoolAssignment {
919                        assignment,
920                        job: job_id,
921                        payload,
922                    }),
923                ));
924        }
925        self.next_assignment = next_assignment;
926        Ok(())
927    }
928}
929
930impl<A, D, J, R, C> Behavior for WorkerPool<A, D, J, R, C>
931where
932    A: Address,
933    A::Nonce: From<u64>,
934    D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>,
935    J: Clone,
936    C: Behavior<Addr = A, Msg = PoolAssignment<J>, Ph = Never>,
937{
938    type Addr = A;
939    type Msg = PoolMessage<A, D, J, R>;
940    type Event = PoolEvent<A, D, J, R>;
941    type Sends = PoolSends<A, D, J, R, C>;
942    type Ph = Never;
943    type Error = PoolError<A::Nonce>;
944    type Birth = Births<Proxy<C>>;
945
946    fn init(&mut self, _: crate::InitializationTurn) -> crate::BehaviorActed<Self> {
947        crate::calculus::initialize(&mut self.supervisor).map_err(|error| match error {
948            SupervisorError::Behavior(never) => match never {},
949            SupervisorError::Fleet(FleetError::UnknownChild(nonce)) => {
950                PoolError::UnknownWorker(nonce)
951            }
952            SupervisorError::Fleet(FleetError::DuplicateChild(nonce)) => {
953                PoolError::DuplicateWorker(nonce)
954            }
955            SupervisorError::Fleet(FleetError::SequenceExhausted) => PoolError::SequenceExhausted,
956            SupervisorError::FactoryIndex { index } => PoolError::WorkerFactoryIndex { index },
957        })
958    }
959
960    fn transition(
961        &mut self,
962        _: crate::ActiveTurn,
963        event: Self::Event,
964    ) -> crate::BehaviorActed<Self> {
965        match event {
966            SupervisionEvent::Behavior(User {
967                message:
968                    PoolMessage::Submit {
969                        job,
970                        payload,
971                        reply_to,
972                    },
973                ..
974            }) => {
975                let mut actions = Actions::cont();
976                self.submit(job, payload, reply_to, &mut actions);
977                self.dispatch(&mut actions)?;
978                Ok(actions)
979            }
980            SupervisionEvent::Behavior(User {
981                message:
982                    PoolMessage::Completed {
983                        worker,
984                        assignment,
985                        result,
986                    },
987                ..
988            }) => {
989                let mut actions = Actions::cont();
990                self.complete(worker, assignment, result, &mut actions)?;
991                self.dispatch(&mut actions)?;
992                Ok(actions)
993            }
994            SupervisionEvent::WorkerStopped(stopped) => {
995                let proxy = stopped.proxy;
996                let mut responses = Vec::new();
997                self.worker_stopped(&stopped, &mut responses)?;
998                let mut actions =
999                    self.supervisor_transition(SupervisionEvent::WorkerStopped(stopped))?;
1000                actions.sends.behavior.responses.extend(responses);
1001                let replacement_requested = actions
1002                    .sends
1003                    .replacement_commands
1004                    .iter()
1005                    .any(|delivery| delivery.to.is_child(proxy));
1006                if !replacement_requested {
1007                    let position = self.slot_position(proxy)?;
1008                    let reason = WorkerRetirement::ReplacementUnavailable;
1009                    self.slots[position].state = SlotState::Retired { reason };
1010                    self.fail_jobs_for_retired_slot(proxy, reason, &mut actions);
1011                }
1012                self.dispatch(&mut actions)?;
1013                self.fail_backlog_if_irrecoverable(&mut actions);
1014                Ok(actions)
1015            }
1016            SupervisionEvent::WorkerCreationResolved(resolved) => {
1017                let proxy = resolved.proxy;
1018                self.creation_resolved(&resolved)?;
1019                let mut actions =
1020                    self.supervisor_transition(SupervisionEvent::WorkerCreationResolved(resolved))?;
1021                if let Some(WorkerPhase::Retired { reason }) = self.worker_phase(proxy) {
1022                    self.fail_jobs_for_retired_slot(proxy, reason, &mut actions);
1023                }
1024                self.dispatch(&mut actions)?;
1025                self.fail_backlog_if_irrecoverable(&mut actions);
1026                Ok(actions)
1027            }
1028            SupervisionEvent::ChildStopped(stopped) => {
1029                self.supervisor_transition(SupervisionEvent::ChildStopped(stopped))
1030            }
1031            SupervisionEvent::CreationResolved(resolved) => {
1032                self.supervisor_transition(SupervisionEvent::CreationResolved(resolved))
1033            }
1034        }
1035    }
1036}
1037
1038#[cfg(test)]
1039#[allow(
1040    clippy::items_after_test_module,
1041    reason = "the local fixed-pool regression sits beside that implementation"
1042)]
1043mod tests {
1044    use super::*;
1045    use crate::{MailAddr, NoBirths};
1046
1047    struct TestReply;
1048
1049    impl Behavior for TestReply {
1050        type Addr = MailAddr;
1051        type Msg = PoolResponse<u8, (), MailAddr>;
1052        type Event = User<MailAddr, Self::Msg>;
1053        type Sends = Vec<Never>;
1054        type Ph = Never;
1055        type Error = Never;
1056        type Birth = NoBirths;
1057
1058        fn init(&mut self, _: crate::InitializationTurn) -> crate::BehaviorActed<Self> {
1059            Ok(Actions::cont())
1060        }
1061
1062        fn transition(
1063            &mut self,
1064            _: crate::ActiveTurn,
1065            _: Self::Event,
1066        ) -> crate::BehaviorActed<Self> {
1067            Ok(Actions::cont())
1068        }
1069    }
1070
1071    #[derive(Clone, Copy)]
1072    struct TestWorker;
1073
1074    impl Behavior for TestWorker {
1075        type Addr = MailAddr;
1076        type Msg = PoolAssignment<u8>;
1077        type Event = User<MailAddr, PoolAssignment<u8>>;
1078        type Sends = Vec<Never>;
1079        type Ph = Never;
1080        type Error = Never;
1081        type Birth = NoBirths;
1082
1083        fn init(&mut self, _: crate::InitializationTurn) -> crate::BehaviorActed<Self> {
1084            Ok(Actions::cont())
1085        }
1086
1087        fn transition(
1088            &mut self,
1089            _: crate::ActiveTurn,
1090            _: Self::Event,
1091        ) -> crate::BehaviorActed<Self> {
1092            Ok(Actions::cont())
1093        }
1094    }
1095
1096    fn test_worker(_: usize) -> TestWorker {
1097        TestWorker
1098    }
1099
1100    #[test]
1101    fn one_dispatch_batch_preserves_fifo_jobs_across_index_removal() {
1102        let mut pool = WorkerPool::new(
1103            |index| u64::try_from(index).unwrap(),
1104            2,
1105            |index| Some(test_worker(index)),
1106            3,
1107            InterruptionPolicy::Fail,
1108            RestartPolicy::Permanent,
1109            1,
1110            Duration::from_secs(1),
1111        )
1112        .unwrap();
1113        crate::calculus::initialize(&mut pool).unwrap();
1114
1115        for job in 1..=3 {
1116            crate::calculus::delegate_transition(
1117                &mut pool,
1118                SupervisionEvent::Behavior(User::new(
1119                    MailAddr(90),
1120                    PoolMessage::Submit {
1121                        job: JobId(job),
1122                        payload: u8::try_from(job).unwrap(),
1123                        reply_to: Recipient::global(MailAddr(91)),
1124                    },
1125                )),
1126            )
1127            .unwrap();
1128        }
1129        pool.slots[0].state = SlotState::Idle;
1130        pool.slots[1].state = SlotState::Idle;
1131
1132        let mut actions: PoolActions<MailAddr, TestReply, u8, (), TestWorker> = Actions::cont();
1133        pool.dispatch(&mut actions).unwrap();
1134
1135        let assignments = &actions.sends.behavior.assignments;
1136        assert_eq!(assignments.len(), 2);
1137        for (index, expected_job) in [JobId(1), JobId(2)].into_iter().enumerate() {
1138            assert!(
1139                assignments[index]
1140                    .to
1141                    .is_child(u64::try_from(index).unwrap())
1142            );
1143            let ProxyCommand::Forward(assignment) = &assignments[index].message else {
1144                panic!("pool dispatches with Forward");
1145            };
1146            assert_eq!(
1147                assignment.assignment,
1148                AssignmentId(u64::try_from(index).unwrap())
1149            );
1150            assert_eq!(assignment.job, expected_job);
1151        }
1152        assert_eq!(pool.backlog.len(), 1);
1153        assert_eq!(pool.backlog[0].accepted.id, JobId(3));
1154    }
1155}
1156
1157/// A worker pool whose admitted keys remain bound to stable worker slots.
1158///
1159/// The selector chooses a stable proxy nonce only when a key is first
1160/// admitted. Replacement incarnations remain behind that proxy, so they do
1161/// not alter affinity. [`KeyedPoolMessage::Rebalance`] is the sole transition
1162/// that changes an established binding, and jobs accepted before it retain
1163/// their original target.
1164///
1165/// Keys must have a concrete equality relation; a key type without `Eq` cannot
1166/// form an affinity table:
1167///
1168/// ```compile_fail
1169/// use behavior::{Actions, Behavior, KeyedWorkerPool, MailAddr, Never, NoBirths, PoolResponse, User};
1170/// struct NonKey(f64);
1171/// struct Reply;
1172/// struct Worker;
1173/// impl Behavior for Reply {
1174///     type Addr = MailAddr;
1175///     type Msg = PoolResponse<u8, (), MailAddr>;
1176///     type Event = User<MailAddr, Self::Msg>;
1177///     type Sends = Vec<Never>;
1178///     type Ph = Never;
1179///     type Error = Never;
1180///     type Birth = NoBirths;
1181///     fn init(&mut self, _: crate::InitializationTurn) -> behavior::BehaviorActed<Self> { Ok(Actions::cont()) }
1182///     fn transition(&mut self, _: crate::ActiveTurn, _: Self::Event) -> behavior::BehaviorActed<Self> { Ok(Actions::cont()) }
1183/// }
1184/// #[behavior::behavior(
1185///     addr = MailAddr,
1186///     message = behavior::PoolAssignment<u8>,
1187///     sends = Vec<Never>,
1188///     births = NoBirths,
1189///     error = Never,
1190/// )]
1191/// impl Worker {
1192///     fn init(&mut self, _: crate::InitializationTurn) -> behavior::Acted<MailAddr, Never, Vec<Never>, NoBirths, Never> {
1193///         Ok(Actions::cont())
1194///     }
1195///     fn receive(&mut self, _: MailAddr, _: behavior::PoolAssignment<u8>) -> behavior::Acted<MailAddr, Never, Vec<Never>, NoBirths, Never> {
1196///         Ok(Actions::cont())
1197///     }
1198/// }
1199/// let _: Option<KeyedWorkerPool<MailAddr, Reply, NonKey, u8, (), Worker, fn(&NonKey) -> u64>> = None;
1200/// ```
1201pub struct KeyedWorkerPool<A: Address, D, K, J, R, C, S>
1202where
1203    A::Nonce: From<u64>,
1204    D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>,
1205    K: Eq,
1206    C: Behavior<Addr = A, Msg = PoolAssignment<J>, Ph = Never>,
1207    S: AffinitySelector<K, A::Nonce>,
1208{
1209    pool: WorkerPool<A, D, J, R, C>,
1210    bindings: Vec<(K, A::Nonce)>,
1211    selector: S,
1212}
1213
1214impl<A, D, K, J, R, C, S> KeyedWorkerPool<A, D, K, J, R, C, S>
1215where
1216    A: Address,
1217    A::Nonce: From<u64>,
1218    D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>,
1219    K: Eq,
1220    C: Behavior<Addr = A, Msg = PoolAssignment<J>, Ph = Never>,
1221    S: AffinitySelector<K, A::Nonce>,
1222{
1223    /// Construct a key-persistent pool over the same fixed supervised slots as
1224    /// [`WorkerPool`]. The selector is pure and is consulted once per
1225    /// previously unseen key. It chooses behavior policy; runtime route
1226    /// resolution remains outside this type.
1227    ///
1228    /// # Errors
1229    ///
1230    /// Returns [`PoolConfigError::NoWorkers`] for an empty topology or
1231    /// [`PoolConfigError::DuplicateWorker`] for a repeated stable nonce.
1232    #[allow(
1233        clippy::too_many_arguments,
1234        reason = "the arguments expose the complete pool and affinity policy"
1235    )]
1236    pub fn new(
1237        nonces: fn(usize) -> A::Nonce,
1238        count: usize,
1239        build: fn(usize) -> Option<C>,
1240        backlog_capacity: usize,
1241        interruption: InterruptionPolicy,
1242        restart_policy: RestartPolicy,
1243        max_restarts: u32,
1244        restart_window: Duration,
1245        selector: S,
1246    ) -> Result<Self, PoolConfigError<A::Nonce>> {
1247        Ok(Self {
1248            pool: WorkerPool::new(
1249                nonces,
1250                count,
1251                build,
1252                backlog_capacity,
1253                interruption,
1254                restart_policy,
1255                max_restarts,
1256                restart_window,
1257            )?,
1258            bindings: Vec::new(),
1259            selector,
1260        })
1261    }
1262
1263    /// Return the stable slot currently bound to `key`.
1264    #[must_use]
1265    pub fn affinity(&self, key: &K) -> Option<A::Nonce> {
1266        self.bindings
1267            .iter()
1268            .find_map(|(bound, worker)| (bound == key).then_some(*worker))
1269    }
1270
1271    #[must_use]
1272    pub fn backlog_len(&self) -> usize {
1273        self.pool.backlog_len()
1274    }
1275
1276    #[must_use]
1277    pub fn worker_phase(&self, worker: A::Nonce) -> Option<WorkerPhase> {
1278        self.pool.worker_phase(worker)
1279    }
1280
1281    fn rebalance(&mut self, key: K, worker: A::Nonce) -> Result<(), PoolError<A::Nonce>> {
1282        let position = self.pool.slot_position(worker)?;
1283        if let SlotState::Retired { reason } = self.pool.slots[position].state {
1284            return Err(PoolError::RebalanceToRetiredWorker { worker, reason });
1285        }
1286        if let Some((_, bound)) = self.bindings.iter_mut().find(|(bound, _)| *bound == key) {
1287            *bound = worker;
1288        } else {
1289            self.bindings.push((key, worker));
1290        }
1291        Ok(())
1292    }
1293}
1294
1295impl<A, D, K, J, R, C, S> Behavior for KeyedWorkerPool<A, D, K, J, R, C, S>
1296where
1297    A: Address,
1298    A::Nonce: From<u64>,
1299    D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>,
1300    K: Eq,
1301    J: Clone,
1302    C: Behavior<Addr = A, Msg = PoolAssignment<J>, Ph = Never>,
1303    S: AffinitySelector<K, A::Nonce>,
1304{
1305    type Addr = A;
1306    type Msg = KeyedPoolMessage<A, D, K, J, R>;
1307    type Event = KeyedPoolEvent<A, D, K, J, R>;
1308    type Sends = PoolSends<A, D, J, R, C>;
1309    type Ph = Never;
1310    type Error = PoolError<A::Nonce>;
1311    type Birth = Births<Proxy<C>>;
1312
1313    fn init(&mut self, _: crate::InitializationTurn) -> crate::BehaviorActed<Self> {
1314        crate::calculus::initialize(&mut self.pool)
1315    }
1316
1317    fn transition(
1318        &mut self,
1319        _: crate::ActiveTurn,
1320        event: Self::Event,
1321    ) -> crate::BehaviorActed<Self> {
1322        match event {
1323            SupervisionEvent::Behavior(User {
1324                message:
1325                    KeyedPoolMessage::Submit {
1326                        key,
1327                        job,
1328                        payload,
1329                        reply_to,
1330                    },
1331                ..
1332            }) => {
1333                let existing = self.affinity(&key);
1334                let target = existing.unwrap_or_else(|| self.selector.select(&key));
1335                let mut actions = Actions::cont();
1336                let admission = self
1337                    .pool
1338                    .submit_to(target, job, payload, reply_to, &mut actions);
1339                match (admission, existing) {
1340                    (Admission::Accepted, None) => self.bindings.push((key, target)),
1341                    (Admission::Accepted | Admission::Rejected, Some(_))
1342                    | (Admission::Rejected, None) => {}
1343                }
1344                self.pool.dispatch(&mut actions)?;
1345                Ok(actions)
1346            }
1347            SupervisionEvent::Behavior(User {
1348                message:
1349                    KeyedPoolMessage::Completed {
1350                        worker,
1351                        assignment,
1352                        result,
1353                    },
1354                ..
1355            }) => {
1356                let mut actions = Actions::cont();
1357                self.pool
1358                    .complete(worker, assignment, result, &mut actions)?;
1359                self.pool.dispatch(&mut actions)?;
1360                Ok(actions)
1361            }
1362            SupervisionEvent::Behavior(User {
1363                message: KeyedPoolMessage::Rebalance { key, worker },
1364                ..
1365            }) => {
1366                self.rebalance(key, worker)?;
1367                Ok(Actions::cont())
1368            }
1369            SupervisionEvent::WorkerStopped(stopped) => crate::calculus::delegate_transition(
1370                &mut self.pool,
1371                SupervisionEvent::WorkerStopped(stopped),
1372            ),
1373            SupervisionEvent::WorkerCreationResolved(resolved) => {
1374                crate::calculus::delegate_transition(
1375                    &mut self.pool,
1376                    SupervisionEvent::WorkerCreationResolved(resolved),
1377                )
1378            }
1379            SupervisionEvent::ChildStopped(stopped) => crate::calculus::delegate_transition(
1380                &mut self.pool,
1381                SupervisionEvent::ChildStopped(stopped),
1382            ),
1383            SupervisionEvent::CreationResolved(resolved) => crate::calculus::delegate_transition(
1384                &mut self.pool,
1385                SupervisionEvent::CreationResolved(resolved),
1386            ),
1387        }
1388    }
1389}