1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub struct JobId(pub u64);
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub struct AssignmentId(pub u64);
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct PoolAssignment<J> {
32 pub assignment: AssignmentId,
33 pub job: JobId,
34 pub payload: J,
35}
36
37#[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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum PoolRejection {
87 BacklogFull,
88 AffinityUnavailable,
90}
91
92#[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 AffinityRetired {
102 worker: A::Nonce,
103 reason: WorkerRetirement,
104 },
105}
106
107#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum InterruptionPolicy {
132 Fail,
134 Retry,
136}
137
138#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub enum WorkerRetirement {
155 CreationRejected(CreationRejection),
156 ReplacementUnavailable,
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
161pub enum PoolConfigError<N> {
162 #[error("a worker pool requires at least one worker")]
164 NoWorkers,
165 #[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
172pub trait AffinitySelector<K, N> {
174 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#[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
272pub type PoolEvent<A, D, J, R> = SupervisionEvent<User<A, PoolMessage<A, D, J, R>>>;
274
275pub type KeyedPoolEvent<A, D, K, J, R> = SupervisionEvent<User<A, KeyedPoolMessage<A, D, K, J, R>>>;
277
278pub 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 pub responses: Vec<Delivery<D>>,
288 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
338pub type PoolSends<A, D, J, R, C> = SupervisorSends<A, KernelSends<A, D, J, R, C>, C>;
341
342pub 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
387pub 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 #[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 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
1157pub 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 #[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 #[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}