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: Address, J, R> {
40 Submit {
41 job: JobId,
42 payload: J,
43 reply_to: Recipient<A, PoolResponse<J, R, A>>,
44 },
45 Completed {
46 worker: A::Nonce,
47 assignment: AssignmentId,
48 result: R,
49 },
50}
51
52#[derive(Clone, PartialEq, Eq)]
58pub enum KeyedPoolMessage<A: Address, K, J, R> {
59 Submit {
60 key: K,
61 job: JobId,
62 payload: J,
63 reply_to: Recipient<A, PoolResponse<J, R, A>>,
64 },
65 Completed {
66 worker: A::Nonce,
67 assignment: AssignmentId,
68 result: R,
69 },
70 Rebalance {
71 key: K,
72 worker: A::Nonce,
73 },
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum PoolRejection {
79 BacklogFull,
80 AffinityUnavailable,
82}
83
84#[derive(Clone, PartialEq, Eq)]
86pub enum PoolInterruption<A: Address> {
87 WorkerStopped {
88 worker: A::Nonce,
89 outcome: Result<Exit<A>, Crash>,
90 },
91 NoRecoverableWorkers,
92 AffinityRetired {
94 worker: A::Nonce,
95 reason: WorkerRetirement,
96 },
97}
98
99#[derive(Clone, PartialEq, Eq)]
101pub enum PoolResponse<J, R, A: Address> {
102 Accepted {
103 job: JobId,
104 },
105 Rejected {
106 job: JobId,
107 payload: J,
108 reason: PoolRejection,
109 },
110 Completed {
111 job: JobId,
112 result: R,
113 },
114 Interrupted {
115 job: JobId,
116 payload: J,
117 reason: PoolInterruption<A>,
118 },
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub enum InterruptionPolicy {
124 Fail,
126 Retry,
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub enum WorkerPhase {
133 Installing,
134 Idle,
135 Assigned {
136 assignment: AssignmentId,
137 job: JobId,
138 },
139 Retired {
140 reason: WorkerRetirement,
141 },
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum WorkerRetirement {
147 CreationRejected(CreationRejection),
148 ReplacementUnavailable,
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub enum PoolConfigError<N> {
154 NoWorkers,
156 DuplicateWorker(N),
158}
159
160pub trait AffinitySelector<K, N> {
162 fn select(&self, key: &K) -> N;
164}
165
166impl<K, N, F> AffinitySelector<K, N> for F
167where
168 F: Fn(&K) -> N,
169{
170 fn select(&self, key: &K) -> N {
171 self(key)
172 }
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177pub enum PoolError<N> {
178 UnknownWorker(N),
179 CompletionForUnavailableWorker {
180 worker: N,
181 phase: WorkerPhase,
182 },
183 StaleCompletion {
184 worker: N,
185 expected: AssignmentId,
186 received: AssignmentId,
187 },
188 WorkerStoppedWhileUnavailable {
189 worker: N,
190 phase: WorkerPhase,
191 },
192 CreationResolvedWhileUnavailable {
193 worker: N,
194 phase: WorkerPhase,
195 },
196 RebalanceToRetiredWorker {
197 worker: N,
198 reason: WorkerRetirement,
199 },
200}
201
202struct AcceptedJob<A: Address, J, R> {
203 id: JobId,
204 payload: J,
205 reply_to: Recipient<A, PoolResponse<J, R, A>>,
206 interruption: Option<PoolInterruption<A>>,
207 target: Option<A::Nonce>,
208}
209
210struct QueuedJob<A: Address, J, R> {
211 accepted: AcceptedJob<A, J, R>,
212 dispatch_payload: J,
213}
214
215enum SlotState<A: Address, J, R> {
216 Installing,
217 Idle,
218 Assigned {
219 assignment: AssignmentId,
220 job: AcceptedJob<A, J, R>,
221 },
222 Retired {
223 reason: WorkerRetirement,
224 },
225}
226
227struct Slot<A: Address, J, R> {
228 nonce: A::Nonce,
229 state: SlotState<A, J, R>,
230}
231
232struct PlannedDispatch {
233 slot_position: usize,
234 job_position: usize,
235}
236
237enum Admission {
238 Accepted,
239 Rejected,
240}
241
242pub type PoolEvent<A, J, R> = SupervisionEvent<User<A, PoolMessage<A, J, R>>>;
244
245pub type KeyedPoolEvent<A, K, J, R> = SupervisionEvent<User<A, KeyedPoolMessage<A, K, J, R>>>;
247
248pub struct PoolBehaviorSends<A: Address, J, R, C: Behavior<Addr = A>> {
250 pub responses: Vec<Delivery<A, PoolResponse<J, R, A>>>,
252 pub assignments: Vec<Delivery<A, ProxyCommand<C>>>,
254}
255
256impl<A: Address, J, R, C: Behavior<Addr = A>> SendAlgebra for PoolBehaviorSends<A, J, R, C> {
257 fn empty() -> Self {
258 Self {
259 responses: Vec::new(),
260 assignments: Vec::new(),
261 }
262 }
263
264 fn append(&mut self, mut other: Self) {
265 self.responses.append(&mut other.responses);
266 self.assignments.append(&mut other.assignments);
267 }
268}
269
270impl<A: Address, J, R, C: Behavior<Addr = A>> SendInput<Delivery<A, PoolResponse<J, R, A>>, Own>
271 for PoolBehaviorSends<A, J, R, C>
272{
273 fn emit(&mut self, input: Delivery<A, PoolResponse<J, R, A>>) {
274 self.responses.push(input);
275 }
276}
277
278impl<A: Address, J, R, C: Behavior<Addr = A>> SendInput<Delivery<A, ProxyCommand<C>>, Own>
279 for PoolBehaviorSends<A, J, R, C>
280{
281 fn emit(&mut self, input: Delivery<A, ProxyCommand<C>>) {
282 self.assignments.push(input);
283 }
284}
285
286type KernelSends<A, J, R, C> = PoolBehaviorSends<A, J, R, C>;
287
288pub type PoolSends<A, J, R, C> = SupervisorSends<A, KernelSends<A, J, R, C>, C>;
291
292pub type PoolActions<A, J, R, C> = Actions<A, Never, PoolSends<A, J, R, C>, Births<Proxy<C>>>;
294
295struct PoolKernel<A: Address, J, R, C>(PhantomData<fn(A, J, R, C)>);
296
297impl<A: Address, J, R, C> PoolKernel<A, J, R, C> {
298 const fn new() -> Self {
299 Self(PhantomData)
300 }
301}
302
303impl<A, J, R, C> Behavior for PoolKernel<A, J, R, C>
304where
305 A: Address,
306 C: Behavior<Addr = A, Msg = PoolAssignment<J>, Ph = Never>,
307{
308 type Addr = A;
309 type Msg = PoolMessage<A, J, R>;
310 type Event = User<A, PoolMessage<A, J, R>>;
311 type Sends = KernelSends<A, J, R, C>;
312 type Ph = Never;
313 type Error = Infallible;
314 type Birth = Births<C>;
315
316 fn init(&mut self) -> crate::BehaviorActed<Self> {
317 Ok(Actions::cont())
318 }
319
320 fn transition(&mut self, _event: Self::Event) -> crate::BehaviorActed<Self> {
321 Ok(Actions::cont())
322 }
323}
324
325type PoolSupervisor<A, J, R, C> = Supervisor<PoolKernel<A, J, R, C>, C>;
326
327pub struct WorkerPool<A: Address, J, R, C>
366where
367 C: Behavior<Addr = A, Msg = PoolAssignment<J>, Ph = Never>,
368{
369 supervisor: PoolSupervisor<A, J, R, C>,
370 slots: Vec<Slot<A, J, R>>,
371 backlog: VecDeque<QueuedJob<A, J, R>>,
372 backlog_capacity: usize,
373 next_assignment: u64,
374 interruption: InterruptionPolicy,
375}
376
377impl<A, J, R, C> WorkerPool<A, J, R, C>
378where
379 A: Address,
380 C: Behavior<Addr = A, Msg = PoolAssignment<J>, Ph = Never>,
381{
382 #[allow(
391 clippy::too_many_arguments,
392 reason = "the arguments expose the complete pool policy"
393 )]
394 pub fn new(
395 nonces: fn(usize) -> A::Nonce,
396 count: usize,
397 build: fn(usize) -> C,
398 backlog_capacity: usize,
399 interruption: InterruptionPolicy,
400 restart_policy: RestartPolicy,
401 max_restarts: u32,
402 restart_window: Duration,
403 ) -> Result<Self, PoolConfigError<A::Nonce>> {
404 if count == 0 {
405 return Err(PoolConfigError::NoWorkers);
406 }
407 let mut slots = Vec::with_capacity(count);
408 for index in 0..count {
409 let nonce = nonces(index);
410 if slots.iter().any(|slot: &Slot<A, J, R>| slot.nonce == nonce) {
411 return Err(PoolConfigError::DuplicateWorker(nonce));
412 }
413 slots.push(Slot {
414 nonce,
415 state: SlotState::Installing,
416 });
417 }
418 Ok(Self {
419 supervisor: Supervisor::new(
420 PoolKernel::new(),
421 nonces,
422 count,
423 build,
424 Strategy::OneForOne,
425 restart_policy,
426 max_restarts,
427 restart_window,
428 ),
429 slots,
430 backlog: VecDeque::new(),
431 backlog_capacity,
432 next_assignment: 0,
433 interruption,
434 })
435 }
436
437 #[must_use]
438 pub fn backlog_len(&self) -> usize {
439 self.backlog.len()
440 }
441
442 #[must_use]
443 pub fn worker_phase(&self, worker: A::Nonce) -> Option<WorkerPhase> {
444 self.slots
445 .iter()
446 .find(|slot| slot.nonce == worker)
447 .map(|slot| match &slot.state {
448 SlotState::Installing => WorkerPhase::Installing,
449 SlotState::Idle => WorkerPhase::Idle,
450 SlotState::Assigned { assignment, job } => WorkerPhase::Assigned {
451 assignment: *assignment,
452 job: job.id,
453 },
454 SlotState::Retired { reason } => WorkerPhase::Retired { reason: *reason },
455 })
456 }
457
458 fn slot_position(&self, worker: A::Nonce) -> Result<usize, PoolError<A::Nonce>> {
459 self.slots
460 .iter()
461 .position(|slot| slot.nonce == worker)
462 .ok_or(PoolError::UnknownWorker(worker))
463 }
464}
465
466impl<A, J, R, C> WorkerPool<A, J, R, C>
467where
468 A: Address,
469 A::Nonce: From<u64>,
470 J: Clone,
471 C: Behavior<Addr = A, Msg = PoolAssignment<J>, Ph = Never>,
472{
473 fn supervisor_transition(&mut self, event: PoolEvent<A, J, R>) -> PoolActions<A, J, R, C> {
474 match delegate_transition(&mut self.supervisor, event) {
475 Ok(actions) => actions,
476 Err(never) => match never {},
477 }
478 }
479
480 fn submit(
481 &mut self,
482 job: JobId,
483 payload: J,
484 reply_to: Recipient<A, PoolResponse<J, R, A>>,
485 actions: &mut PoolActions<A, J, R, C>,
486 ) {
487 let can_dispatch = self
488 .slots
489 .iter()
490 .any(|slot| matches!(slot.state, SlotState::Idle));
491 if !can_dispatch && self.backlog.len() == self.backlog_capacity {
492 actions.sends.behavior.send::<_, Own>(Delivery::new(
493 reply_to,
494 PoolResponse::Rejected {
495 job,
496 payload,
497 reason: PoolRejection::BacklogFull,
498 },
499 ));
500 return;
501 }
502 let dispatch_payload = payload.clone();
503 self.backlog.push_back(QueuedJob {
504 accepted: AcceptedJob {
505 id: job,
506 payload,
507 reply_to,
508 interruption: None,
509 target: None,
510 },
511 dispatch_payload,
512 });
513 actions
514 .sends
515 .behavior
516 .send::<_, Own>(Delivery::new(reply_to, PoolResponse::Accepted { job }));
517 }
518
519 fn submit_to(
520 &mut self,
521 target: A::Nonce,
522 job: JobId,
523 payload: J,
524 reply_to: Recipient<A, PoolResponse<J, R, A>>,
525 actions: &mut PoolActions<A, J, R, C>,
526 ) -> Admission {
527 let Some(slot) = self.slots.iter().find(|slot| slot.nonce == target) else {
528 actions.sends.behavior.send::<_, Own>(Delivery::new(
529 reply_to,
530 PoolResponse::Rejected {
531 job,
532 payload,
533 reason: PoolRejection::AffinityUnavailable,
534 },
535 ));
536 return Admission::Rejected;
537 };
538 if matches!(slot.state, SlotState::Retired { .. }) {
539 actions.sends.behavior.send::<_, Own>(Delivery::new(
540 reply_to,
541 PoolResponse::Rejected {
542 job,
543 payload,
544 reason: PoolRejection::AffinityUnavailable,
545 },
546 ));
547 return Admission::Rejected;
548 }
549 let can_dispatch = matches!(slot.state, SlotState::Idle);
550 if !can_dispatch && self.backlog.len() == self.backlog_capacity {
551 actions.sends.behavior.send::<_, Own>(Delivery::new(
552 reply_to,
553 PoolResponse::Rejected {
554 job,
555 payload,
556 reason: PoolRejection::BacklogFull,
557 },
558 ));
559 return Admission::Rejected;
560 }
561 let dispatch_payload = payload.clone();
562 self.backlog.push_back(QueuedJob {
563 accepted: AcceptedJob {
564 id: job,
565 payload,
566 reply_to,
567 interruption: None,
568 target: Some(target),
569 },
570 dispatch_payload,
571 });
572 actions
573 .sends
574 .behavior
575 .send::<_, Own>(Delivery::new(reply_to, PoolResponse::Accepted { job }));
576 Admission::Accepted
577 }
578
579 fn complete(
580 &mut self,
581 worker: A::Nonce,
582 assignment: AssignmentId,
583 result: R,
584 actions: &mut PoolActions<A, J, R, C>,
585 ) -> Result<(), PoolError<A::Nonce>> {
586 let position = self.slot_position(worker)?;
587 let phase = self
588 .worker_phase(worker)
589 .expect("position proves the slot exists");
590 let SlotState::Assigned {
591 assignment: expected,
592 ..
593 } = &self.slots[position].state
594 else {
595 return Err(PoolError::CompletionForUnavailableWorker { worker, phase });
596 };
597 if *expected != assignment {
598 return Err(PoolError::StaleCompletion {
599 worker,
600 expected: *expected,
601 received: assignment,
602 });
603 }
604 let SlotState::Assigned { job, .. } =
605 core::mem::replace(&mut self.slots[position].state, SlotState::Idle)
606 else {
607 unreachable!("the state was proven assigned")
608 };
609 actions.sends.behavior.send::<_, Own>(Delivery::new(
610 job.reply_to,
611 PoolResponse::Completed {
612 job: job.id,
613 result,
614 },
615 ));
616 Ok(())
617 }
618
619 fn worker_stopped(
620 &mut self,
621 stopped: &WorkerStopped<A>,
622 responses: &mut Vec<Delivery<A, PoolResponse<J, R, A>>>,
623 ) -> Result<(), PoolError<A::Nonce>> {
624 let position = self.slot_position(stopped.proxy)?;
625 let phase = self
626 .worker_phase(stopped.proxy)
627 .expect("position proves the slot exists");
628 if matches!(phase, WorkerPhase::Installing | WorkerPhase::Retired { .. }) {
629 return Err(PoolError::WorkerStoppedWhileUnavailable {
630 worker: stopped.proxy,
631 phase,
632 });
633 }
634 if self.interruption == InterruptionPolicy::Retry {
635 if let SlotState::Assigned { job, .. } = &self.slots[position].state {
636 let dispatch_payload = job.payload.clone();
637 let SlotState::Assigned { mut job, .. } =
638 core::mem::replace(&mut self.slots[position].state, SlotState::Installing)
639 else {
640 unreachable!("the assigned state was matched before committing retry")
641 };
642 job.interruption = Some(PoolInterruption::WorkerStopped {
643 worker: stopped.proxy,
644 outcome: stopped.outcome,
645 });
646 self.backlog.push_front(QueuedJob {
647 accepted: job,
648 dispatch_payload,
649 });
650 } else {
651 self.slots[position].state = SlotState::Installing;
652 }
653 return Ok(());
654 }
655
656 let previous = core::mem::replace(&mut self.slots[position].state, SlotState::Installing);
657 if let SlotState::Assigned { job, .. } = previous {
658 responses.push(Delivery::new(
659 job.reply_to,
660 PoolResponse::Interrupted {
661 job: job.id,
662 payload: job.payload,
663 reason: PoolInterruption::WorkerStopped {
664 worker: stopped.proxy,
665 outcome: stopped.outcome,
666 },
667 },
668 ));
669 }
670 Ok(())
671 }
672
673 fn fail_backlog_if_irrecoverable(&mut self, actions: &mut PoolActions<A, J, R, C>) {
674 if self
675 .slots
676 .iter()
677 .any(|slot| !matches!(slot.state, SlotState::Retired { .. }))
678 {
679 return;
680 }
681 for queued in self.backlog.drain(..) {
682 let job = queued.accepted;
683 actions.sends.behavior.send::<_, Own>(Delivery::new(
684 job.reply_to,
685 PoolResponse::Interrupted {
686 job: job.id,
687 payload: job.payload,
688 reason: job
689 .interruption
690 .unwrap_or(PoolInterruption::NoRecoverableWorkers),
691 },
692 ));
693 }
694 }
695
696 fn fail_jobs_for_retired_slot(
697 &mut self,
698 worker: A::Nonce,
699 reason: WorkerRetirement,
700 actions: &mut PoolActions<A, J, R, C>,
701 ) {
702 let mut retained = VecDeque::with_capacity(self.backlog.len());
703 while let Some(queued) = self.backlog.pop_front() {
704 if queued.accepted.target == Some(worker) {
705 let job = queued.accepted;
706 actions.sends.behavior.send::<_, Own>(Delivery::new(
707 job.reply_to,
708 PoolResponse::Interrupted {
709 job: job.id,
710 payload: job.payload,
711 reason: job
712 .interruption
713 .unwrap_or(PoolInterruption::AffinityRetired { worker, reason }),
714 },
715 ));
716 } else {
717 retained.push_back(queued);
718 }
719 }
720 self.backlog = retained;
721 }
722
723 fn creation_resolved(
724 &mut self,
725 resolved: &WorkerCreationResolved<A::Nonce>,
726 ) -> Result<(), PoolError<A::Nonce>> {
727 let position = self.slot_position(resolved.proxy)?;
728 let phase = self
729 .worker_phase(resolved.proxy)
730 .expect("position proves the slot exists");
731 if !matches!(phase, WorkerPhase::Installing) {
732 return Err(PoolError::CreationResolvedWhileUnavailable {
733 worker: resolved.proxy,
734 phase,
735 });
736 }
737 self.slots[position].state = match resolved.result {
738 Ok(()) => SlotState::Idle,
739 Err(rejection) => SlotState::Retired {
740 reason: WorkerRetirement::CreationRejected(rejection),
741 },
742 };
743 Ok(())
744 }
745
746 fn dispatch(&mut self, actions: &mut PoolActions<A, J, R, C>) {
747 let mut selected_jobs = Vec::new();
748 let mut plan = Vec::new();
749 for (slot_position, slot) in self.slots.iter().enumerate() {
750 if !matches!(slot.state, SlotState::Idle) {
751 continue;
752 }
753 let Some(job_position) = self.backlog.iter().enumerate().find_map(|(position, job)| {
754 (!selected_jobs.contains(&position)
755 && job
756 .accepted
757 .target
758 .is_none_or(|target| target == slot.nonce))
759 .then_some(position)
760 }) else {
761 continue;
762 };
763 selected_jobs.push(job_position);
764 plan.push(PlannedDispatch {
765 slot_position,
766 job_position,
767 });
768 }
769 let count =
770 u64::try_from(plan.len()).expect("a pool cannot contain more than u64::MAX slots");
771 let next_assignment = self
772 .next_assignment
773 .checked_add(count)
774 .expect("pool assignment identifiers exhausted");
775
776 let mut selected_by_position = BTreeMap::new();
777 for planned in plan {
778 selected_by_position.insert(planned.job_position, planned.slot_position);
779 }
780 let mut selected_by_slot: Vec<Option<QueuedJob<A, J, R>>> = std::iter::repeat_with(|| None)
781 .take(self.slots.len())
782 .collect();
783 let mut remaining = VecDeque::new();
784 for (position, queued) in self.backlog.drain(..).enumerate() {
785 if let Some(slot_position) = selected_by_position.remove(&position) {
786 selected_by_slot[slot_position] = Some(queued);
787 } else {
788 remaining.push_back(queued);
789 }
790 }
791 self.backlog = remaining;
792
793 for (offset, (slot_position, queued)) in selected_by_slot
794 .into_iter()
795 .enumerate()
796 .filter_map(|(slot_position, queued)| queued.map(|queued| (slot_position, queued)))
797 .enumerate()
798 {
799 let payload = queued.dispatch_payload;
800 let job = queued.accepted;
801 let assignment = AssignmentId(
802 self.next_assignment
803 + u64::try_from(offset).expect("offset is bounded by the checked plan length"),
804 );
805 let nonce = self.slots[slot_position].nonce;
806 let job_id = job.id;
807 self.slots[slot_position].state = SlotState::Assigned { assignment, job };
808 actions.sends.behavior.send::<_, Own>(Delivery::new(
809 Recipient::child(nonce),
810 ProxyCommand::Forward(PoolAssignment {
811 assignment,
812 job: job_id,
813 payload,
814 }),
815 ));
816 }
817 self.next_assignment = next_assignment;
818 }
819}
820
821impl<A, J, R, C> Behavior for WorkerPool<A, J, R, C>
822where
823 A: Address,
824 A::Nonce: From<u64>,
825 J: Clone,
826 C: Behavior<Addr = A, Msg = PoolAssignment<J>, Ph = Never>,
827{
828 type Addr = A;
829 type Msg = PoolMessage<A, J, R>;
830 type Event = PoolEvent<A, J, R>;
831 type Sends = PoolSends<A, J, R, C>;
832 type Ph = Never;
833 type Error = PoolError<A::Nonce>;
834 type Birth = Births<Proxy<C>>;
835
836 fn init(&mut self) -> crate::BehaviorActed<Self> {
837 match self.supervisor.init() {
838 Ok(actions) => Ok(actions),
839 Err(never) => match never {},
840 }
841 }
842
843 fn transition(&mut self, event: Self::Event) -> crate::BehaviorActed<Self> {
844 match event {
845 SupervisionEvent::Inner(User {
846 message:
847 PoolMessage::Submit {
848 job,
849 payload,
850 reply_to,
851 },
852 ..
853 }) => {
854 let mut actions = Actions::cont();
855 self.submit(job, payload, reply_to, &mut actions);
856 self.dispatch(&mut actions);
857 Ok(actions)
858 }
859 SupervisionEvent::Inner(User {
860 message:
861 PoolMessage::Completed {
862 worker,
863 assignment,
864 result,
865 },
866 ..
867 }) => {
868 let mut actions = Actions::cont();
869 self.complete(worker, assignment, result, &mut actions)?;
870 self.dispatch(&mut actions);
871 Ok(actions)
872 }
873 SupervisionEvent::WorkerStopped(stopped) => {
874 let proxy = stopped.proxy;
875 let mut responses = Vec::new();
876 self.worker_stopped(&stopped, &mut responses)?;
877 let mut actions =
878 self.supervisor_transition(SupervisionEvent::WorkerStopped(stopped));
879 actions.sends.behavior.responses.extend(responses);
880 let replacement_requested = actions
881 .sends
882 .replacement_commands
883 .iter()
884 .any(|delivery| delivery.to.route() == crate::Route::Child(proxy));
885 if !replacement_requested {
886 let position = self.slot_position(proxy)?;
887 let reason = WorkerRetirement::ReplacementUnavailable;
888 self.slots[position].state = SlotState::Retired { reason };
889 self.fail_jobs_for_retired_slot(proxy, reason, &mut actions);
890 }
891 self.dispatch(&mut actions);
892 self.fail_backlog_if_irrecoverable(&mut actions);
893 Ok(actions)
894 }
895 SupervisionEvent::WorkerCreationResolved(resolved) => {
896 let proxy = resolved.proxy;
897 self.creation_resolved(&resolved)?;
898 let mut actions =
899 self.supervisor_transition(SupervisionEvent::WorkerCreationResolved(resolved));
900 if let Some(WorkerPhase::Retired { reason }) = self.worker_phase(proxy) {
901 self.fail_jobs_for_retired_slot(proxy, reason, &mut actions);
902 }
903 self.dispatch(&mut actions);
904 self.fail_backlog_if_irrecoverable(&mut actions);
905 Ok(actions)
906 }
907 SupervisionEvent::ChildStopped(stopped) => {
908 Ok(self.supervisor_transition(SupervisionEvent::ChildStopped(stopped)))
909 }
910 SupervisionEvent::CreationResolved(resolved) => {
911 Ok(self.supervisor_transition(SupervisionEvent::CreationResolved(resolved)))
912 }
913 }
914 }
915}
916
917#[cfg(test)]
918mod tests {
919 use super::*;
920 use crate::{MailAddr, NoBirths, Route};
921
922 #[derive(Clone, Copy)]
923 struct TestWorker;
924
925 impl Behavior for TestWorker {
926 type Addr = MailAddr;
927 type Msg = PoolAssignment<u8>;
928 type Event = User<MailAddr, PoolAssignment<u8>>;
929 type Sends = Vec<Delivery<MailAddr, Never>>;
930 type Ph = Never;
931 type Error = Never;
932 type Birth = NoBirths;
933
934 fn init(&mut self) -> crate::BehaviorActed<Self> {
935 Ok(Actions::cont())
936 }
937
938 fn transition(&mut self, _: Self::Event) -> crate::BehaviorActed<Self> {
939 Ok(Actions::cont())
940 }
941 }
942
943 fn test_worker(_: usize) -> TestWorker {
944 TestWorker
945 }
946
947 #[test]
948 fn one_dispatch_batch_preserves_fifo_jobs_across_index_removal() {
949 let mut pool = WorkerPool::new(
950 |index| u64::try_from(index).unwrap(),
951 2,
952 test_worker,
953 3,
954 InterruptionPolicy::Fail,
955 RestartPolicy::Permanent,
956 1,
957 Duration::from_secs(1),
958 )
959 .unwrap();
960 pool.init().unwrap();
961
962 for job in 1..=3 {
963 pool.transition(SupervisionEvent::Inner(User::new(
964 MailAddr(90),
965 PoolMessage::Submit {
966 job: JobId(job),
967 payload: u8::try_from(job).unwrap(),
968 reply_to: Recipient::global(MailAddr(91)),
969 },
970 )))
971 .unwrap();
972 }
973 pool.slots[0].state = SlotState::Idle;
974 pool.slots[1].state = SlotState::Idle;
975
976 let mut actions: PoolActions<MailAddr, u8, (), TestWorker> = Actions::cont();
977 pool.dispatch(&mut actions);
978
979 let assignments = &actions.sends.behavior.assignments;
980 assert_eq!(assignments.len(), 2);
981 for (index, expected_job) in [JobId(1), JobId(2)].into_iter().enumerate() {
982 assert_eq!(
983 assignments[index].to.route(),
984 Route::Child(u64::try_from(index).unwrap())
985 );
986 let ProxyCommand::Forward(assignment) = &assignments[index].message else {
987 panic!("pool dispatches with Forward");
988 };
989 assert_eq!(
990 assignment.assignment,
991 AssignmentId(u64::try_from(index).unwrap())
992 );
993 assert_eq!(assignment.job, expected_job);
994 }
995 assert_eq!(pool.backlog.len(), 1);
996 assert_eq!(pool.backlog[0].accepted.id, JobId(3));
997 }
998}
999
1000pub struct KeyedWorkerPool<A: Address, K, J, R, C, S>
1033where
1034 K: Eq,
1035 C: Behavior<Addr = A, Msg = PoolAssignment<J>, Ph = Never>,
1036 S: AffinitySelector<K, A::Nonce>,
1037{
1038 pool: WorkerPool<A, J, R, C>,
1039 bindings: Vec<(K, A::Nonce)>,
1040 selector: S,
1041}
1042
1043impl<A, K, J, R, C, S> KeyedWorkerPool<A, K, J, R, C, S>
1044where
1045 A: Address,
1046 K: Eq,
1047 C: Behavior<Addr = A, Msg = PoolAssignment<J>, Ph = Never>,
1048 S: AffinitySelector<K, A::Nonce>,
1049{
1050 #[allow(
1060 clippy::too_many_arguments,
1061 reason = "the arguments expose the complete pool and affinity policy"
1062 )]
1063 pub fn new(
1064 nonces: fn(usize) -> A::Nonce,
1065 count: usize,
1066 build: fn(usize) -> C,
1067 backlog_capacity: usize,
1068 interruption: InterruptionPolicy,
1069 restart_policy: RestartPolicy,
1070 max_restarts: u32,
1071 restart_window: Duration,
1072 selector: S,
1073 ) -> Result<Self, PoolConfigError<A::Nonce>> {
1074 Ok(Self {
1075 pool: WorkerPool::new(
1076 nonces,
1077 count,
1078 build,
1079 backlog_capacity,
1080 interruption,
1081 restart_policy,
1082 max_restarts,
1083 restart_window,
1084 )?,
1085 bindings: Vec::new(),
1086 selector,
1087 })
1088 }
1089
1090 #[must_use]
1092 pub fn affinity(&self, key: &K) -> Option<A::Nonce> {
1093 self.bindings
1094 .iter()
1095 .find_map(|(bound, worker)| (bound == key).then_some(*worker))
1096 }
1097
1098 #[must_use]
1099 pub fn backlog_len(&self) -> usize {
1100 self.pool.backlog_len()
1101 }
1102
1103 #[must_use]
1104 pub fn worker_phase(&self, worker: A::Nonce) -> Option<WorkerPhase> {
1105 self.pool.worker_phase(worker)
1106 }
1107
1108 fn rebalance(&mut self, key: K, worker: A::Nonce) -> Result<(), PoolError<A::Nonce>> {
1109 let position = self.pool.slot_position(worker)?;
1110 if let SlotState::Retired { reason } = self.pool.slots[position].state {
1111 return Err(PoolError::RebalanceToRetiredWorker { worker, reason });
1112 }
1113 if let Some((_, bound)) = self.bindings.iter_mut().find(|(bound, _)| *bound == key) {
1114 *bound = worker;
1115 } else {
1116 self.bindings.push((key, worker));
1117 }
1118 Ok(())
1119 }
1120}
1121
1122impl<A, K, J, R, C, S> Behavior for KeyedWorkerPool<A, K, J, R, C, S>
1123where
1124 A: Address,
1125 A::Nonce: From<u64>,
1126 K: Eq,
1127 J: Clone,
1128 C: Behavior<Addr = A, Msg = PoolAssignment<J>, Ph = Never>,
1129 S: AffinitySelector<K, A::Nonce>,
1130{
1131 type Addr = A;
1132 type Msg = KeyedPoolMessage<A, K, J, R>;
1133 type Event = KeyedPoolEvent<A, K, J, R>;
1134 type Sends = PoolSends<A, J, R, C>;
1135 type Ph = Never;
1136 type Error = PoolError<A::Nonce>;
1137 type Birth = Births<Proxy<C>>;
1138
1139 fn init(&mut self) -> crate::BehaviorActed<Self> {
1140 self.pool.init()
1141 }
1142
1143 fn transition(&mut self, event: Self::Event) -> crate::BehaviorActed<Self> {
1144 match event {
1145 SupervisionEvent::Inner(User {
1146 message:
1147 KeyedPoolMessage::Submit {
1148 key,
1149 job,
1150 payload,
1151 reply_to,
1152 },
1153 ..
1154 }) => {
1155 let existing = self.affinity(&key);
1156 let target = existing.unwrap_or_else(|| self.selector.select(&key));
1157 let mut actions = Actions::cont();
1158 let admission = self
1159 .pool
1160 .submit_to(target, job, payload, reply_to, &mut actions);
1161 match (admission, existing) {
1162 (Admission::Accepted, None) => self.bindings.push((key, target)),
1163 (Admission::Accepted | Admission::Rejected, Some(_))
1164 | (Admission::Rejected, None) => {}
1165 }
1166 self.pool.dispatch(&mut actions);
1167 Ok(actions)
1168 }
1169 SupervisionEvent::Inner(User {
1170 message:
1171 KeyedPoolMessage::Completed {
1172 worker,
1173 assignment,
1174 result,
1175 },
1176 ..
1177 }) => {
1178 let mut actions = Actions::cont();
1179 self.pool
1180 .complete(worker, assignment, result, &mut actions)?;
1181 self.pool.dispatch(&mut actions);
1182 Ok(actions)
1183 }
1184 SupervisionEvent::Inner(User {
1185 message: KeyedPoolMessage::Rebalance { key, worker },
1186 ..
1187 }) => {
1188 self.rebalance(key, worker)?;
1189 Ok(Actions::cont())
1190 }
1191 SupervisionEvent::WorkerStopped(stopped) => self
1192 .pool
1193 .transition(SupervisionEvent::WorkerStopped(stopped)),
1194 SupervisionEvent::WorkerCreationResolved(resolved) => self
1195 .pool
1196 .transition(SupervisionEvent::WorkerCreationResolved(resolved)),
1197 SupervisionEvent::ChildStopped(stopped) => self
1198 .pool
1199 .transition(SupervisionEvent::ChildStopped(stopped)),
1200 SupervisionEvent::CreationResolved(resolved) => self
1201 .pool
1202 .transition(SupervisionEvent::CreationResolved(resolved)),
1203 }
1204 }
1205}