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, Never, Own,
14 Proxy, ProxyCommand, Recipient, RestartPolicy, SendAlgebra, SendInput, Strategy,
15 SupervisionEvent, Supervisor, SupervisorSends, User, WorkerCreationResolved, WorkerStopped,
16 delegate_transition,
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)]
161pub enum PoolConfigError<N> {
162 NoWorkers,
164 DuplicateWorker(N),
166}
167
168pub trait AffinitySelector<K, N> {
170 fn select(&self, key: &K) -> N;
172}
173
174impl<K, N, F> AffinitySelector<K, N> for F
175where
176 F: Fn(&K) -> N,
177{
178 fn select(&self, key: &K) -> N {
179 self(key)
180 }
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185pub enum PoolError<N> {
186 UnknownWorker(N),
187 CompletionForUnavailableWorker {
188 worker: N,
189 phase: WorkerPhase,
190 },
191 StaleCompletion {
192 worker: N,
193 expected: AssignmentId,
194 received: AssignmentId,
195 },
196 WorkerStoppedWhileUnavailable {
197 worker: N,
198 phase: WorkerPhase,
199 },
200 CreationResolvedWhileUnavailable {
201 worker: N,
202 phase: WorkerPhase,
203 },
204 RebalanceToRetiredWorker {
205 worker: N,
206 reason: WorkerRetirement,
207 },
208}
209
210struct AcceptedJob<A: Address, D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>, J, R> {
211 id: JobId,
212 payload: J,
213 reply_to: Recipient<D>,
214 interruption: Option<PoolInterruption<A>>,
215 target: Option<A::Nonce>,
216}
217
218struct QueuedJob<A: Address, D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>, J, R> {
219 accepted: AcceptedJob<A, D, J, R>,
220 dispatch_payload: J,
221}
222
223enum SlotState<A: Address, D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>, J, R> {
224 Installing,
225 Idle,
226 Assigned {
227 assignment: AssignmentId,
228 job: AcceptedJob<A, D, J, R>,
229 },
230 Retired {
231 reason: WorkerRetirement,
232 },
233}
234
235struct Slot<A: Address, D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>, J, R> {
236 nonce: A::Nonce,
237 state: SlotState<A, D, J, R>,
238}
239
240struct PlannedDispatch {
241 slot_position: usize,
242 job_position: usize,
243}
244
245enum Admission {
246 Accepted,
247 Rejected,
248}
249
250pub type PoolEvent<A, D, J, R> = SupervisionEvent<User<A, PoolMessage<A, D, J, R>>>;
252
253pub type KeyedPoolEvent<A, D, K, J, R> = SupervisionEvent<User<A, KeyedPoolMessage<A, D, K, J, R>>>;
255
256pub struct PoolBehaviorSends<A, D, J, R, C>
258where
259 A: Address,
260 A::Nonce: From<u64>,
261 D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>,
262 C: Behavior<Addr = A, Ph = Never>,
263{
264 pub responses: Vec<Delivery<D>>,
266 pub assignments: Vec<Delivery<Proxy<C>>>,
268}
269
270impl<A, D, J, R, C> SendAlgebra for PoolBehaviorSends<A, D, J, R, C>
271where
272 A: Address,
273 A::Nonce: From<u64>,
274 D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>,
275 C: Behavior<Addr = A, Ph = Never>,
276{
277 fn empty() -> Self {
278 Self {
279 responses: Vec::new(),
280 assignments: Vec::new(),
281 }
282 }
283
284 fn append(&mut self, mut other: Self) {
285 self.responses.append(&mut other.responses);
286 self.assignments.append(&mut other.assignments);
287 }
288}
289
290impl<A, D, J, R, C> SendInput<Delivery<D>, Own> for PoolBehaviorSends<A, D, J, R, C>
291where
292 A: Address,
293 A::Nonce: From<u64>,
294 D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>,
295 C: Behavior<Addr = A, Ph = Never>,
296{
297 fn emit(&mut self, input: Delivery<D>) {
298 self.responses.push(input);
299 }
300}
301
302impl<A, D, J, R, C> SendInput<Delivery<Proxy<C>>, Own> for PoolBehaviorSends<A, D, J, R, C>
303where
304 A: Address,
305 A::Nonce: From<u64>,
306 D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>,
307 C: Behavior<Addr = A, Ph = Never>,
308{
309 fn emit(&mut self, input: Delivery<Proxy<C>>) {
310 self.assignments.push(input);
311 }
312}
313
314type KernelSends<A, D, J, R, C> = PoolBehaviorSends<A, D, J, R, C>;
315
316pub type PoolSends<A, D, J, R, C> = SupervisorSends<A, KernelSends<A, D, J, R, C>, C>;
319
320pub type PoolActions<A, D, J, R, C> = Actions<A, Never, PoolSends<A, D, J, R, C>, Births<Proxy<C>>>;
322
323#[allow(
324 clippy::type_complexity,
325 reason = "the marker retains the complete pool topology signature"
326)]
327struct PoolKernel<A: Address, D, J, R, C>(PhantomData<fn(A, D, J, R, C)>);
328
329impl<A: Address, D, J, R, C> PoolKernel<A, D, J, R, C> {
330 const fn new() -> Self {
331 Self(PhantomData)
332 }
333}
334
335impl<A, D, J, R, C> Behavior for PoolKernel<A, D, J, R, C>
336where
337 A: Address,
338 A::Nonce: From<u64>,
339 D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>,
340 C: Behavior<Addr = A, Msg = PoolAssignment<J>, Ph = Never>,
341{
342 type Addr = A;
343 type Msg = PoolMessage<A, D, J, R>;
344 type Event = User<A, PoolMessage<A, D, J, R>>;
345 type Sends = KernelSends<A, D, J, R, C>;
346 type Ph = Never;
347 type Error = Infallible;
348 type Birth = Births<C>;
349
350 fn init(&mut self) -> crate::BehaviorActed<Self> {
351 Ok(Actions::cont())
352 }
353
354 fn transition(&mut self, _event: Self::Event) -> crate::BehaviorActed<Self> {
355 Ok(Actions::cont())
356 }
357}
358
359type PoolSupervisor<A, D, J, R, C> = Supervisor<PoolKernel<A, D, J, R, C>, C>;
360
361pub struct WorkerPool<A: Address, D, J, R, C>
413where
414 D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>,
415 A::Nonce: From<u64>,
416 C: Behavior<Addr = A, Msg = PoolAssignment<J>, Ph = Never>,
417{
418 supervisor: PoolSupervisor<A, D, J, R, C>,
419 slots: Vec<Slot<A, D, J, R>>,
420 backlog: VecDeque<QueuedJob<A, D, J, R>>,
421 backlog_capacity: usize,
422 next_assignment: u64,
423 interruption: InterruptionPolicy,
424}
425
426impl<A, D, J, R, C> WorkerPool<A, D, J, R, C>
427where
428 A: Address,
429 A::Nonce: From<u64>,
430 D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>,
431 C: Behavior<Addr = A, Msg = PoolAssignment<J>, Ph = Never>,
432{
433 #[allow(
442 clippy::too_many_arguments,
443 reason = "the arguments expose the complete pool policy"
444 )]
445 pub fn new(
446 nonces: fn(usize) -> A::Nonce,
447 count: usize,
448 build: fn(usize) -> C,
449 backlog_capacity: usize,
450 interruption: InterruptionPolicy,
451 restart_policy: RestartPolicy,
452 max_restarts: u32,
453 restart_window: Duration,
454 ) -> Result<Self, PoolConfigError<A::Nonce>> {
455 if count == 0 {
456 return Err(PoolConfigError::NoWorkers);
457 }
458 let mut slots = Vec::with_capacity(count);
459 for index in 0..count {
460 let nonce = nonces(index);
461 if slots
462 .iter()
463 .any(|slot: &Slot<A, D, J, R>| slot.nonce == nonce)
464 {
465 return Err(PoolConfigError::DuplicateWorker(nonce));
466 }
467 slots.push(Slot {
468 nonce,
469 state: SlotState::Installing,
470 });
471 }
472 Ok(Self {
473 supervisor: Supervisor::new(
474 PoolKernel::new(),
475 nonces,
476 count,
477 build,
478 Strategy::OneForOne,
479 restart_policy,
480 max_restarts,
481 restart_window,
482 ),
483 slots,
484 backlog: VecDeque::new(),
485 backlog_capacity,
486 next_assignment: 0,
487 interruption,
488 })
489 }
490
491 #[must_use]
492 pub fn backlog_len(&self) -> usize {
493 self.backlog.len()
494 }
495
496 #[must_use]
497 pub fn worker_phase(&self, worker: A::Nonce) -> Option<WorkerPhase> {
498 self.slots
499 .iter()
500 .find(|slot| slot.nonce == worker)
501 .map(|slot| match &slot.state {
502 SlotState::Installing => WorkerPhase::Installing,
503 SlotState::Idle => WorkerPhase::Idle,
504 SlotState::Assigned { assignment, job } => WorkerPhase::Assigned {
505 assignment: *assignment,
506 job: job.id,
507 },
508 SlotState::Retired { reason } => WorkerPhase::Retired { reason: *reason },
509 })
510 }
511
512 fn slot_position(&self, worker: A::Nonce) -> Result<usize, PoolError<A::Nonce>> {
513 self.slots
514 .iter()
515 .position(|slot| slot.nonce == worker)
516 .ok_or(PoolError::UnknownWorker(worker))
517 }
518}
519
520impl<A, D, J, R, C> WorkerPool<A, D, J, R, C>
521where
522 A: Address,
523 A::Nonce: From<u64>,
524 D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>,
525 J: Clone,
526 C: Behavior<Addr = A, Msg = PoolAssignment<J>, Ph = Never>,
527{
528 fn supervisor_transition(
529 &mut self,
530 event: PoolEvent<A, D, J, R>,
531 ) -> PoolActions<A, D, J, R, C> {
532 match delegate_transition(&mut self.supervisor, event) {
533 Ok(actions) => actions,
534 Err(never) => match never {},
535 }
536 }
537
538 fn submit(
539 &mut self,
540 job: JobId,
541 payload: J,
542 reply_to: Recipient<D>,
543 actions: &mut PoolActions<A, D, J, R, C>,
544 ) {
545 let can_dispatch = self
546 .slots
547 .iter()
548 .any(|slot| matches!(slot.state, SlotState::Idle));
549 if !can_dispatch && self.backlog.len() == self.backlog_capacity {
550 actions
551 .sends
552 .behavior
553 .send::<Delivery<D>, Own>(Delivery::new(
554 reply_to,
555 PoolResponse::Rejected {
556 job,
557 payload,
558 reason: PoolRejection::BacklogFull,
559 },
560 ));
561 return;
562 }
563 let dispatch_payload = payload.clone();
564 self.backlog.push_back(QueuedJob {
565 accepted: AcceptedJob {
566 id: job,
567 payload,
568 reply_to,
569 interruption: None,
570 target: None,
571 },
572 dispatch_payload,
573 });
574 actions
575 .sends
576 .behavior
577 .send::<_, Own>(Delivery::new(reply_to, PoolResponse::Accepted { job }));
578 }
579
580 fn submit_to(
581 &mut self,
582 target: A::Nonce,
583 job: JobId,
584 payload: J,
585 reply_to: Recipient<D>,
586 actions: &mut PoolActions<A, D, J, R, C>,
587 ) -> Admission {
588 let Some(slot) = self.slots.iter().find(|slot| slot.nonce == target) else {
589 actions
590 .sends
591 .behavior
592 .send::<Delivery<D>, Own>(Delivery::new(
593 reply_to,
594 PoolResponse::Rejected {
595 job,
596 payload,
597 reason: PoolRejection::AffinityUnavailable,
598 },
599 ));
600 return Admission::Rejected;
601 };
602 if matches!(slot.state, SlotState::Retired { .. }) {
603 actions.sends.behavior.send::<_, Own>(Delivery::new(
604 reply_to,
605 PoolResponse::Rejected {
606 job,
607 payload,
608 reason: PoolRejection::AffinityUnavailable,
609 },
610 ));
611 return Admission::Rejected;
612 }
613 let can_dispatch = matches!(slot.state, SlotState::Idle);
614 if !can_dispatch && self.backlog.len() == self.backlog_capacity {
615 actions.sends.behavior.send::<_, Own>(Delivery::new(
616 reply_to,
617 PoolResponse::Rejected {
618 job,
619 payload,
620 reason: PoolRejection::BacklogFull,
621 },
622 ));
623 return Admission::Rejected;
624 }
625 let dispatch_payload = payload.clone();
626 self.backlog.push_back(QueuedJob {
627 accepted: AcceptedJob {
628 id: job,
629 payload,
630 reply_to,
631 interruption: None,
632 target: Some(target),
633 },
634 dispatch_payload,
635 });
636 actions
637 .sends
638 .behavior
639 .send::<_, Own>(Delivery::new(reply_to, PoolResponse::Accepted { job }));
640 Admission::Accepted
641 }
642
643 fn complete(
644 &mut self,
645 worker: A::Nonce,
646 assignment: AssignmentId,
647 result: R,
648 actions: &mut PoolActions<A, D, J, R, C>,
649 ) -> Result<(), PoolError<A::Nonce>> {
650 let position = self.slot_position(worker)?;
651 let phase = self
652 .worker_phase(worker)
653 .expect("position proves the slot exists");
654 let SlotState::Assigned {
655 assignment: expected,
656 ..
657 } = &self.slots[position].state
658 else {
659 return Err(PoolError::CompletionForUnavailableWorker { worker, phase });
660 };
661 if *expected != assignment {
662 return Err(PoolError::StaleCompletion {
663 worker,
664 expected: *expected,
665 received: assignment,
666 });
667 }
668 let SlotState::Assigned { job, .. } =
669 core::mem::replace(&mut self.slots[position].state, SlotState::Idle)
670 else {
671 unreachable!("the state was proven assigned")
672 };
673 actions.sends.behavior.send::<_, Own>(Delivery::new(
674 job.reply_to,
675 PoolResponse::Completed {
676 job: job.id,
677 result,
678 },
679 ));
680 Ok(())
681 }
682
683 fn worker_stopped(
684 &mut self,
685 stopped: &WorkerStopped<A>,
686 responses: &mut Vec<Delivery<D>>,
687 ) -> Result<(), PoolError<A::Nonce>> {
688 let position = self.slot_position(stopped.proxy)?;
689 let phase = self
690 .worker_phase(stopped.proxy)
691 .expect("position proves the slot exists");
692 if matches!(phase, WorkerPhase::Installing | WorkerPhase::Retired { .. }) {
693 return Err(PoolError::WorkerStoppedWhileUnavailable {
694 worker: stopped.proxy,
695 phase,
696 });
697 }
698 if self.interruption == InterruptionPolicy::Retry {
699 if let SlotState::Assigned { job, .. } = &self.slots[position].state {
700 let dispatch_payload = job.payload.clone();
701 let SlotState::Assigned { mut job, .. } =
702 core::mem::replace(&mut self.slots[position].state, SlotState::Installing)
703 else {
704 unreachable!("the assigned state was matched before committing retry")
705 };
706 job.interruption = Some(PoolInterruption::WorkerStopped {
707 worker: stopped.proxy,
708 outcome: stopped.outcome,
709 });
710 self.backlog.push_front(QueuedJob {
711 accepted: job,
712 dispatch_payload,
713 });
714 } else {
715 self.slots[position].state = SlotState::Installing;
716 }
717 return Ok(());
718 }
719
720 let previous = core::mem::replace(&mut self.slots[position].state, SlotState::Installing);
721 if let SlotState::Assigned { job, .. } = previous {
722 responses.push(Delivery::new(
723 job.reply_to,
724 PoolResponse::Interrupted {
725 job: job.id,
726 payload: job.payload,
727 reason: PoolInterruption::WorkerStopped {
728 worker: stopped.proxy,
729 outcome: stopped.outcome,
730 },
731 },
732 ));
733 }
734 Ok(())
735 }
736
737 fn fail_backlog_if_irrecoverable(&mut self, actions: &mut PoolActions<A, D, J, R, C>) {
738 if self
739 .slots
740 .iter()
741 .any(|slot| !matches!(slot.state, SlotState::Retired { .. }))
742 {
743 return;
744 }
745 for queued in self.backlog.drain(..) {
746 let job = queued.accepted;
747 actions.sends.behavior.send::<_, Own>(Delivery::new(
748 job.reply_to,
749 PoolResponse::Interrupted {
750 job: job.id,
751 payload: job.payload,
752 reason: job
753 .interruption
754 .unwrap_or(PoolInterruption::NoRecoverableWorkers),
755 },
756 ));
757 }
758 }
759
760 fn fail_jobs_for_retired_slot(
761 &mut self,
762 worker: A::Nonce,
763 reason: WorkerRetirement,
764 actions: &mut PoolActions<A, D, J, R, C>,
765 ) {
766 let mut retained = VecDeque::with_capacity(self.backlog.len());
767 while let Some(queued) = self.backlog.pop_front() {
768 if queued.accepted.target == Some(worker) {
769 let job = queued.accepted;
770 actions.sends.behavior.send::<_, Own>(Delivery::new(
771 job.reply_to,
772 PoolResponse::Interrupted {
773 job: job.id,
774 payload: job.payload,
775 reason: job
776 .interruption
777 .unwrap_or(PoolInterruption::AffinityRetired { worker, reason }),
778 },
779 ));
780 } else {
781 retained.push_back(queued);
782 }
783 }
784 self.backlog = retained;
785 }
786
787 fn creation_resolved(
788 &mut self,
789 resolved: &WorkerCreationResolved<A::Nonce>,
790 ) -> Result<(), PoolError<A::Nonce>> {
791 let position = self.slot_position(resolved.proxy)?;
792 let phase = self
793 .worker_phase(resolved.proxy)
794 .expect("position proves the slot exists");
795 if !matches!(phase, WorkerPhase::Installing) {
796 return Err(PoolError::CreationResolvedWhileUnavailable {
797 worker: resolved.proxy,
798 phase,
799 });
800 }
801 self.slots[position].state = match resolved.result {
802 Ok(()) => SlotState::Idle,
803 Err(rejection) => SlotState::Retired {
804 reason: WorkerRetirement::CreationRejected(rejection),
805 },
806 };
807 Ok(())
808 }
809
810 fn dispatch(&mut self, actions: &mut PoolActions<A, D, J, R, C>) {
811 let mut selected_jobs = Vec::new();
812 let mut plan = Vec::new();
813 for (slot_position, slot) in self.slots.iter().enumerate() {
814 if !matches!(slot.state, SlotState::Idle) {
815 continue;
816 }
817 let Some(job_position) = self.backlog.iter().enumerate().find_map(|(position, job)| {
818 (!selected_jobs.contains(&position)
819 && job
820 .accepted
821 .target
822 .is_none_or(|target| target == slot.nonce))
823 .then_some(position)
824 }) else {
825 continue;
826 };
827 selected_jobs.push(job_position);
828 plan.push(PlannedDispatch {
829 slot_position,
830 job_position,
831 });
832 }
833 let count =
834 u64::try_from(plan.len()).expect("a pool cannot contain more than u64::MAX slots");
835 let next_assignment = self
836 .next_assignment
837 .checked_add(count)
838 .expect("pool assignment identifiers exhausted");
839
840 let mut selected_by_position = BTreeMap::new();
841 for planned in plan {
842 selected_by_position.insert(planned.job_position, planned.slot_position);
843 }
844 let mut selected_by_slot: Vec<Option<QueuedJob<A, D, J, R>>> =
845 std::iter::repeat_with(|| None)
846 .take(self.slots.len())
847 .collect();
848 let mut remaining = VecDeque::new();
849 for (position, queued) in self.backlog.drain(..).enumerate() {
850 if let Some(slot_position) = selected_by_position.remove(&position) {
851 selected_by_slot[slot_position] = Some(queued);
852 } else {
853 remaining.push_back(queued);
854 }
855 }
856 self.backlog = remaining;
857
858 for (offset, (slot_position, queued)) in selected_by_slot
859 .into_iter()
860 .enumerate()
861 .filter_map(|(slot_position, queued)| queued.map(|queued| (slot_position, queued)))
862 .enumerate()
863 {
864 let payload = queued.dispatch_payload;
865 let job = queued.accepted;
866 let assignment = AssignmentId(
867 self.next_assignment
868 + u64::try_from(offset).expect("offset is bounded by the checked plan length"),
869 );
870 let nonce = self.slots[slot_position].nonce;
871 let job_id = job.id;
872 self.slots[slot_position].state = SlotState::Assigned { assignment, job };
873 actions
874 .sends
875 .behavior
876 .send::<Delivery<Proxy<C>>, Own>(Delivery::new(
877 Recipient::child(nonce),
878 ProxyCommand::Forward(PoolAssignment {
879 assignment,
880 job: job_id,
881 payload,
882 }),
883 ));
884 }
885 self.next_assignment = next_assignment;
886 }
887}
888
889impl<A, D, J, R, C> Behavior for WorkerPool<A, D, J, R, C>
890where
891 A: Address,
892 A::Nonce: From<u64>,
893 D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>,
894 J: Clone,
895 C: Behavior<Addr = A, Msg = PoolAssignment<J>, Ph = Never>,
896{
897 type Addr = A;
898 type Msg = PoolMessage<A, D, J, R>;
899 type Event = PoolEvent<A, D, J, R>;
900 type Sends = PoolSends<A, D, J, R, C>;
901 type Ph = Never;
902 type Error = PoolError<A::Nonce>;
903 type Birth = Births<Proxy<C>>;
904
905 fn init(&mut self) -> crate::BehaviorActed<Self> {
906 match self.supervisor.init() {
907 Ok(actions) => Ok(actions),
908 Err(never) => match never {},
909 }
910 }
911
912 fn transition(&mut self, event: Self::Event) -> crate::BehaviorActed<Self> {
913 match event {
914 SupervisionEvent::Inner(User {
915 message:
916 PoolMessage::Submit {
917 job,
918 payload,
919 reply_to,
920 },
921 ..
922 }) => {
923 let mut actions = Actions::cont();
924 self.submit(job, payload, reply_to, &mut actions);
925 self.dispatch(&mut actions);
926 Ok(actions)
927 }
928 SupervisionEvent::Inner(User {
929 message:
930 PoolMessage::Completed {
931 worker,
932 assignment,
933 result,
934 },
935 ..
936 }) => {
937 let mut actions = Actions::cont();
938 self.complete(worker, assignment, result, &mut actions)?;
939 self.dispatch(&mut actions);
940 Ok(actions)
941 }
942 SupervisionEvent::WorkerStopped(stopped) => {
943 let proxy = stopped.proxy;
944 let mut responses = Vec::new();
945 self.worker_stopped(&stopped, &mut responses)?;
946 let mut actions =
947 self.supervisor_transition(SupervisionEvent::WorkerStopped(stopped));
948 actions.sends.behavior.responses.extend(responses);
949 let replacement_requested = actions
950 .sends
951 .replacement_commands
952 .iter()
953 .any(|delivery| delivery.to.is_child(proxy));
954 if !replacement_requested {
955 let position = self.slot_position(proxy)?;
956 let reason = WorkerRetirement::ReplacementUnavailable;
957 self.slots[position].state = SlotState::Retired { reason };
958 self.fail_jobs_for_retired_slot(proxy, reason, &mut actions);
959 }
960 self.dispatch(&mut actions);
961 self.fail_backlog_if_irrecoverable(&mut actions);
962 Ok(actions)
963 }
964 SupervisionEvent::WorkerCreationResolved(resolved) => {
965 let proxy = resolved.proxy;
966 self.creation_resolved(&resolved)?;
967 let mut actions =
968 self.supervisor_transition(SupervisionEvent::WorkerCreationResolved(resolved));
969 if let Some(WorkerPhase::Retired { reason }) = self.worker_phase(proxy) {
970 self.fail_jobs_for_retired_slot(proxy, reason, &mut actions);
971 }
972 self.dispatch(&mut actions);
973 self.fail_backlog_if_irrecoverable(&mut actions);
974 Ok(actions)
975 }
976 SupervisionEvent::ChildStopped(stopped) => {
977 Ok(self.supervisor_transition(SupervisionEvent::ChildStopped(stopped)))
978 }
979 SupervisionEvent::CreationResolved(resolved) => {
980 Ok(self.supervisor_transition(SupervisionEvent::CreationResolved(resolved)))
981 }
982 }
983 }
984}
985
986#[cfg(test)]
987#[allow(
988 clippy::items_after_test_module,
989 reason = "the local fixed-pool regression sits beside that implementation"
990)]
991mod tests {
992 use super::*;
993 use crate::{MailAddr, NoBirths};
994
995 struct TestReply;
996
997 impl Behavior for TestReply {
998 type Addr = MailAddr;
999 type Msg = PoolResponse<u8, (), MailAddr>;
1000 type Event = User<MailAddr, Self::Msg>;
1001 type Sends = Vec<Never>;
1002 type Ph = Never;
1003 type Error = Never;
1004 type Birth = NoBirths;
1005
1006 fn init(&mut self) -> crate::BehaviorActed<Self> {
1007 Ok(Actions::cont())
1008 }
1009
1010 fn transition(&mut self, _: Self::Event) -> crate::BehaviorActed<Self> {
1011 Ok(Actions::cont())
1012 }
1013 }
1014
1015 #[derive(Clone, Copy)]
1016 struct TestWorker;
1017
1018 impl Behavior for TestWorker {
1019 type Addr = MailAddr;
1020 type Msg = PoolAssignment<u8>;
1021 type Event = User<MailAddr, PoolAssignment<u8>>;
1022 type Sends = Vec<Never>;
1023 type Ph = Never;
1024 type Error = Never;
1025 type Birth = NoBirths;
1026
1027 fn init(&mut self) -> crate::BehaviorActed<Self> {
1028 Ok(Actions::cont())
1029 }
1030
1031 fn transition(&mut self, _: Self::Event) -> crate::BehaviorActed<Self> {
1032 Ok(Actions::cont())
1033 }
1034 }
1035
1036 fn test_worker(_: usize) -> TestWorker {
1037 TestWorker
1038 }
1039
1040 #[test]
1041 fn one_dispatch_batch_preserves_fifo_jobs_across_index_removal() {
1042 let mut pool = WorkerPool::new(
1043 |index| u64::try_from(index).unwrap(),
1044 2,
1045 test_worker,
1046 3,
1047 InterruptionPolicy::Fail,
1048 RestartPolicy::Permanent,
1049 1,
1050 Duration::from_secs(1),
1051 )
1052 .unwrap();
1053 pool.init().unwrap();
1054
1055 for job in 1..=3 {
1056 pool.transition(SupervisionEvent::Inner(User::new(
1057 MailAddr(90),
1058 PoolMessage::Submit {
1059 job: JobId(job),
1060 payload: u8::try_from(job).unwrap(),
1061 reply_to: Recipient::global(MailAddr(91)),
1062 },
1063 )))
1064 .unwrap();
1065 }
1066 pool.slots[0].state = SlotState::Idle;
1067 pool.slots[1].state = SlotState::Idle;
1068
1069 let mut actions: PoolActions<MailAddr, TestReply, u8, (), TestWorker> = Actions::cont();
1070 pool.dispatch(&mut actions);
1071
1072 let assignments = &actions.sends.behavior.assignments;
1073 assert_eq!(assignments.len(), 2);
1074 for (index, expected_job) in [JobId(1), JobId(2)].into_iter().enumerate() {
1075 assert!(
1076 assignments[index]
1077 .to
1078 .is_child(u64::try_from(index).unwrap())
1079 );
1080 let ProxyCommand::Forward(assignment) = &assignments[index].message else {
1081 panic!("pool dispatches with Forward");
1082 };
1083 assert_eq!(
1084 assignment.assignment,
1085 AssignmentId(u64::try_from(index).unwrap())
1086 );
1087 assert_eq!(assignment.job, expected_job);
1088 }
1089 assert_eq!(pool.backlog.len(), 1);
1090 assert_eq!(pool.backlog[0].accepted.id, JobId(3));
1091 }
1092}
1093
1094pub struct KeyedWorkerPool<A: Address, D, K, J, R, C, S>
1139where
1140 A::Nonce: From<u64>,
1141 D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>,
1142 K: Eq,
1143 C: Behavior<Addr = A, Msg = PoolAssignment<J>, Ph = Never>,
1144 S: AffinitySelector<K, A::Nonce>,
1145{
1146 pool: WorkerPool<A, D, J, R, C>,
1147 bindings: Vec<(K, A::Nonce)>,
1148 selector: S,
1149}
1150
1151impl<A, D, K, J, R, C, S> KeyedWorkerPool<A, D, K, J, R, C, S>
1152where
1153 A: Address,
1154 A::Nonce: From<u64>,
1155 D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>,
1156 K: Eq,
1157 C: Behavior<Addr = A, Msg = PoolAssignment<J>, Ph = Never>,
1158 S: AffinitySelector<K, A::Nonce>,
1159{
1160 #[allow(
1170 clippy::too_many_arguments,
1171 reason = "the arguments expose the complete pool and affinity policy"
1172 )]
1173 pub fn new(
1174 nonces: fn(usize) -> A::Nonce,
1175 count: usize,
1176 build: fn(usize) -> C,
1177 backlog_capacity: usize,
1178 interruption: InterruptionPolicy,
1179 restart_policy: RestartPolicy,
1180 max_restarts: u32,
1181 restart_window: Duration,
1182 selector: S,
1183 ) -> Result<Self, PoolConfigError<A::Nonce>> {
1184 Ok(Self {
1185 pool: WorkerPool::new(
1186 nonces,
1187 count,
1188 build,
1189 backlog_capacity,
1190 interruption,
1191 restart_policy,
1192 max_restarts,
1193 restart_window,
1194 )?,
1195 bindings: Vec::new(),
1196 selector,
1197 })
1198 }
1199
1200 #[must_use]
1202 pub fn affinity(&self, key: &K) -> Option<A::Nonce> {
1203 self.bindings
1204 .iter()
1205 .find_map(|(bound, worker)| (bound == key).then_some(*worker))
1206 }
1207
1208 #[must_use]
1209 pub fn backlog_len(&self) -> usize {
1210 self.pool.backlog_len()
1211 }
1212
1213 #[must_use]
1214 pub fn worker_phase(&self, worker: A::Nonce) -> Option<WorkerPhase> {
1215 self.pool.worker_phase(worker)
1216 }
1217
1218 fn rebalance(&mut self, key: K, worker: A::Nonce) -> Result<(), PoolError<A::Nonce>> {
1219 let position = self.pool.slot_position(worker)?;
1220 if let SlotState::Retired { reason } = self.pool.slots[position].state {
1221 return Err(PoolError::RebalanceToRetiredWorker { worker, reason });
1222 }
1223 if let Some((_, bound)) = self.bindings.iter_mut().find(|(bound, _)| *bound == key) {
1224 *bound = worker;
1225 } else {
1226 self.bindings.push((key, worker));
1227 }
1228 Ok(())
1229 }
1230}
1231
1232impl<A, D, K, J, R, C, S> Behavior for KeyedWorkerPool<A, D, K, J, R, C, S>
1233where
1234 A: Address,
1235 A::Nonce: From<u64>,
1236 D: Behavior<Addr = A, Msg = PoolResponse<J, R, A>>,
1237 K: Eq,
1238 J: Clone,
1239 C: Behavior<Addr = A, Msg = PoolAssignment<J>, Ph = Never>,
1240 S: AffinitySelector<K, A::Nonce>,
1241{
1242 type Addr = A;
1243 type Msg = KeyedPoolMessage<A, D, K, J, R>;
1244 type Event = KeyedPoolEvent<A, D, K, J, R>;
1245 type Sends = PoolSends<A, D, J, R, C>;
1246 type Ph = Never;
1247 type Error = PoolError<A::Nonce>;
1248 type Birth = Births<Proxy<C>>;
1249
1250 fn init(&mut self) -> crate::BehaviorActed<Self> {
1251 self.pool.init()
1252 }
1253
1254 fn transition(&mut self, event: Self::Event) -> crate::BehaviorActed<Self> {
1255 match event {
1256 SupervisionEvent::Inner(User {
1257 message:
1258 KeyedPoolMessage::Submit {
1259 key,
1260 job,
1261 payload,
1262 reply_to,
1263 },
1264 ..
1265 }) => {
1266 let existing = self.affinity(&key);
1267 let target = existing.unwrap_or_else(|| self.selector.select(&key));
1268 let mut actions = Actions::cont();
1269 let admission = self
1270 .pool
1271 .submit_to(target, job, payload, reply_to, &mut actions);
1272 match (admission, existing) {
1273 (Admission::Accepted, None) => self.bindings.push((key, target)),
1274 (Admission::Accepted | Admission::Rejected, Some(_))
1275 | (Admission::Rejected, None) => {}
1276 }
1277 self.pool.dispatch(&mut actions);
1278 Ok(actions)
1279 }
1280 SupervisionEvent::Inner(User {
1281 message:
1282 KeyedPoolMessage::Completed {
1283 worker,
1284 assignment,
1285 result,
1286 },
1287 ..
1288 }) => {
1289 let mut actions = Actions::cont();
1290 self.pool
1291 .complete(worker, assignment, result, &mut actions)?;
1292 self.pool.dispatch(&mut actions);
1293 Ok(actions)
1294 }
1295 SupervisionEvent::Inner(User {
1296 message: KeyedPoolMessage::Rebalance { key, worker },
1297 ..
1298 }) => {
1299 self.rebalance(key, worker)?;
1300 Ok(Actions::cont())
1301 }
1302 SupervisionEvent::WorkerStopped(stopped) => self
1303 .pool
1304 .transition(SupervisionEvent::WorkerStopped(stopped)),
1305 SupervisionEvent::WorkerCreationResolved(resolved) => self
1306 .pool
1307 .transition(SupervisionEvent::WorkerCreationResolved(resolved)),
1308 SupervisionEvent::ChildStopped(stopped) => self
1309 .pool
1310 .transition(SupervisionEvent::ChildStopped(stopped)),
1311 SupervisionEvent::CreationResolved(resolved) => self
1312 .pool
1313 .transition(SupervisionEvent::CreationResolved(resolved)),
1314 }
1315 }
1316}