Skip to main content

sfo_pool/
classified_worker_pool.rs

1use crate::{
2    pool_cleared_error, pool_clearing_error, pool_invalid_config_error, PoolError, PoolResult,
3};
4use notify_future::Notify;
5use std::collections::{HashMap, VecDeque};
6use std::hash::Hash;
7use std::ops::{Deref, DerefMut};
8use std::sync::{Arc, Mutex};
9use std::time::{Duration, Instant};
10
11pub trait WorkerClassification: Send + 'static + Clone + Hash + Eq + PartialEq {}
12
13impl<T: Send + 'static + Clone + Hash + Eq + PartialEq> WorkerClassification for T {}
14
15#[derive(Debug, Clone, Default)]
16pub struct ClassifiedWorkerPoolConfig {
17    /// Target maximum number of workers managed by the pool.
18    ///
19    /// `None` leaves the worker count unlimited. A finite target may be
20    /// temporarily exceeded to create a worker for a missing classification.
21    pub max_count: Option<u16>,
22    pub idle_timeout: Option<Duration>,
23    /// Maximum number of workers whose primary classification is the same.
24    ///
25    /// `None` leaves classification counts unlimited. This limit is independent
26    /// of the pool-wide `max_count` target.
27    pub max_count_per_classification: Option<u16>,
28}
29
30#[async_trait::async_trait]
31/// A classification-aware worker managed by [`ClassifiedWorkerPool`].
32///
33/// Methods on this trait may be called while the pool's internal state lock is held.
34/// Implementations must be non-blocking and must not re-enter APIs on the same pool.
35pub trait ClassifiedWorker<C: WorkerClassification>: Send + 'static {
36    fn is_work(&self) -> bool;
37    /// Returns whether this worker can currently serve the requested classification.
38    /// The pool still tracks capacity by the worker's primary `classification()`.
39    /// A worker that is no longer valid for its cached primary classification is discarded.
40    fn is_valid(&self, c: C) -> bool;
41    /// Returns the worker's primary classification used for accounting and replacement.
42    /// The pool validates and caches this value when the worker is created.
43    /// If it differs from the cached value when the worker is returned, the worker is discarded.
44    fn classification(&self) -> C;
45}
46
47pub struct ClassifiedWorkerGuard<
48    C: WorkerClassification,
49    W: ClassifiedWorker<C>,
50    F: ClassifiedWorkerFactory<C, W>,
51> {
52    pool_ref: ClassifiedWorkerPoolRef<C, W, F>,
53    worker: Option<W>,
54    primary_classification: C,
55}
56
57impl<C: WorkerClassification, W: ClassifiedWorker<C>, F: ClassifiedWorkerFactory<C, W>>
58    ClassifiedWorkerGuard<C, W, F>
59{
60    fn new(
61        worker: W,
62        pool_ref: ClassifiedWorkerPoolRef<C, W, F>,
63        primary_classification: C,
64    ) -> Self {
65        ClassifiedWorkerGuard {
66            pool_ref,
67            worker: Some(worker),
68            primary_classification,
69        }
70    }
71}
72
73impl<C: WorkerClassification, W: ClassifiedWorker<C>, F: ClassifiedWorkerFactory<C, W>> DerefMut
74    for ClassifiedWorkerGuard<C, W, F>
75{
76    fn deref_mut(&mut self) -> &mut Self::Target {
77        self.worker.as_mut().unwrap()
78    }
79}
80
81impl<C: WorkerClassification, W: ClassifiedWorker<C>, F: ClassifiedWorkerFactory<C, W>> Deref
82    for ClassifiedWorkerGuard<C, W, F>
83{
84    type Target = W;
85
86    fn deref(&self) -> &Self::Target {
87        self.worker.as_ref().unwrap()
88    }
89}
90
91impl<C: WorkerClassification, W: ClassifiedWorker<C>, F: ClassifiedWorkerFactory<C, W>> Drop
92    for ClassifiedWorkerGuard<C, W, F>
93{
94    fn drop(&mut self) {
95        if let Some(worker) = self.worker.take() {
96            self.pool_ref
97                .release(worker, self.primary_classification.clone());
98        }
99    }
100}
101
102struct ClassifiedWorkerReservation<
103    C: WorkerClassification,
104    W: ClassifiedWorker<C>,
105    F: ClassifiedWorkerFactory<C, W>,
106> {
107    pool_ref: ClassifiedWorkerPoolRef<C, W, F>,
108    requested_classification: C,
109    active: bool,
110}
111
112enum ReservationCompletion {
113    Complete,
114    Clearing,
115}
116
117impl<C: WorkerClassification, W: ClassifiedWorker<C>, F: ClassifiedWorkerFactory<C, W>>
118    ClassifiedWorkerReservation<C, W, F>
119{
120    fn new(pool_ref: ClassifiedWorkerPoolRef<C, W, F>, classification: C) -> Self {
121        Self {
122            pool_ref,
123            requested_classification: classification,
124            active: true,
125        }
126    }
127
128    fn complete(mut self, worker_classification: C) -> ReservationCompletion {
129        let (completion, clear_waiters) = {
130            let mut state = self.pool_ref.state.lock().unwrap();
131            state.dec_pending_classified_count(self.requested_classification.clone());
132            if state.clearing {
133                state.current_count -= 1;
134                (
135                    ReservationCompletion::Clearing,
136                    state.take_clear_waiters_if_done(),
137                )
138            } else {
139                state.inc_classified_count(worker_classification);
140                (ReservationCompletion::Complete, Vec::new())
141            }
142        };
143        self.active = false;
144        for waiter in clear_waiters {
145            waiter.notify(());
146        }
147        completion
148    }
149}
150
151impl<C: WorkerClassification, W: ClassifiedWorker<C>, F: ClassifiedWorkerFactory<C, W>> Drop
152    for ClassifiedWorkerReservation<C, W, F>
153{
154    fn drop(&mut self) {
155        if self.active {
156            self.pool_ref
157                .rollback_reservation(&self.requested_classification);
158        }
159    }
160}
161
162#[async_trait::async_trait]
163pub trait ClassifiedWorkerFactory<C: WorkerClassification, W: ClassifiedWorker<C>>:
164    Send + Sync + 'static
165{
166    /// Creates a usable worker for `c`.
167    ///
168    /// Returning `Ok` asserts that the worker is ready for use. Its primary
169    /// classification must be `c`.
170    async fn create(&self, c: C) -> PoolResult<W>;
171}
172
173struct WaitingItem<
174    C: WorkerClassification,
175    W: ClassifiedWorker<C>,
176    F: ClassifiedWorkerFactory<C, W>,
177> {
178    future: Notify<ClassifiedWorkerWaitResult<C, W, F>>,
179    condition: C,
180}
181
182struct IdleWorker<C, W> {
183    worker: W,
184    primary_classification: C,
185    idle_since: Instant,
186}
187
188enum ClassifiedWorkerWaitResult<
189    C: WorkerClassification,
190    W: ClassifiedWorker<C>,
191    F: ClassifiedWorkerFactory<C, W>,
192> {
193    Worker(ClassifiedWorkerGuard<C, W, F>),
194    Retry,
195    Error(PoolError),
196}
197
198struct WorkerPoolState<
199    C: WorkerClassification,
200    W: ClassifiedWorker<C>,
201    F: ClassifiedWorkerFactory<C, W>,
202> {
203    current_count: usize,
204    classified_count_map: HashMap<C, usize>,
205    pending_classified_count_map: HashMap<C, usize>,
206    worker_list: VecDeque<IdleWorker<C, W>>,
207    waiting_list: Vec<WaitingItem<C, W, F>>,
208    clearing: bool,
209    clear_waiting_list: Vec<Notify<()>>,
210}
211
212impl<C: WorkerClassification, W: ClassifiedWorker<C>, F: ClassifiedWorkerFactory<C, W>>
213    WorkerPoolState<C, W, F>
214{
215    fn inc_classified_count(&mut self, c: C) {
216        let count = self.classified_count_map.entry(c).or_insert(0);
217        *count += 1;
218    }
219
220    fn dec_classified_count(&mut self, c: C) {
221        let mut should_remove = false;
222        if let Some(count) = self.classified_count_map.get_mut(&c) {
223            debug_assert!(*count > 0);
224            *count -= 1;
225            should_remove = *count == 0;
226        }
227        if should_remove {
228            self.classified_count_map.remove(&c);
229        }
230    }
231
232    fn inc_pending_classified_count(&mut self, c: C) {
233        let count = self.pending_classified_count_map.entry(c).or_insert(0);
234        *count += 1;
235    }
236
237    fn dec_pending_classified_count(&mut self, c: C) {
238        let mut should_remove = false;
239        if let Some(count) = self.pending_classified_count_map.get_mut(&c) {
240            debug_assert!(*count > 0);
241            *count -= 1;
242            should_remove = *count == 0;
243        }
244        if should_remove {
245            self.pending_classified_count_map.remove(&c);
246        }
247    }
248
249    fn reserved_classified_count(&self, c: &C) -> usize {
250        self.classified_count_map.get(c).copied().unwrap_or(0)
251            + self
252                .pending_classified_count_map
253                .get(c)
254                .copied()
255                .unwrap_or(0)
256    }
257
258    fn take_clear_waiters_if_done(&mut self) -> Vec<Notify<()>> {
259        if self.clearing && self.current_count == 0 {
260            self.clearing = false;
261            self.clear_waiting_list.drain(..).collect()
262        } else {
263            Vec::new()
264        }
265    }
266
267    fn find_matching_waiter_index_for_worker(&self, worker: &W) -> Option<usize> {
268        self.waiting_list.iter().position(|waiting| {
269            if waiting.future.is_canceled() {
270                return false;
271            }
272            worker.is_valid(waiting.condition.clone())
273        })
274    }
275
276    fn remove_canceled_waiters(&mut self) {
277        self.waiting_list
278            .retain(|waiting| !waiting.future.is_canceled());
279    }
280
281    fn drain_waiters(&mut self) -> Vec<Notify<ClassifiedWorkerWaitResult<C, W, F>>> {
282        self.waiting_list
283            .drain(..)
284            .map(|waiting| waiting.future)
285            .collect()
286    }
287}
288
289pub struct ClassifiedWorkerPool<
290    C: WorkerClassification,
291    W: ClassifiedWorker<C>,
292    F: ClassifiedWorkerFactory<C, W>,
293> {
294    factory: Arc<F>,
295    config: ClassifiedWorkerPoolConfig,
296    state: Mutex<WorkerPoolState<C, W, F>>,
297}
298pub type ClassifiedWorkerPoolRef<C, W, F> = Arc<ClassifiedWorkerPool<C, W, F>>;
299
300impl<C: WorkerClassification, W: ClassifiedWorker<C>, F: ClassifiedWorkerFactory<C, W>>
301    ClassifiedWorkerPool<C, W, F>
302{
303    fn classification_limit_reached(&self, state: &WorkerPoolState<C, W, F>, c: &C) -> bool {
304        self.config
305            .max_count_per_classification
306            .map(|max_count| state.reserved_classified_count(c) >= usize::from(max_count))
307            .unwrap_or(false)
308    }
309
310    fn find_replaceable_classified_waiter_index(
311        &self,
312        state: &WorkerPoolState<C, W, F>,
313    ) -> Option<usize> {
314        state.waiting_list.iter().position(|waiting| {
315            !waiting.future.is_canceled()
316                && !self.classification_limit_reached(state, &waiting.condition)
317        })
318    }
319
320    fn validate_created_worker(requested_classification: &C, worker: &W) -> PoolResult<C> {
321        let worker_classification = worker.classification();
322        if !worker.is_valid(worker_classification.clone()) {
323            return Err(pool_invalid_config_error(
324                "worker primary classification is not valid for itself",
325            ));
326        }
327        if worker_classification != requested_classification.clone() {
328            return Err(pool_invalid_config_error(
329                "factory returned worker with mismatched classification",
330            ));
331        }
332        Ok(worker_classification)
333    }
334
335    /// Creates a classification-aware worker pool with explicit configuration.
336    ///
337    /// A finite `max_count` is a target rather than a strict upper bound. When the
338    /// pool is full, no idle worker can be replaced, and a requested classification
339    /// has no created or pending worker, the pool may temporarily exceed the target.
340    /// Excess workers are removed when they are returned and are not needed by a
341    /// waiter. `None` leaves the pool-wide worker count unlimited.
342    pub fn new(factory: F, config: ClassifiedWorkerPoolConfig) -> ClassifiedWorkerPoolRef<C, W, F> {
343        let idle_capacity = config.max_count.unwrap_or(0) as usize;
344        Arc::new(ClassifiedWorkerPool {
345            factory: Arc::new(factory),
346            config,
347            state: Mutex::new(WorkerPoolState {
348                current_count: 0,
349                classified_count_map: HashMap::new(),
350                pending_classified_count_map: HashMap::new(),
351                worker_list: VecDeque::with_capacity(idle_capacity),
352                waiting_list: Vec::new(),
353                clearing: false,
354                clear_waiting_list: Vec::new(),
355            }),
356        })
357    }
358
359    fn take_expired_idle_workers(
360        state: &mut WorkerPoolState<C, W, F>,
361        idle_timeout: Option<std::time::Duration>,
362    ) -> Vec<IdleWorker<C, W>> {
363        let Some(idle_timeout) = idle_timeout else {
364            return Vec::new();
365        };
366        let mut removed_workers = Vec::new();
367        let now = Instant::now();
368        while state
369            .worker_list
370            .front()
371            .map(|idle_worker| now.duration_since(idle_worker.idle_since) >= idle_timeout)
372            .unwrap_or(false)
373        {
374            let idle_worker = state.worker_list.pop_front().unwrap();
375            state.current_count -= 1;
376            state.dec_classified_count(idle_worker.primary_classification.clone());
377            removed_workers.push(idle_worker);
378        }
379        removed_workers
380    }
381
382    pub fn cleanup_idle_worker(&self) -> usize {
383        let (removed_workers, clear_waiters) = {
384            let mut state = self.state.lock().unwrap();
385            let removed_workers =
386                Self::take_expired_idle_workers(&mut state, self.config.idle_timeout);
387            let clear_waiters = state.take_clear_waiters_if_done();
388            (removed_workers, clear_waiters)
389        };
390        for waiter in clear_waiters {
391            waiter.notify(());
392        }
393        let removed_count = removed_workers.len();
394        drop(removed_workers);
395        removed_count
396    }
397
398    pub async fn get_worker(
399        self: &ClassifiedWorkerPoolRef<C, W, F>,
400        classification: C,
401    ) -> PoolResult<ClassifiedWorkerGuard<C, W, F>> {
402        loop {
403            if self.config.max_count == Some(0) {
404                return Err(pool_invalid_config_error("pool max_count is zero"));
405            }
406            if self.config.max_count_per_classification == Some(0) {
407                return Err(pool_invalid_config_error(
408                    "pool max_count_per_classification is zero",
409                ));
410            }
411
412            let (worker, wait, should_create, removed_workers) = {
413                let mut state = self.state.lock().unwrap();
414                if state.clearing {
415                    return Err(pool_clearing_error());
416                }
417                state.remove_canceled_waiters();
418
419                let mut removed_workers =
420                    Self::take_expired_idle_workers(&mut state, self.config.idle_timeout);
421
422                let mut valid_workers = VecDeque::with_capacity(state.worker_list.len());
423                while let Some(idle_worker) = state.worker_list.pop_front() {
424                    if idle_worker.worker.is_work()
425                        && idle_worker.worker.classification() == idle_worker.primary_classification
426                        && idle_worker
427                            .worker
428                            .is_valid(idle_worker.primary_classification.clone())
429                    {
430                        valid_workers.push_back(idle_worker);
431                    } else {
432                        state.current_count -= 1;
433                        state.dec_classified_count(idle_worker.primary_classification.clone());
434                        removed_workers.push(idle_worker);
435                    }
436                }
437                state.worker_list = valid_workers;
438
439                let worker = state
440                    .worker_list
441                    .iter()
442                    .rposition(|idle_worker| idle_worker.worker.is_valid(classification.clone()))
443                    .map(|index| {
444                        let idle_worker = state.worker_list.remove(index).unwrap();
445                        (idle_worker.worker, idle_worker.primary_classification)
446                    });
447
448                if worker.is_some() {
449                    (worker, None, false, removed_workers)
450                } else if self.classification_limit_reached(&state, &classification) {
451                    let (notify, waiter) = Notify::new();
452                    state.waiting_list.push(WaitingItem {
453                        future: notify,
454                        condition: classification.clone(),
455                    });
456                    (None, Some(waiter), false, removed_workers)
457                } else if self
458                    .config
459                    .max_count
460                    .map(|max_count| state.current_count < usize::from(max_count))
461                    .unwrap_or(true)
462                {
463                    state.current_count += 1;
464                    state.inc_pending_classified_count(classification.clone());
465                    (None, None, true, removed_workers)
466                } else if let Some(idle_worker) = state.worker_list.pop_front() {
467                    state.dec_classified_count(idle_worker.primary_classification.clone());
468                    state.inc_pending_classified_count(classification.clone());
469                    removed_workers.push(idle_worker);
470                    (None, None, true, removed_workers)
471                } else if state.reserved_classified_count(&classification) == 0 {
472                    state.current_count += 1;
473                    state.inc_pending_classified_count(classification.clone());
474                    (None, None, true, removed_workers)
475                } else {
476                    let (notify, waiter) = Notify::new();
477                    state.waiting_list.push(WaitingItem {
478                        future: notify,
479                        condition: classification.clone(),
480                    });
481                    (None, Some(waiter), false, removed_workers)
482                }
483            };
484
485            let reservation = should_create
486                .then(|| ClassifiedWorkerReservation::new(self.clone(), classification.clone()));
487            drop(removed_workers);
488
489            if let Some((worker, primary_classification)) = worker {
490                return Ok(ClassifiedWorkerGuard::new(
491                    worker,
492                    self.clone(),
493                    primary_classification,
494                ));
495            }
496
497            if let Some(wait) = wait {
498                match wait.await {
499                    ClassifiedWorkerWaitResult::Worker(worker) => return Ok(worker),
500                    ClassifiedWorkerWaitResult::Retry => continue,
501                    ClassifiedWorkerWaitResult::Error(err) => return Err(err),
502                }
503            }
504
505            let reservation = reservation.unwrap();
506            let (worker, primary_classification) =
507                match self.factory.create(classification.clone()).await {
508                    Ok(worker) => {
509                        let primary_classification =
510                            Self::validate_created_worker(&classification, &worker)?;
511                        (worker, primary_classification)
512                    }
513                    Err(err) => return Err(err),
514                };
515            match reservation.complete(primary_classification.clone()) {
516                ReservationCompletion::Complete => {}
517                ReservationCompletion::Clearing => return Err(pool_cleared_error()),
518            }
519            return Ok(ClassifiedWorkerGuard::new(
520                worker,
521                self.clone(),
522                primary_classification,
523            ));
524        }
525    }
526
527    pub async fn clear_all_worker(&self) {
528        let (waiter, waiting_list, clear_waiters, idle_workers) = {
529            let mut state = self.state.lock().unwrap();
530            let idle_workers = if !state.clearing {
531                state.clearing = true;
532                let idle_workers = state.worker_list.drain(..).collect::<Vec<_>>();
533                let cur_worker_count = idle_workers.len();
534                state.current_count -= cur_worker_count;
535                for idle_worker in &idle_workers {
536                    state.dec_classified_count(idle_worker.primary_classification.clone());
537                }
538                idle_workers
539            } else {
540                Vec::new()
541            };
542
543            let waiting_list = state.waiting_list.drain(..).collect::<Vec<_>>();
544            if state.current_count == 0 {
545                let clear_waiters = state.take_clear_waiters_if_done();
546                (None, waiting_list, clear_waiters, idle_workers)
547            } else {
548                let (notify, waiter) = Notify::new();
549                state.clear_waiting_list.push(notify);
550                (Some(waiter), waiting_list, Vec::new(), idle_workers)
551            }
552        };
553        for waiting in waiting_list {
554            waiting
555                .future
556                .notify(ClassifiedWorkerWaitResult::Error(pool_cleared_error()));
557        }
558        for waiter in clear_waiters {
559            waiter.notify(());
560        }
561        drop(idle_workers);
562        if let Some(waiter) = waiter {
563            waiter.await;
564        }
565    }
566
567    fn notify_retry_waiters(waiters: Vec<Notify<ClassifiedWorkerWaitResult<C, W, F>>>) {
568        for waiter in waiters {
569            waiter.notify(ClassifiedWorkerWaitResult::Retry);
570        }
571    }
572
573    fn rollback_reservation(&self, classification: &C) {
574        let (retry_waiters, clear_waiters) = {
575            let mut state = self.state.lock().unwrap();
576            state.current_count -= 1;
577            state.dec_pending_classified_count(classification.clone());
578            let retry_waiters = state.drain_waiters();
579            let clear_waiters = state.take_clear_waiters_if_done();
580            (retry_waiters, clear_waiters)
581        };
582        Self::notify_retry_waiters(retry_waiters);
583        for waiter in clear_waiters {
584            waiter.notify(());
585        }
586    }
587
588    fn release(self: &ClassifiedWorkerPoolRef<C, W, F>, work: W, primary_classification: C) {
589        enum ReleaseAction<
590            C: WorkerClassification,
591            W: ClassifiedWorker<C>,
592            F: ClassifiedWorkerFactory<C, W>,
593        > {
594            None,
595            Notify(
596                Notify<ClassifiedWorkerWaitResult<C, W, F>>,
597                ClassifiedWorkerGuard<C, W, F>,
598            ),
599            Retry(Vec<Notify<ClassifiedWorkerWaitResult<C, W, F>>>),
600        }
601
602        let primary_classification_valid = work.classification() == primary_classification
603            && work.is_valid(primary_classification.clone());
604        let mut clear_waiters = Vec::new();
605        let action = {
606            let mut state = self.state.lock().unwrap();
607            state.remove_canceled_waiters();
608            if state.clearing {
609                state.current_count -= 1;
610                state.dec_classified_count(primary_classification);
611                clear_waiters = state.take_clear_waiters_if_done();
612                ReleaseAction::None
613            } else if !primary_classification_valid {
614                state.current_count -= 1;
615                state.dec_classified_count(primary_classification);
616                let waiters = state.drain_waiters();
617                if !waiters.is_empty() {
618                    ReleaseAction::Retry(waiters)
619                } else {
620                    ReleaseAction::None
621                }
622            } else if work.is_work() {
623                if let Some(index) = state.find_matching_waiter_index_for_worker(&work) {
624                    let waiting_item = state.waiting_list.remove(index);
625                    ReleaseAction::Notify(
626                        waiting_item.future,
627                        ClassifiedWorkerGuard::new(work, self.clone(), primary_classification),
628                    )
629                } else if let Some(index) = self.find_replaceable_classified_waiter_index(&state) {
630                    state.current_count -= 1;
631                    state.dec_classified_count(primary_classification);
632                    let mut waiters = state.drain_waiters();
633                    if index < waiters.len() {
634                        waiters.swap(0, index);
635                    }
636                    ReleaseAction::Retry(waiters)
637                } else if self
638                    .config
639                    .max_count
640                    .map(|max_count| state.current_count > usize::from(max_count))
641                    .unwrap_or(false)
642                {
643                    state.current_count -= 1;
644                    state.dec_classified_count(primary_classification);
645                    clear_waiters = state.take_clear_waiters_if_done();
646                    ReleaseAction::None
647                } else {
648                    state.worker_list.push_back(IdleWorker {
649                        worker: work,
650                        primary_classification,
651                        idle_since: Instant::now(),
652                    });
653                    ReleaseAction::None
654                }
655            } else {
656                state.dec_classified_count(primary_classification);
657                state.current_count -= 1;
658                let waiters = state.drain_waiters();
659                if !waiters.is_empty() {
660                    ReleaseAction::Retry(waiters)
661                } else {
662                    clear_waiters = state.take_clear_waiters_if_done();
663                    ReleaseAction::None
664                }
665            }
666        };
667
668        for waiter in clear_waiters {
669            waiter.notify(());
670        }
671
672        match action {
673            ReleaseAction::None => {}
674            ReleaseAction::Notify(waiting, worker) => {
675                waiting.notify(ClassifiedWorkerWaitResult::Worker(worker));
676            }
677            ReleaseAction::Retry(waiters) => {
678                Self::notify_retry_waiters(waiters);
679            }
680        }
681    }
682}
683
684#[cfg(test)]
685fn new_classified_worker_pool<
686    C: WorkerClassification,
687    W: ClassifiedWorker<C>,
688    F: ClassifiedWorkerFactory<C, W>,
689>(
690    max_count: u16,
691    factory: F,
692) -> ClassifiedWorkerPoolRef<C, W, F> {
693    ClassifiedWorkerPool::new(
694        factory,
695        ClassifiedWorkerPoolConfig {
696            max_count: Some(max_count),
697            ..Default::default()
698        },
699    )
700}
701
702#[tokio::test]
703async fn test_pool() {
704    struct TestWorker {
705        work: bool,
706        classification: TestWorkerClassification,
707    }
708
709    #[derive(Clone, Debug, Eq, PartialEq, Hash)]
710    enum TestWorkerClassification {
711        A,
712        B,
713    }
714    #[async_trait::async_trait]
715    impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
716        fn is_work(&self) -> bool {
717            self.work
718        }
719
720        fn is_valid(&self, c: TestWorkerClassification) -> bool {
721            self.classification == c
722        }
723
724        fn classification(&self) -> TestWorkerClassification {
725            self.classification.clone()
726        }
727    }
728
729    struct TestWorkerFactory;
730
731    #[async_trait::async_trait]
732    impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
733        async fn create(&self, classification: TestWorkerClassification) -> PoolResult<TestWorker> {
734            Ok(TestWorker {
735                work: true,
736                classification,
737            })
738        }
739    }
740
741    let pool = new_classified_worker_pool(3, TestWorkerFactory);
742
743    let worker_a1 = pool
744        .get_worker(TestWorkerClassification::A)
745        .await
746        .unwrap();
747    let worker_a2 = pool
748        .get_worker(TestWorkerClassification::A)
749        .await
750        .unwrap();
751    let worker_b = pool
752        .get_worker(TestWorkerClassification::B)
753        .await
754        .unwrap();
755
756    let pool_ref = pool.clone();
757    let classified_waiter = tokio::spawn(async move {
758        pool_ref
759            .get_worker(TestWorkerClassification::B)
760            .await
761    });
762    tokio::time::sleep(std::time::Duration::from_millis(20)).await;
763    assert!(!classified_waiter.is_finished());
764
765    drop(worker_b);
766    let worker_b = tokio::time::timeout(std::time::Duration::from_secs(1), classified_waiter)
767        .await
768        .unwrap()
769        .unwrap()
770        .unwrap();
771    drop(worker_a1);
772    drop(worker_a2);
773    drop(worker_b);
774
775    let worker3 = pool
776        .get_worker(TestWorkerClassification::B)
777        .await
778        .unwrap();
779    let worker1 = pool
780        .get_worker(TestWorkerClassification::A)
781        .await
782        .unwrap();
783    let worker2 = pool
784        .get_worker(TestWorkerClassification::A)
785        .await
786        .unwrap();
787
788    let pool_ref = pool.clone();
789    let classified_a_waiter = tokio::spawn(async move {
790        pool_ref
791            .get_worker(TestWorkerClassification::A)
792            .await
793    });
794    let pool_ref = pool.clone();
795    let classified_waiter = tokio::spawn(async move {
796        pool_ref
797            .get_worker(TestWorkerClassification::B)
798            .await
799    });
800    tokio::time::sleep(std::time::Duration::from_millis(20)).await;
801    assert!(!classified_a_waiter.is_finished());
802    assert!(!classified_waiter.is_finished());
803
804    let pool_ref = pool.clone();
805    let clear_task = tokio::spawn(async move {
806        pool_ref.clear_all_worker().await;
807    });
808    tokio::time::sleep(std::time::Duration::from_millis(20)).await;
809
810    assert!(classified_a_waiter.await.unwrap().is_err());
811    assert!(classified_waiter.await.unwrap().is_err());
812
813    drop(worker1);
814    drop(worker2);
815    drop(worker3);
816
817    tokio::time::timeout(std::time::Duration::from_secs(1), clear_task)
818        .await
819        .unwrap()
820        .unwrap();
821}
822
823#[tokio::test]
824async fn test_clear_all_worker_waits_for_inflight_create() {
825    use std::sync::atomic::{AtomicUsize, Ordering};
826    use std::sync::Arc;
827
828    #[derive(Clone, Debug, Eq, PartialEq, Hash)]
829    enum TestWorkerClassification {
830        A,
831    }
832
833    struct TestWorker {
834        classification: TestWorkerClassification,
835    }
836
837    #[async_trait::async_trait]
838    impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
839        fn is_work(&self) -> bool {
840            true
841        }
842
843        fn is_valid(&self, c: TestWorkerClassification) -> bool {
844            self.classification == c
845        }
846
847        fn classification(&self) -> TestWorkerClassification {
848            self.classification.clone()
849        }
850    }
851
852    struct TestWorkerFactory {
853        create_count: Arc<AtomicUsize>,
854    }
855
856    #[async_trait::async_trait]
857    impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
858        async fn create(&self, classification: TestWorkerClassification) -> PoolResult<TestWorker> {
859            self.create_count.fetch_add(1, Ordering::SeqCst);
860            tokio::time::sleep(std::time::Duration::from_millis(100)).await;
861            Ok(TestWorker { classification })
862        }
863    }
864
865    let create_count = Arc::new(AtomicUsize::new(0));
866    let pool = new_classified_worker_pool(
867        1,
868        TestWorkerFactory {
869            create_count: create_count.clone(),
870        },
871    );
872
873    let pool_ref = pool.clone();
874    let worker_task = tokio::spawn(async move {
875        pool_ref
876            .get_worker(TestWorkerClassification::A)
877            .await
878    });
879    tokio::time::sleep(std::time::Duration::from_millis(100)).await;
880
881    pool.clear_all_worker().await;
882
883    let worker = worker_task.await.unwrap();
884    assert!(worker.is_err());
885    assert_eq!(create_count.load(Ordering::SeqCst), 1);
886}
887
888#[tokio::test]
889async fn test_concurrent_clear_all_worker() {
890    #[derive(Clone, Debug, Eq, PartialEq, Hash)]
891    enum TestWorkerClassification {
892        A,
893    }
894
895    struct TestWorker {
896        classification: TestWorkerClassification,
897    }
898
899    #[async_trait::async_trait]
900    impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
901        fn is_work(&self) -> bool {
902            true
903        }
904
905        fn is_valid(&self, c: TestWorkerClassification) -> bool {
906            self.classification == c
907        }
908
909        fn classification(&self) -> TestWorkerClassification {
910            self.classification.clone()
911        }
912    }
913
914    struct TestWorkerFactory;
915
916    #[async_trait::async_trait]
917    impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
918        async fn create(&self, classification: TestWorkerClassification) -> PoolResult<TestWorker> {
919            Ok(TestWorker { classification })
920        }
921    }
922
923    let pool = new_classified_worker_pool(1, TestWorkerFactory);
924    let worker = pool
925        .get_worker(TestWorkerClassification::A)
926        .await
927        .unwrap();
928
929    let pool_ref = pool.clone();
930    let clear_task1 = tokio::spawn(async move {
931        pool_ref.clear_all_worker().await;
932    });
933
934    let pool_ref = pool.clone();
935    let clear_task2 = tokio::spawn(async move {
936        pool_ref.clear_all_worker().await;
937    });
938
939    tokio::time::sleep(std::time::Duration::from_millis(100)).await;
940    drop(worker);
941
942    tokio::time::timeout(std::time::Duration::from_secs(1), async {
943        clear_task1.await.unwrap();
944        clear_task2.await.unwrap();
945    })
946    .await
947    .unwrap();
948}
949
950#[tokio::test]
951async fn test_zero_max_count_returns_error() {
952    #[derive(Clone, Debug, Eq, PartialEq, Hash)]
953    enum TestWorkerClassification {
954        A,
955    }
956
957    struct TestWorker {
958        classification: TestWorkerClassification,
959    }
960
961    #[async_trait::async_trait]
962    impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
963        fn is_work(&self) -> bool {
964            true
965        }
966
967        fn is_valid(&self, c: TestWorkerClassification) -> bool {
968            self.classification == c
969        }
970
971        fn classification(&self) -> TestWorkerClassification {
972            self.classification.clone()
973        }
974    }
975
976    struct TestWorkerFactory;
977
978    #[async_trait::async_trait]
979    impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
980        async fn create(&self, classification: TestWorkerClassification) -> PoolResult<TestWorker> {
981            Ok(TestWorker { classification })
982        }
983    }
984
985    let pool = new_classified_worker_pool(0, TestWorkerFactory);
986    let worker = pool
987        .get_worker(TestWorkerClassification::A)
988        .await;
989    assert!(worker.is_err());
990    assert_eq!(
991        worker.err().unwrap().code(),
992        crate::PoolErrorCode::InvalidConfig
993    );
994}
995
996#[tokio::test]
997async fn test_classified_pool_default_config_has_no_max_count() {
998    #[derive(Clone, Debug, Eq, PartialEq, Hash)]
999    struct TestWorkerClassification;
1000
1001    struct TestWorker;
1002
1003    impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
1004        fn is_work(&self) -> bool {
1005            true
1006        }
1007
1008        fn is_valid(&self, _classification: TestWorkerClassification) -> bool {
1009            true
1010        }
1011
1012        fn classification(&self) -> TestWorkerClassification {
1013            TestWorkerClassification
1014        }
1015    }
1016
1017    struct TestWorkerFactory;
1018
1019    #[async_trait::async_trait]
1020    impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
1021        async fn create(
1022            &self,
1023            _classification: TestWorkerClassification,
1024        ) -> PoolResult<TestWorker> {
1025            Ok(TestWorker)
1026        }
1027    }
1028
1029    let pool = ClassifiedWorkerPool::new(TestWorkerFactory, Default::default());
1030    let worker1 = pool
1031        .get_worker(TestWorkerClassification)
1032        .await
1033        .unwrap();
1034    let worker2 = pool
1035        .get_worker(TestWorkerClassification)
1036        .await
1037        .unwrap();
1038    drop((worker1, worker2));
1039}
1040
1041#[tokio::test]
1042async fn test_classified_pool_waits_when_classification_already_has_worker() {
1043    #[derive(Clone, Debug, Eq, PartialEq, Hash)]
1044    enum TestWorkerClassification {
1045        B,
1046    }
1047
1048    struct TestWorker {
1049        classification: TestWorkerClassification,
1050    }
1051
1052    #[async_trait::async_trait]
1053    impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
1054        fn is_work(&self) -> bool {
1055            true
1056        }
1057
1058        fn is_valid(&self, c: TestWorkerClassification) -> bool {
1059            self.classification == c
1060        }
1061
1062        fn classification(&self) -> TestWorkerClassification {
1063            self.classification.clone()
1064        }
1065    }
1066
1067    struct TestWorkerFactory;
1068
1069    #[async_trait::async_trait]
1070    impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
1071        async fn create(&self, classification: TestWorkerClassification) -> PoolResult<TestWorker> {
1072            Ok(TestWorker { classification })
1073        }
1074    }
1075
1076    let pool = new_classified_worker_pool(1, TestWorkerFactory);
1077    let _worker = pool
1078        .get_worker(TestWorkerClassification::B)
1079        .await
1080        .unwrap();
1081
1082    let pool_ref = pool.clone();
1083    let result = tokio::time::timeout(std::time::Duration::from_millis(100), async move {
1084        pool_ref
1085            .get_worker(TestWorkerClassification::B)
1086            .await
1087    })
1088    .await;
1089
1090    assert!(result.is_err());
1091}
1092
1093#[tokio::test]
1094async fn test_missing_classification_can_exceed_max_count_once() {
1095    use std::sync::atomic::{AtomicUsize, Ordering};
1096    use std::sync::Arc;
1097
1098    #[derive(Clone, Debug, Eq, PartialEq, Hash)]
1099    enum TestWorkerClassification {
1100        A,
1101        B,
1102    }
1103
1104    struct TestWorker {
1105        id: usize,
1106        classification: TestWorkerClassification,
1107    }
1108
1109    #[async_trait::async_trait]
1110    impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
1111        fn is_work(&self) -> bool {
1112            true
1113        }
1114
1115        fn is_valid(&self, c: TestWorkerClassification) -> bool {
1116            self.classification == c
1117        }
1118
1119        fn classification(&self) -> TestWorkerClassification {
1120            self.classification.clone()
1121        }
1122    }
1123
1124    struct TestWorkerFactory {
1125        create_count: Arc<AtomicUsize>,
1126    }
1127
1128    #[async_trait::async_trait]
1129    impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
1130        async fn create(&self, classification: TestWorkerClassification) -> PoolResult<TestWorker> {
1131            let id = self.create_count.fetch_add(1, Ordering::SeqCst);
1132            Ok(TestWorker { id, classification })
1133        }
1134    }
1135
1136    let create_count = Arc::new(AtomicUsize::new(0));
1137    let pool = new_classified_worker_pool(
1138        1,
1139        TestWorkerFactory {
1140            create_count: create_count.clone(),
1141        },
1142    );
1143
1144    let worker_a = pool
1145        .get_worker(TestWorkerClassification::A)
1146        .await
1147        .unwrap();
1148    let worker_b = pool
1149        .get_worker(TestWorkerClassification::B)
1150        .await
1151        .unwrap();
1152
1153    assert_eq!(worker_a.id, 0);
1154    assert_eq!(worker_b.id, 1);
1155    assert_eq!(worker_b.classification(), TestWorkerClassification::B);
1156    assert_eq!(create_count.load(Ordering::SeqCst), 2);
1157}
1158
1159#[tokio::test]
1160async fn test_classified_create_failure_fails_same_classification_waiters() {
1161    #[derive(Clone, Debug, Eq, PartialEq, Hash)]
1162    enum TestWorkerClassification {
1163        B,
1164    }
1165
1166    struct TestWorker {
1167        classification: TestWorkerClassification,
1168    }
1169
1170    #[async_trait::async_trait]
1171    impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
1172        fn is_work(&self) -> bool {
1173            true
1174        }
1175
1176        fn is_valid(&self, c: TestWorkerClassification) -> bool {
1177            self.classification == c
1178        }
1179
1180        fn classification(&self) -> TestWorkerClassification {
1181            self.classification.clone()
1182        }
1183    }
1184
1185    struct TestWorkerFactory;
1186
1187    #[async_trait::async_trait]
1188    impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
1189        async fn create(
1190            &self,
1191            _classification: TestWorkerClassification,
1192        ) -> PoolResult<TestWorker> {
1193            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1194            Err(crate::pool_invalid_config_error("create failed"))
1195        }
1196    }
1197
1198    let pool = new_classified_worker_pool(1, TestWorkerFactory);
1199
1200    let pool_ref = pool.clone();
1201    let worker1 = tokio::spawn(async move {
1202        pool_ref
1203            .get_worker(TestWorkerClassification::B)
1204            .await
1205    });
1206    tokio::time::sleep(std::time::Duration::from_millis(20)).await;
1207
1208    let pool_ref = pool.clone();
1209    let worker2 = tokio::spawn(async move {
1210        pool_ref
1211            .get_worker(TestWorkerClassification::B)
1212            .await
1213    });
1214
1215    let (worker1, worker2) = tokio::time::timeout(std::time::Duration::from_secs(1), async {
1216        (worker1.await.unwrap(), worker2.await.unwrap())
1217    })
1218    .await
1219    .unwrap();
1220
1221    assert_eq!(
1222        worker1.err().unwrap().code(),
1223        crate::PoolErrorCode::InvalidConfig
1224    );
1225    assert_eq!(
1226        worker2.err().unwrap().code(),
1227        crate::PoolErrorCode::InvalidConfig
1228    );
1229}
1230
1231#[tokio::test]
1232async fn test_classified_create_failure_wakes_waiter_to_create() {
1233    use std::sync::atomic::{AtomicUsize, Ordering};
1234    use std::sync::Arc;
1235
1236    #[derive(Clone, Debug, Eq, PartialEq, Hash)]
1237    enum TestWorkerClassification {
1238        A,
1239    }
1240
1241    struct TestWorker {
1242        id: usize,
1243        classification: TestWorkerClassification,
1244    }
1245
1246    #[async_trait::async_trait]
1247    impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
1248        fn is_work(&self) -> bool {
1249            true
1250        }
1251
1252        fn is_valid(&self, c: TestWorkerClassification) -> bool {
1253            self.classification == c
1254        }
1255
1256        fn classification(&self) -> TestWorkerClassification {
1257            self.classification.clone()
1258        }
1259    }
1260
1261    struct TestWorkerFactory {
1262        create_count: Arc<AtomicUsize>,
1263    }
1264
1265    #[async_trait::async_trait]
1266    impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
1267        async fn create(&self, classification: TestWorkerClassification) -> PoolResult<TestWorker> {
1268            let id = self.create_count.fetch_add(1, Ordering::SeqCst);
1269            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1270            if id == 0 && classification == TestWorkerClassification::A {
1271                Err(crate::pool_invalid_config_error("create failed"))
1272            } else {
1273                Ok(TestWorker { id, classification })
1274            }
1275        }
1276    }
1277
1278    let create_count = Arc::new(AtomicUsize::new(0));
1279    let pool = new_classified_worker_pool(
1280        1,
1281        TestWorkerFactory {
1282            create_count: create_count.clone(),
1283        },
1284    );
1285
1286    let pool_ref = pool.clone();
1287    let classified = tokio::spawn(async move {
1288        pool_ref
1289            .get_worker(TestWorkerClassification::A)
1290            .await
1291    });
1292    tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1293
1294    let pool_ref = pool.clone();
1295    let waiter = tokio::spawn(async move {
1296        pool_ref
1297            .get_worker(TestWorkerClassification::A)
1298            .await
1299    });
1300
1301    let (classified, waiter) = tokio::time::timeout(std::time::Duration::from_secs(1), async {
1302        (classified.await.unwrap(), waiter.await.unwrap())
1303    })
1304    .await
1305    .unwrap();
1306
1307    assert_eq!(
1308        classified.err().unwrap().code(),
1309        crate::PoolErrorCode::InvalidConfig
1310    );
1311    let waiter = waiter.unwrap();
1312    assert_eq!(waiter.id, 1);
1313    assert_eq!(create_count.load(Ordering::SeqCst), 2);
1314}
1315
1316#[tokio::test]
1317async fn test_classified_retry_notification_skips_canceled_waiter() {
1318    #[derive(Clone, Debug, Eq, PartialEq, Hash)]
1319    enum TestWorkerClassification {
1320        A,
1321    }
1322
1323    struct TestWorker {
1324        classification: TestWorkerClassification,
1325    }
1326
1327    #[async_trait::async_trait]
1328    impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
1329        fn is_work(&self) -> bool {
1330            true
1331        }
1332
1333        fn is_valid(&self, c: TestWorkerClassification) -> bool {
1334            self.classification == c
1335        }
1336
1337        fn classification(&self) -> TestWorkerClassification {
1338            self.classification.clone()
1339        }
1340    }
1341
1342    struct TestWorkerFactory;
1343
1344    #[async_trait::async_trait]
1345    impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
1346        async fn create(&self, classification: TestWorkerClassification) -> PoolResult<TestWorker> {
1347            Ok(TestWorker { classification })
1348        }
1349    }
1350
1351    let (canceled_notify, canceled_waiter) = Notify::new();
1352    let _classification = TestWorkerClassification::A;
1353    drop(canceled_waiter);
1354    let (notify, waiter) = Notify::new();
1355
1356    ClassifiedWorkerPool::<TestWorkerClassification, TestWorker, TestWorkerFactory>::notify_retry_waiters(
1357        vec![canceled_notify, notify],
1358    );
1359
1360    let result = tokio::time::timeout(std::time::Duration::from_secs(1), waiter)
1361        .await
1362        .unwrap();
1363    assert!(matches!(result, ClassifiedWorkerWaitResult::Retry));
1364}
1365
1366#[tokio::test]
1367async fn test_classified_request_replaces_non_matching_idle_worker() {
1368    use std::sync::atomic::{AtomicUsize, Ordering};
1369    use std::sync::Arc;
1370
1371    #[derive(Clone, Debug, Eq, PartialEq, Hash)]
1372    enum TestWorkerClassification {
1373        A,
1374        B,
1375    }
1376
1377    struct TestWorker {
1378        id: usize,
1379        classification: TestWorkerClassification,
1380    }
1381
1382    #[async_trait::async_trait]
1383    impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
1384        fn is_work(&self) -> bool {
1385            true
1386        }
1387
1388        fn is_valid(&self, c: TestWorkerClassification) -> bool {
1389            self.classification == c
1390        }
1391
1392        fn classification(&self) -> TestWorkerClassification {
1393            self.classification.clone()
1394        }
1395    }
1396
1397    struct TestWorkerFactory {
1398        create_count: Arc<AtomicUsize>,
1399    }
1400
1401    #[async_trait::async_trait]
1402    impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
1403        async fn create(&self, classification: TestWorkerClassification) -> PoolResult<TestWorker> {
1404            let id = self.create_count.fetch_add(1, Ordering::SeqCst);
1405            Ok(TestWorker { id, classification })
1406        }
1407    }
1408
1409    let create_count = Arc::new(AtomicUsize::new(0));
1410    let pool = new_classified_worker_pool(
1411        1,
1412        TestWorkerFactory {
1413            create_count: create_count.clone(),
1414        },
1415    );
1416
1417    {
1418        let worker = pool
1419            .get_worker(TestWorkerClassification::A)
1420            .await
1421            .unwrap();
1422        assert_eq!(worker.id, 0);
1423    }
1424
1425    let worker = pool
1426        .get_worker(TestWorkerClassification::B)
1427        .await
1428        .unwrap();
1429    assert_eq!(worker.id, 1);
1430    assert_eq!(worker.classification(), TestWorkerClassification::B);
1431    assert_eq!(create_count.load(Ordering::SeqCst), 2);
1432}
1433
1434#[tokio::test]
1435async fn test_classified_waiter_replaces_returned_non_matching_worker() {
1436    use std::sync::atomic::{AtomicUsize, Ordering};
1437    use std::sync::Arc;
1438
1439    #[derive(Clone, Debug, Eq, PartialEq, Hash)]
1440    enum TestWorkerClassification {
1441        A,
1442        B,
1443    }
1444
1445    struct TestWorker {
1446        id: usize,
1447        classification: TestWorkerClassification,
1448    }
1449
1450    #[async_trait::async_trait]
1451    impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
1452        fn is_work(&self) -> bool {
1453            true
1454        }
1455
1456        fn is_valid(&self, c: TestWorkerClassification) -> bool {
1457            self.classification == c
1458        }
1459
1460        fn classification(&self) -> TestWorkerClassification {
1461            self.classification.clone()
1462        }
1463    }
1464
1465    struct TestWorkerFactory {
1466        create_count: Arc<AtomicUsize>,
1467    }
1468
1469    #[async_trait::async_trait]
1470    impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
1471        async fn create(&self, classification: TestWorkerClassification) -> PoolResult<TestWorker> {
1472            let id = self.create_count.fetch_add(1, Ordering::SeqCst);
1473            Ok(TestWorker { id, classification })
1474        }
1475    }
1476
1477    let create_count = Arc::new(AtomicUsize::new(0));
1478    let pool = new_classified_worker_pool(
1479        2,
1480        TestWorkerFactory {
1481            create_count: create_count.clone(),
1482        },
1483    );
1484    let worker_a = pool
1485        .get_worker(TestWorkerClassification::A)
1486        .await
1487        .unwrap();
1488    let _worker_b = pool
1489        .get_worker(TestWorkerClassification::B)
1490        .await
1491        .unwrap();
1492
1493    let pool_ref = pool.clone();
1494    let waiter = tokio::spawn(async move {
1495        pool_ref
1496            .get_worker(TestWorkerClassification::B)
1497            .await
1498    });
1499    tokio::time::sleep(std::time::Duration::from_millis(20)).await;
1500    assert!(!waiter.is_finished());
1501
1502    drop(worker_a);
1503    let worker = tokio::time::timeout(std::time::Duration::from_secs(1), waiter)
1504        .await
1505        .unwrap()
1506        .unwrap()
1507        .unwrap();
1508    assert_eq!(worker.id, 2);
1509    assert_eq!(worker.classification(), TestWorkerClassification::B);
1510    assert_eq!(create_count.load(Ordering::SeqCst), 3);
1511}
1512
1513#[tokio::test]
1514async fn test_classified_waiter_replaces_unwork_non_matching_worker() {
1515    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
1516    use std::sync::Arc;
1517
1518    #[derive(Clone, Debug, Eq, PartialEq, Hash)]
1519    enum TestWorkerClassification {
1520        A,
1521        B,
1522    }
1523
1524    struct TestWorker {
1525        id: usize,
1526        work: AtomicBool,
1527        classification: TestWorkerClassification,
1528    }
1529
1530    #[async_trait::async_trait]
1531    impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
1532        fn is_work(&self) -> bool {
1533            self.work.load(Ordering::SeqCst)
1534        }
1535
1536        fn is_valid(&self, c: TestWorkerClassification) -> bool {
1537            self.classification == c
1538        }
1539
1540        fn classification(&self) -> TestWorkerClassification {
1541            self.classification.clone()
1542        }
1543    }
1544
1545    struct TestWorkerFactory {
1546        create_count: Arc<AtomicUsize>,
1547    }
1548
1549    #[async_trait::async_trait]
1550    impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
1551        async fn create(&self, classification: TestWorkerClassification) -> PoolResult<TestWorker> {
1552            let id = self.create_count.fetch_add(1, Ordering::SeqCst);
1553            Ok(TestWorker {
1554                id,
1555                work: AtomicBool::new(true),
1556                classification,
1557            })
1558        }
1559    }
1560
1561    let create_count = Arc::new(AtomicUsize::new(0));
1562    let pool = new_classified_worker_pool(
1563        2,
1564        TestWorkerFactory {
1565            create_count: create_count.clone(),
1566        },
1567    );
1568    let worker_a = pool
1569        .get_worker(TestWorkerClassification::A)
1570        .await
1571        .unwrap();
1572    let _worker_b = pool
1573        .get_worker(TestWorkerClassification::B)
1574        .await
1575        .unwrap();
1576
1577    let pool_ref = pool.clone();
1578    let waiter = tokio::spawn(async move {
1579        pool_ref
1580            .get_worker(TestWorkerClassification::B)
1581            .await
1582    });
1583    tokio::time::sleep(std::time::Duration::from_millis(20)).await;
1584    assert!(!waiter.is_finished());
1585
1586    worker_a.work.store(false, Ordering::SeqCst);
1587    drop(worker_a);
1588
1589    let worker = tokio::time::timeout(std::time::Duration::from_secs(1), waiter)
1590        .await
1591        .unwrap()
1592        .unwrap()
1593        .unwrap();
1594    assert_eq!(worker.id, 2);
1595    assert_eq!(worker.classification(), TestWorkerClassification::B);
1596    assert_eq!(create_count.load(Ordering::SeqCst), 3);
1597}
1598
1599#[tokio::test]
1600async fn test_factory_must_return_matching_classification() {
1601    use std::sync::atomic::{AtomicUsize, Ordering};
1602    use std::sync::Arc;
1603
1604    #[derive(Clone, Debug, Eq, PartialEq, Hash)]
1605    enum TestWorkerClassification {
1606        A,
1607        B,
1608    }
1609
1610    struct TestWorker {
1611        classification: TestWorkerClassification,
1612    }
1613
1614    #[async_trait::async_trait]
1615    impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
1616        fn is_work(&self) -> bool {
1617            true
1618        }
1619
1620        fn is_valid(&self, c: TestWorkerClassification) -> bool {
1621            self.classification == c
1622        }
1623
1624        fn classification(&self) -> TestWorkerClassification {
1625            self.classification.clone()
1626        }
1627    }
1628
1629    struct TestWorkerFactory {
1630        create_count: Arc<AtomicUsize>,
1631    }
1632
1633    #[async_trait::async_trait]
1634    impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
1635        async fn create(&self, classification: TestWorkerClassification) -> PoolResult<TestWorker> {
1636            let count = self.create_count.fetch_add(1, Ordering::SeqCst);
1637            let classification = if count == 0 {
1638                TestWorkerClassification::A
1639            } else {
1640                classification
1641            };
1642            Ok(TestWorker { classification })
1643        }
1644    }
1645
1646    let create_count = Arc::new(AtomicUsize::new(0));
1647    let pool = new_classified_worker_pool(
1648        1,
1649        TestWorkerFactory {
1650            create_count: create_count.clone(),
1651        },
1652    );
1653    let worker = pool
1654        .get_worker(TestWorkerClassification::B)
1655        .await;
1656    assert!(worker.is_err());
1657    assert_eq!(
1658        worker.err().unwrap().code(),
1659        crate::PoolErrorCode::InvalidConfig
1660    );
1661
1662    let worker = pool
1663        .get_worker(TestWorkerClassification::B)
1664        .await;
1665    assert!(worker.is_ok());
1666    assert_eq!(create_count.load(Ordering::SeqCst), 2);
1667}
1668
1669#[tokio::test(flavor = "multi_thread")]
1670async fn test_classified_waiter_keeps_queue_priority_over_later_waiter() {
1671    use std::sync::mpsc;
1672
1673    #[derive(Clone, Debug, Eq, PartialEq, Hash)]
1674    enum TestWorkerClassification {
1675        B,
1676    }
1677
1678    struct TestWorker {
1679        classification: TestWorkerClassification,
1680    }
1681
1682    #[async_trait::async_trait]
1683    impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
1684        fn is_work(&self) -> bool {
1685            true
1686        }
1687
1688        fn is_valid(&self, c: TestWorkerClassification) -> bool {
1689            self.classification == c
1690        }
1691
1692        fn classification(&self) -> TestWorkerClassification {
1693            self.classification.clone()
1694        }
1695    }
1696
1697    struct TestWorkerFactory;
1698
1699    #[async_trait::async_trait]
1700    impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
1701        async fn create(&self, classification: TestWorkerClassification) -> PoolResult<TestWorker> {
1702            Ok(TestWorker { classification })
1703        }
1704    }
1705
1706    let pool = new_classified_worker_pool(1, TestWorkerFactory);
1707    let worker = pool
1708        .get_worker(TestWorkerClassification::B)
1709        .await
1710        .unwrap();
1711
1712    let (tx, rx) = mpsc::channel();
1713
1714    let pool_ref = pool.clone();
1715    let tx_classified = tx.clone();
1716    let classified_task = tokio::spawn(async move {
1717        let _worker = pool_ref
1718            .get_worker(TestWorkerClassification::B)
1719            .await
1720            .unwrap();
1721        tx_classified.send("classified").unwrap();
1722        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1723    });
1724
1725    tokio::time::sleep(std::time::Duration::from_millis(20)).await;
1726
1727    let pool_ref = pool.clone();
1728    let later_task = tokio::spawn(async move {
1729        let _worker = pool_ref
1730            .get_worker(TestWorkerClassification::B)
1731            .await
1732            .unwrap();
1733        tx.send("later").unwrap();
1734    });
1735
1736    tokio::time::sleep(std::time::Duration::from_millis(20)).await;
1737    drop(worker);
1738
1739    let first = rx.recv_timeout(std::time::Duration::from_secs(2)).unwrap();
1740    assert_eq!(first, "classified");
1741
1742    classified_task.await.unwrap();
1743    later_task.await.unwrap();
1744}
1745
1746#[tokio::test]
1747async fn test_factory_worker_must_be_valid_for_its_primary_classification() {
1748    #[derive(Clone, Debug, Eq, PartialEq, Hash)]
1749    enum TestWorkerClassification {
1750        A,
1751        B,
1752    }
1753
1754    struct TestWorker {
1755        classification: TestWorkerClassification,
1756    }
1757
1758    #[async_trait::async_trait]
1759    impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
1760        fn is_work(&self) -> bool {
1761            true
1762        }
1763
1764        fn is_valid(&self, c: TestWorkerClassification) -> bool {
1765            c == TestWorkerClassification::B
1766        }
1767
1768        fn classification(&self) -> TestWorkerClassification {
1769            self.classification.clone()
1770        }
1771    }
1772
1773    struct TestWorkerFactory;
1774
1775    #[async_trait::async_trait]
1776    impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
1777        async fn create(
1778            &self,
1779            _classification: TestWorkerClassification,
1780        ) -> PoolResult<TestWorker> {
1781            Ok(TestWorker {
1782                classification: TestWorkerClassification::A,
1783            })
1784        }
1785    }
1786
1787    let pool = new_classified_worker_pool(1, TestWorkerFactory);
1788    let worker = pool
1789        .get_worker(TestWorkerClassification::A)
1790        .await;
1791    assert!(worker.is_err());
1792    assert_eq!(
1793        worker.err().unwrap().code(),
1794        crate::PoolErrorCode::InvalidConfig
1795    );
1796}
1797
1798#[tokio::test]
1799async fn test_classified_idle_worker_timeout_releases_worker() {
1800    use std::sync::atomic::{AtomicUsize, Ordering};
1801    use std::sync::Arc;
1802
1803    #[derive(Clone, Debug, Eq, PartialEq, Hash)]
1804    enum TestWorkerClassification {
1805        A,
1806        B,
1807    }
1808
1809    struct TestWorker {
1810        id: usize,
1811        classification: TestWorkerClassification,
1812    }
1813
1814    #[async_trait::async_trait]
1815    impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
1816        fn is_work(&self) -> bool {
1817            true
1818        }
1819
1820        fn is_valid(&self, c: TestWorkerClassification) -> bool {
1821            self.classification == c
1822        }
1823
1824        fn classification(&self) -> TestWorkerClassification {
1825            self.classification.clone()
1826        }
1827    }
1828
1829    struct TestWorkerFactory {
1830        create_count: Arc<AtomicUsize>,
1831    }
1832
1833    #[async_trait::async_trait]
1834    impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
1835        async fn create(&self, classification: TestWorkerClassification) -> PoolResult<TestWorker> {
1836            let id = self.create_count.fetch_add(1, Ordering::SeqCst);
1837            Ok(TestWorker { id, classification })
1838        }
1839    }
1840
1841    let create_count = Arc::new(AtomicUsize::new(0));
1842    let pool = ClassifiedWorkerPool::new(
1843        TestWorkerFactory {
1844            create_count: create_count.clone(),
1845        },
1846        ClassifiedWorkerPoolConfig {
1847            max_count: Some(1),
1848            idle_timeout: Some(std::time::Duration::from_millis(30)),
1849            ..Default::default()
1850        },
1851    );
1852
1853    {
1854        let worker = pool
1855            .get_worker(TestWorkerClassification::B)
1856            .await
1857            .unwrap();
1858        assert_eq!(worker.id, 0);
1859        assert_eq!(worker.classification(), TestWorkerClassification::B);
1860    }
1861
1862    tokio::time::sleep(std::time::Duration::from_millis(80)).await;
1863
1864    let worker = pool
1865        .get_worker(TestWorkerClassification::A)
1866        .await
1867        .unwrap();
1868    assert_eq!(worker.id, 1);
1869    assert_eq!(worker.classification(), TestWorkerClassification::A);
1870    assert_eq!(create_count.load(Ordering::SeqCst), 2);
1871}
1872
1873#[tokio::test]
1874async fn test_get_classified_worker_uses_most_recent_matching_idle_worker() {
1875    use std::sync::atomic::{AtomicUsize, Ordering};
1876    use std::sync::Arc;
1877
1878    #[derive(Clone, Debug, Eq, PartialEq, Hash)]
1879    enum TestWorkerClassification {
1880        A,
1881    }
1882
1883    struct TestWorker {
1884        id: usize,
1885        classification: TestWorkerClassification,
1886    }
1887
1888    #[async_trait::async_trait]
1889    impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
1890        fn is_work(&self) -> bool {
1891            true
1892        }
1893
1894        fn is_valid(&self, c: TestWorkerClassification) -> bool {
1895            self.classification == c
1896        }
1897
1898        fn classification(&self) -> TestWorkerClassification {
1899            self.classification.clone()
1900        }
1901    }
1902
1903    struct TestWorkerFactory {
1904        create_count: Arc<AtomicUsize>,
1905    }
1906
1907    #[async_trait::async_trait]
1908    impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
1909        async fn create(&self, classification: TestWorkerClassification) -> PoolResult<TestWorker> {
1910            let id = self.create_count.fetch_add(1, Ordering::SeqCst);
1911            Ok(TestWorker { id, classification })
1912        }
1913    }
1914
1915    let create_count = Arc::new(AtomicUsize::new(0));
1916    let pool = new_classified_worker_pool(
1917        2,
1918        TestWorkerFactory {
1919            create_count: create_count.clone(),
1920        },
1921    );
1922
1923    let worker1 = pool
1924        .get_worker(TestWorkerClassification::A)
1925        .await
1926        .unwrap();
1927    let worker2 = pool
1928        .get_worker(TestWorkerClassification::A)
1929        .await
1930        .unwrap();
1931    assert_eq!(worker1.id, 0);
1932    assert_eq!(worker2.id, 1);
1933
1934    drop(worker1);
1935    drop(worker2);
1936
1937    let worker = pool
1938        .get_worker(TestWorkerClassification::A)
1939        .await
1940        .unwrap();
1941    assert_eq!(worker.id, 1);
1942    assert_eq!(create_count.load(Ordering::SeqCst), 2);
1943}
1944
1945#[tokio::test]
1946async fn test_classified_cleanup_idle_worker_can_be_triggered_externally() {
1947    use std::sync::atomic::{AtomicUsize, Ordering};
1948    use std::sync::Arc;
1949
1950    #[derive(Clone, Debug, Eq, PartialEq, Hash)]
1951    enum TestWorkerClassification {
1952        A,
1953        B,
1954    }
1955
1956    struct TestWorker {
1957        id: usize,
1958        classification: TestWorkerClassification,
1959    }
1960
1961    #[async_trait::async_trait]
1962    impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
1963        fn is_work(&self) -> bool {
1964            true
1965        }
1966
1967        fn is_valid(&self, c: TestWorkerClassification) -> bool {
1968            self.classification == c
1969        }
1970
1971        fn classification(&self) -> TestWorkerClassification {
1972            self.classification.clone()
1973        }
1974    }
1975
1976    struct TestWorkerFactory {
1977        create_count: Arc<AtomicUsize>,
1978    }
1979
1980    #[async_trait::async_trait]
1981    impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
1982        async fn create(&self, classification: TestWorkerClassification) -> PoolResult<TestWorker> {
1983            let id = self.create_count.fetch_add(1, Ordering::SeqCst);
1984            Ok(TestWorker { id, classification })
1985        }
1986    }
1987
1988    let create_count = Arc::new(AtomicUsize::new(0));
1989    let pool = ClassifiedWorkerPool::new(
1990        TestWorkerFactory {
1991            create_count: create_count.clone(),
1992        },
1993        ClassifiedWorkerPoolConfig {
1994            max_count: Some(1),
1995            idle_timeout: Some(std::time::Duration::from_millis(30)),
1996            ..Default::default()
1997        },
1998    );
1999
2000    {
2001        let worker = pool
2002            .get_worker(TestWorkerClassification::B)
2003            .await
2004            .unwrap();
2005        assert_eq!(worker.id, 0);
2006        assert_eq!(worker.classification(), TestWorkerClassification::B);
2007    }
2008
2009    tokio::time::sleep(std::time::Duration::from_millis(80)).await;
2010
2011    assert_eq!(pool.cleanup_idle_worker(), 1);
2012
2013    let worker = pool
2014        .get_worker(TestWorkerClassification::A)
2015        .await
2016        .unwrap();
2017    assert_eq!(worker.id, 1);
2018    assert_eq!(worker.classification(), TestWorkerClassification::A);
2019    assert_eq!(create_count.load(Ordering::SeqCst), 2);
2020}
2021
2022#[tokio::test]
2023async fn test_canceled_classified_create_rolls_back_reservation() {
2024    use std::sync::atomic::{AtomicBool, Ordering};
2025
2026    #[derive(Clone, Debug, Eq, PartialEq, Hash)]
2027    enum TestWorkerClassification {
2028        A,
2029    }
2030
2031    struct TestWorker;
2032
2033    #[async_trait::async_trait]
2034    impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
2035        fn is_work(&self) -> bool {
2036            true
2037        }
2038
2039        fn is_valid(&self, _classification: TestWorkerClassification) -> bool {
2040            true
2041        }
2042
2043        fn classification(&self) -> TestWorkerClassification {
2044            TestWorkerClassification::A
2045        }
2046    }
2047
2048    struct TestWorkerFactory {
2049        create_started: Arc<AtomicBool>,
2050        allow_create: Arc<AtomicBool>,
2051    }
2052
2053    #[async_trait::async_trait]
2054    impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
2055        async fn create(
2056            &self,
2057            _classification: TestWorkerClassification,
2058        ) -> PoolResult<TestWorker> {
2059            self.create_started.store(true, Ordering::SeqCst);
2060            while !self.allow_create.load(Ordering::SeqCst) {
2061                tokio::task::yield_now().await;
2062            }
2063            Ok(TestWorker)
2064        }
2065    }
2066
2067    let create_started = Arc::new(AtomicBool::new(false));
2068    let allow_create = Arc::new(AtomicBool::new(false));
2069    let pool = new_classified_worker_pool(
2070        1,
2071        TestWorkerFactory {
2072            create_started: create_started.clone(),
2073            allow_create: allow_create.clone(),
2074        },
2075    );
2076
2077    let pool_ref = pool.clone();
2078    let create_task = tokio::spawn(async move {
2079        pool_ref
2080            .get_worker(TestWorkerClassification::A)
2081            .await
2082    });
2083    while !create_started.load(Ordering::SeqCst) {
2084        tokio::task::yield_now().await;
2085    }
2086    create_task.abort();
2087    assert!(matches!(create_task.await, Err(err) if err.is_cancelled()));
2088
2089    allow_create.store(true, Ordering::SeqCst);
2090    let worker = tokio::time::timeout(
2091        std::time::Duration::from_secs(1),
2092        pool.get_worker(TestWorkerClassification::A),
2093    )
2094    .await
2095    .unwrap()
2096    .unwrap();
2097    drop(worker);
2098
2099    tokio::time::timeout(std::time::Duration::from_secs(1), pool.clear_all_worker())
2100        .await
2101        .unwrap();
2102}
2103
2104#[tokio::test]
2105async fn test_mutating_worker_classification_removes_returned_worker() {
2106    #[derive(Clone, Debug, Eq, PartialEq, Hash)]
2107    enum TestWorkerClassification {
2108        A,
2109        B,
2110    }
2111
2112    struct TestWorker {
2113        work: bool,
2114        classification: TestWorkerClassification,
2115    }
2116
2117    #[async_trait::async_trait]
2118    impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
2119        fn is_work(&self) -> bool {
2120            self.work
2121        }
2122
2123        fn is_valid(&self, classification: TestWorkerClassification) -> bool {
2124            self.classification == classification
2125        }
2126
2127        fn classification(&self) -> TestWorkerClassification {
2128            self.classification.clone()
2129        }
2130    }
2131
2132    struct TestWorkerFactory;
2133
2134    #[async_trait::async_trait]
2135    impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
2136        async fn create(&self, classification: TestWorkerClassification) -> PoolResult<TestWorker> {
2137            Ok(TestWorker {
2138                work: true,
2139                classification,
2140            })
2141        }
2142    }
2143
2144    let pool = ClassifiedWorkerPool::new(
2145        TestWorkerFactory,
2146        ClassifiedWorkerPoolConfig {
2147            max_count: Some(1),
2148            idle_timeout: None,
2149            max_count_per_classification: Some(1),
2150        },
2151    );
2152    let mut worker = pool
2153        .get_worker(TestWorkerClassification::A)
2154        .await
2155        .unwrap();
2156    worker.classification = TestWorkerClassification::B;
2157    drop(worker);
2158
2159    {
2160        let state = pool.state.lock().unwrap();
2161        assert_eq!(state.current_count, 0);
2162        assert!(state.classified_count_map.is_empty());
2163    }
2164
2165    let worker = tokio::time::timeout(
2166        std::time::Duration::from_secs(1),
2167        pool.get_worker(TestWorkerClassification::A),
2168    )
2169    .await
2170    .unwrap()
2171    .unwrap();
2172    assert_eq!(worker.classification(), TestWorkerClassification::A);
2173}
2174
2175#[cfg(test)]
2176mod affected_path_tests {
2177    use super::*;
2178    use std::collections::VecDeque;
2179    use std::sync::atomic::{AtomicBool, Ordering};
2180    use std::sync::mpsc;
2181
2182    #[derive(Clone, Debug, Eq, PartialEq, Hash)]
2183    enum Classification {
2184        A,
2185        B,
2186        C,
2187    }
2188
2189    struct BlockingWorker {
2190        classification: Classification,
2191    }
2192
2193    #[async_trait::async_trait]
2194    impl ClassifiedWorker<Classification> for BlockingWorker {
2195        fn is_work(&self) -> bool {
2196            true
2197        }
2198
2199        fn is_valid(&self, classification: Classification) -> bool {
2200            self.classification == classification
2201        }
2202
2203        fn classification(&self) -> Classification {
2204            self.classification.clone()
2205        }
2206    }
2207
2208    struct BlockingFactory {
2209        create_started: Arc<AtomicBool>,
2210        allow_create: Arc<AtomicBool>,
2211    }
2212
2213    #[async_trait::async_trait]
2214    impl ClassifiedWorkerFactory<Classification, BlockingWorker> for BlockingFactory {
2215        async fn create(&self, classification: Classification) -> PoolResult<BlockingWorker> {
2216            self.create_started.store(true, Ordering::SeqCst);
2217            while !self.allow_create.load(Ordering::SeqCst) {
2218                tokio::task::yield_now().await;
2219            }
2220            Ok(BlockingWorker { classification })
2221        }
2222    }
2223
2224    async fn wait_for_create(create_started: &AtomicBool) {
2225        while !create_started.load(Ordering::SeqCst) {
2226            tokio::task::yield_now().await;
2227        }
2228    }
2229
2230    fn new_blocking_pool(
2231        max_count: u16,
2232    ) -> (
2233        ClassifiedWorkerPoolRef<Classification, BlockingWorker, BlockingFactory>,
2234        Arc<AtomicBool>,
2235        Arc<AtomicBool>,
2236    ) {
2237        let create_started = Arc::new(AtomicBool::new(false));
2238        let allow_create = Arc::new(AtomicBool::new(false));
2239        let pool = new_classified_worker_pool(
2240            max_count,
2241            BlockingFactory {
2242                create_started: create_started.clone(),
2243                allow_create: allow_create.clone(),
2244            },
2245        );
2246        (pool, create_started, allow_create)
2247    }
2248
2249    #[tokio::test]
2250    async fn test_canceled_create_rolls_back_classified_pool_reservation() {
2251        let (pool, create_started, allow_create) = new_blocking_pool(1);
2252        let pool_ref = pool.clone();
2253        let create_task =
2254            tokio::spawn(async move { pool_ref.get_worker(Classification::A).await });
2255        wait_for_create(&create_started).await;
2256        create_task.abort();
2257        assert!(matches!(create_task.await, Err(err) if err.is_cancelled()));
2258
2259        allow_create.store(true, Ordering::SeqCst);
2260        let worker = tokio::time::timeout(
2261            Duration::from_secs(1),
2262            pool.get_worker(Classification::A),
2263        )
2264        .await
2265        .unwrap()
2266        .unwrap();
2267        drop(worker);
2268        tokio::time::timeout(Duration::from_secs(1), pool.clear_all_worker())
2269            .await
2270            .unwrap();
2271    }
2272
2273    #[tokio::test]
2274    async fn test_canceled_replacement_create_rolls_back_classified_pool_reservation() {
2275        let (pool, create_started, allow_create) = new_blocking_pool(1);
2276        allow_create.store(true, Ordering::SeqCst);
2277        let worker = pool.get_worker(Classification::A).await.unwrap();
2278        drop(worker);
2279
2280        create_started.store(false, Ordering::SeqCst);
2281        allow_create.store(false, Ordering::SeqCst);
2282        let pool_ref = pool.clone();
2283        let create_task =
2284            tokio::spawn(async move { pool_ref.get_worker(Classification::B).await });
2285        wait_for_create(&create_started).await;
2286        create_task.abort();
2287        assert!(matches!(create_task.await, Err(err) if err.is_cancelled()));
2288
2289        allow_create.store(true, Ordering::SeqCst);
2290        let worker = tokio::time::timeout(
2291            Duration::from_secs(1),
2292            pool.get_worker(Classification::B),
2293        )
2294        .await
2295        .unwrap()
2296        .unwrap();
2297        drop(worker);
2298        tokio::time::timeout(Duration::from_secs(1), pool.clear_all_worker())
2299            .await
2300            .unwrap();
2301    }
2302
2303    #[tokio::test]
2304    async fn test_canceled_overcommit_create_rolls_back_classified_pool_reservation() {
2305        let (pool, create_started, allow_create) = new_blocking_pool(1);
2306        allow_create.store(true, Ordering::SeqCst);
2307        let worker_a = pool.get_worker(Classification::A).await.unwrap();
2308
2309        create_started.store(false, Ordering::SeqCst);
2310        allow_create.store(false, Ordering::SeqCst);
2311        let pool_ref = pool.clone();
2312        let create_task =
2313            tokio::spawn(async move { pool_ref.get_worker(Classification::B).await });
2314        wait_for_create(&create_started).await;
2315        create_task.abort();
2316        assert!(matches!(create_task.await, Err(err) if err.is_cancelled()));
2317
2318        {
2319            let state = pool.state.lock().unwrap();
2320            assert_eq!(state.current_count, 1);
2321            assert_eq!(state.reserved_classified_count(&Classification::B), 0);
2322        }
2323
2324        allow_create.store(true, Ordering::SeqCst);
2325        let worker_b = tokio::time::timeout(
2326            Duration::from_secs(1),
2327            pool.get_worker(Classification::B),
2328        )
2329        .await
2330        .unwrap()
2331        .unwrap();
2332        drop(worker_b);
2333        drop(worker_a);
2334        tokio::time::timeout(Duration::from_secs(1), pool.clear_all_worker())
2335            .await
2336            .unwrap();
2337    }
2338
2339    type DropCallback = Box<dyn FnOnce() + Send>;
2340
2341    struct DropProbeWorker {
2342        working: Arc<AtomicBool>,
2343        classification: Classification,
2344        on_drop: Option<DropCallback>,
2345    }
2346
2347    #[async_trait::async_trait]
2348    impl ClassifiedWorker<Classification> for DropProbeWorker {
2349        fn is_work(&self) -> bool {
2350            self.working.load(Ordering::SeqCst)
2351        }
2352
2353        fn is_valid(&self, classification: Classification) -> bool {
2354            self.classification == classification
2355        }
2356
2357        fn classification(&self) -> Classification {
2358            self.classification.clone()
2359        }
2360    }
2361
2362    impl Drop for DropProbeWorker {
2363        fn drop(&mut self) {
2364            if let Some(on_drop) = self.on_drop.take() {
2365                on_drop();
2366            }
2367        }
2368    }
2369
2370    struct DropProbeSpec {
2371        working: Arc<AtomicBool>,
2372        on_drop: Option<DropCallback>,
2373    }
2374
2375    struct DropProbeFactory {
2376        specs: Arc<Mutex<VecDeque<DropProbeSpec>>>,
2377    }
2378
2379    #[async_trait::async_trait]
2380    impl ClassifiedWorkerFactory<Classification, DropProbeWorker> for DropProbeFactory {
2381        async fn create(&self, classification: Classification) -> PoolResult<DropProbeWorker> {
2382            let spec = self.specs.lock().unwrap().pop_front().unwrap();
2383            Ok(DropProbeWorker {
2384                working: spec.working,
2385                classification,
2386                on_drop: spec.on_drop,
2387            })
2388        }
2389    }
2390
2391    type DropProbePool = ClassifiedWorkerPoolRef<Classification, DropProbeWorker, DropProbeFactory>;
2392
2393    fn new_drop_probe_pool(
2394        idle_timeout: Option<Duration>,
2395    ) -> (DropProbePool, Arc<Mutex<VecDeque<DropProbeSpec>>>) {
2396        let specs = Arc::new(Mutex::new(VecDeque::new()));
2397        let pool = ClassifiedWorkerPool::new(
2398            DropProbeFactory {
2399                specs: specs.clone(),
2400            },
2401            ClassifiedWorkerPoolConfig {
2402                max_count: Some(1),
2403                idle_timeout,
2404                ..Default::default()
2405            },
2406        );
2407        (pool, specs)
2408    }
2409
2410    fn drop_lock_probe(
2411        pool: &DropProbePool,
2412        working: Arc<AtomicBool>,
2413    ) -> (DropProbeSpec, mpsc::Receiver<bool>) {
2414        let (tx, rx) = mpsc::channel();
2415        let pool_ref = Arc::downgrade(pool);
2416        let on_drop = Box::new(move || {
2417            let pool_ref = pool_ref.upgrade().unwrap();
2418            tx.send(pool_ref.state.try_lock().is_ok()).unwrap();
2419        });
2420        (
2421            DropProbeSpec {
2422                working,
2423                on_drop: Some(on_drop),
2424            },
2425            rx,
2426        )
2427    }
2428
2429    fn plain_drop_probe_spec() -> DropProbeSpec {
2430        DropProbeSpec {
2431            working: Arc::new(AtomicBool::new(true)),
2432            on_drop: None,
2433        }
2434    }
2435
2436    #[derive(Copy, Clone)]
2437    enum IdleDropPath {
2438        Cleanup,
2439        ClassifiedInvalidScan,
2440        ClassifiedReplacement,
2441        Clear,
2442    }
2443
2444    async fn assert_idle_drop_path_runs_outside_lock(path: IdleDropPath) {
2445        let idle_timeout = matches!(path, IdleDropPath::Cleanup).then_some(Duration::ZERO);
2446        let (pool, specs) = new_drop_probe_pool(idle_timeout);
2447        let working = Arc::new(AtomicBool::new(true));
2448        let (spec, drop_result) = drop_lock_probe(&pool, working.clone());
2449        specs.lock().unwrap().push_back(spec);
2450
2451        let worker = pool.get_worker(Classification::A).await.unwrap();
2452        drop(worker);
2453
2454        match path {
2455            IdleDropPath::Cleanup => {
2456                assert_eq!(pool.cleanup_idle_worker(), 1);
2457            }
2458            IdleDropPath::ClassifiedInvalidScan => {
2459                working.store(false, Ordering::SeqCst);
2460                specs.lock().unwrap().push_back(plain_drop_probe_spec());
2461                let worker = pool.get_worker(Classification::A).await.unwrap();
2462                drop(worker);
2463            }
2464            IdleDropPath::ClassifiedReplacement => {
2465                specs.lock().unwrap().push_back(plain_drop_probe_spec());
2466                let worker = pool.get_worker(Classification::B).await.unwrap();
2467                drop(worker);
2468            }
2469            IdleDropPath::Clear => pool.clear_all_worker().await,
2470        }
2471
2472        assert!(drop_result.recv_timeout(Duration::from_secs(1)).unwrap());
2473    }
2474
2475    #[tokio::test]
2476    async fn test_all_classified_idle_drop_paths_run_outside_state_lock() {
2477        for path in [
2478            IdleDropPath::Cleanup,
2479            IdleDropPath::ClassifiedInvalidScan,
2480            IdleDropPath::ClassifiedReplacement,
2481            IdleDropPath::Clear,
2482        ] {
2483            assert_idle_drop_path_runs_outside_lock(path).await;
2484        }
2485    }
2486
2487    struct MutableWorker {
2488        working: Arc<AtomicBool>,
2489        valid: Arc<AtomicBool>,
2490        classification: Classification,
2491    }
2492
2493    #[async_trait::async_trait]
2494    impl ClassifiedWorker<Classification> for MutableWorker {
2495        fn is_work(&self) -> bool {
2496            self.working.load(Ordering::SeqCst)
2497        }
2498
2499        fn is_valid(&self, classification: Classification) -> bool {
2500            self.valid.load(Ordering::SeqCst) && self.classification == classification
2501        }
2502
2503        fn classification(&self) -> Classification {
2504            self.classification.clone()
2505        }
2506    }
2507
2508    struct MutableWorkerFactory;
2509
2510    #[async_trait::async_trait]
2511    impl ClassifiedWorkerFactory<Classification, MutableWorker> for MutableWorkerFactory {
2512        async fn create(&self, classification: Classification) -> PoolResult<MutableWorker> {
2513            Ok(MutableWorker {
2514                working: Arc::new(AtomicBool::new(true)),
2515                valid: Arc::new(AtomicBool::new(true)),
2516                classification,
2517            })
2518        }
2519    }
2520
2521    type MutablePool = ClassifiedWorkerPoolRef<Classification, MutableWorker, MutableWorkerFactory>;
2522
2523    fn new_mutable_pool(max_count: u16, idle_timeout: Option<Duration>) -> MutablePool {
2524        ClassifiedWorkerPool::new(
2525            MutableWorkerFactory,
2526            ClassifiedWorkerPoolConfig {
2527                max_count: Some(max_count),
2528                idle_timeout,
2529                ..Default::default()
2530            },
2531        )
2532    }
2533
2534    fn new_limited_mutable_pool(max_count: u16, max_count_per_classification: u16) -> MutablePool {
2535        ClassifiedWorkerPool::new(
2536            MutableWorkerFactory,
2537            ClassifiedWorkerPoolConfig {
2538                max_count: Some(max_count),
2539                idle_timeout: None,
2540                max_count_per_classification: Some(max_count_per_classification),
2541            },
2542        )
2543    }
2544
2545    fn assert_accounting_empty(pool: &MutablePool) {
2546        let state = pool.state.lock().unwrap();
2547        assert_eq!(state.current_count, 0);
2548        assert!(state.classified_count_map.is_empty());
2549        assert!(state.pending_classified_count_map.is_empty());
2550    }
2551
2552    fn assert_only_classification(
2553        pool: &MutablePool,
2554        classification: Classification,
2555        count: usize,
2556    ) {
2557        let state = pool.state.lock().unwrap();
2558        assert_eq!(state.current_count, count);
2559        assert_eq!(state.classified_count_map.len(), 1);
2560        assert_eq!(
2561            state.classified_count_map.get(&classification).copied(),
2562            Some(count)
2563        );
2564    }
2565
2566    #[derive(Copy, Clone)]
2567    enum IdleAccountingPath {
2568        Cleanup,
2569        Clear,
2570        ClassifiedInvalidScan,
2571        ClassifiedReplacement,
2572    }
2573
2574    async fn assert_idle_accounting_path(path: IdleAccountingPath) {
2575        let idle_timeout = matches!(path, IdleAccountingPath::Cleanup).then_some(Duration::ZERO);
2576        let pool = new_mutable_pool(1, idle_timeout);
2577        let worker = pool.get_worker(Classification::A).await.unwrap();
2578        let working = worker.working.clone();
2579        drop(worker);
2580
2581        match path {
2582            IdleAccountingPath::Cleanup => {
2583                assert_eq!(pool.cleanup_idle_worker(), 1);
2584                assert_accounting_empty(&pool);
2585            }
2586            IdleAccountingPath::Clear => {
2587                pool.clear_all_worker().await;
2588                assert_accounting_empty(&pool);
2589            }
2590            IdleAccountingPath::ClassifiedInvalidScan => {
2591                working.store(false, Ordering::SeqCst);
2592                let worker = pool.get_worker(Classification::A).await.unwrap();
2593                assert_only_classification(&pool, Classification::A, 1);
2594                worker.working.store(false, Ordering::SeqCst);
2595                drop(worker);
2596                assert_accounting_empty(&pool);
2597            }
2598            IdleAccountingPath::ClassifiedReplacement => {
2599                let worker = pool.get_worker(Classification::B).await.unwrap();
2600                assert_only_classification(&pool, Classification::B, 1);
2601                worker.working.store(false, Ordering::SeqCst);
2602                drop(worker);
2603                assert_accounting_empty(&pool);
2604            }
2605        }
2606    }
2607
2608    #[tokio::test]
2609    async fn test_all_idle_accounting_paths() {
2610        for path in [
2611            IdleAccountingPath::Cleanup,
2612            IdleAccountingPath::Clear,
2613            IdleAccountingPath::ClassifiedInvalidScan,
2614            IdleAccountingPath::ClassifiedReplacement,
2615        ] {
2616            assert_idle_accounting_path(path).await;
2617        }
2618    }
2619
2620    async fn wait_for_waiter(pool: &MutablePool) {
2621        loop {
2622            if !pool.state.lock().unwrap().waiting_list.is_empty() {
2623                return;
2624            }
2625            tokio::task::yield_now().await;
2626        }
2627    }
2628
2629    #[tokio::test]
2630    async fn test_canceled_waiter_is_removed_when_worker_returns() {
2631        let pool = new_limited_mutable_pool(1, 1);
2632        let worker_a = pool.get_worker(Classification::A).await.unwrap();
2633
2634        let pool_ref = pool.clone();
2635        let waiter =
2636            tokio::spawn(async move { pool_ref.get_worker(Classification::A).await });
2637        wait_for_waiter(&pool).await;
2638        waiter.abort();
2639        assert!(matches!(waiter.await, Err(err) if err.is_cancelled()));
2640
2641        drop(worker_a);
2642
2643        let state = pool.state.lock().unwrap();
2644        assert!(state.waiting_list.is_empty());
2645        assert_eq!(state.worker_list.len(), 1);
2646    }
2647
2648    #[tokio::test]
2649    async fn test_worker_invalid_for_primary_classification_wakes_waiter() {
2650        let pool = new_limited_mutable_pool(1, 1);
2651        let worker_a = pool.get_worker(Classification::A).await.unwrap();
2652        let valid = worker_a.valid.clone();
2653
2654        let pool_ref = pool.clone();
2655        let waiting_a =
2656            tokio::spawn(async move { pool_ref.get_worker(Classification::A).await });
2657        wait_for_waiter(&pool).await;
2658
2659        valid.store(false, Ordering::SeqCst);
2660        drop(worker_a);
2661
2662        let replacement_a = tokio::time::timeout(Duration::from_secs(1), waiting_a)
2663            .await
2664            .unwrap()
2665            .unwrap()
2666            .unwrap();
2667        assert_eq!(replacement_a.classification(), Classification::A);
2668    }
2669
2670    #[tokio::test]
2671    async fn test_idle_worker_invalid_for_primary_classification_is_replaced() {
2672        let pool = new_limited_mutable_pool(1, 1);
2673        let worker_a = pool.get_worker(Classification::A).await.unwrap();
2674        let valid = worker_a.valid.clone();
2675        drop(worker_a);
2676
2677        valid.store(false, Ordering::SeqCst);
2678
2679        let replacement_a = tokio::time::timeout(
2680            Duration::from_secs(1),
2681            pool.get_worker(Classification::A),
2682        )
2683        .await
2684        .unwrap()
2685        .unwrap();
2686        assert_eq!(replacement_a.classification(), Classification::A);
2687    }
2688
2689    #[tokio::test]
2690    async fn test_capped_waiter_does_not_replace_other_classification_worker() {
2691        let pool = new_limited_mutable_pool(2, 1);
2692        let worker_a = pool.get_worker(Classification::A).await.unwrap();
2693        let worker_b = pool.get_worker(Classification::B).await.unwrap();
2694
2695        let pool_ref = pool.clone();
2696        let waiting_a =
2697            tokio::spawn(async move { pool_ref.get_worker(Classification::A).await });
2698        wait_for_waiter(&pool).await;
2699
2700        drop(worker_b);
2701        tokio::task::yield_now().await;
2702
2703        {
2704            let state = pool.state.lock().unwrap();
2705            assert_eq!(state.current_count, 2);
2706            assert_eq!(state.worker_list.len(), 1);
2707            assert_eq!(
2708                state.worker_list.front().unwrap().primary_classification,
2709                Classification::B
2710            );
2711        }
2712        assert!(!waiting_a.is_finished());
2713
2714        drop(worker_a);
2715        let replacement_a = tokio::time::timeout(Duration::from_secs(1), waiting_a)
2716            .await
2717            .unwrap()
2718            .unwrap()
2719            .unwrap();
2720        assert_eq!(replacement_a.classification(), Classification::A);
2721    }
2722
2723    #[tokio::test]
2724    async fn test_changed_classification_worker_is_removed_and_waiter_retries() {
2725        let pool = new_mutable_pool(1, None);
2726        let mut worker = pool.get_worker(Classification::A).await.unwrap();
2727        worker.classification = Classification::B;
2728
2729        let pool_ref = pool.clone();
2730        let waiter =
2731            tokio::spawn(async move { pool_ref.get_worker(Classification::A).await });
2732        wait_for_waiter(&pool).await;
2733        drop(worker);
2734
2735        let worker = waiter.await.unwrap().unwrap();
2736        assert_eq!(worker.classification(), Classification::A);
2737        worker.working.store(false, Ordering::SeqCst);
2738        drop(worker);
2739        assert_accounting_empty(&pool);
2740    }
2741
2742    #[tokio::test]
2743    async fn test_cached_classification_is_used_while_clearing() {
2744        let pool = new_mutable_pool(1, None);
2745        let mut worker = pool.get_worker(Classification::A).await.unwrap();
2746        worker.classification = Classification::B;
2747
2748        let pool_ref = pool.clone();
2749        let clear_task = tokio::spawn(async move { pool_ref.clear_all_worker().await });
2750        loop {
2751            if pool.state.lock().unwrap().clearing {
2752                break;
2753            }
2754            tokio::task::yield_now().await;
2755        }
2756        drop(worker);
2757        clear_task.await.unwrap();
2758        assert_accounting_empty(&pool);
2759    }
2760
2761    #[tokio::test]
2762    async fn test_changed_classification_worker_wakes_classified_waiter() {
2763        let pool = new_mutable_pool(2, None);
2764        let mut worker_a = pool.get_worker(Classification::A).await.unwrap();
2765        let worker_b = pool.get_worker(Classification::B).await.unwrap();
2766        worker_a.classification = Classification::C;
2767
2768        let pool_ref = pool.clone();
2769        let waiter =
2770            tokio::spawn(async move { pool_ref.get_worker(Classification::B).await });
2771        wait_for_waiter(&pool).await;
2772        drop(worker_a);
2773
2774        let replacement_b = waiter.await.unwrap().unwrap();
2775        assert_only_classification(&pool, Classification::B, 2);
2776        replacement_b.working.store(false, Ordering::SeqCst);
2777        worker_b.working.store(false, Ordering::SeqCst);
2778        drop(replacement_b);
2779        drop(worker_b);
2780        assert_accounting_empty(&pool);
2781    }
2782
2783    #[tokio::test]
2784    async fn test_changed_classification_overcommit_worker_is_removed() {
2785        let pool = new_mutable_pool(1, None);
2786        let worker_a = pool.get_worker(Classification::A).await.unwrap();
2787        let mut worker_b = pool.get_worker(Classification::B).await.unwrap();
2788        worker_b.classification = Classification::C;
2789        drop(worker_b);
2790
2791        assert_only_classification(&pool, Classification::A, 1);
2792        worker_a.working.store(false, Ordering::SeqCst);
2793        drop(worker_a);
2794        assert_accounting_empty(&pool);
2795    }
2796
2797    #[tokio::test]
2798    async fn test_max_count_per_classification_blocks_only_that_classification() {
2799        let pool = new_limited_mutable_pool(4, 2);
2800        let worker_a1 = pool.get_worker(Classification::A).await.unwrap();
2801        let worker_a2 = pool.get_worker(Classification::A).await.unwrap();
2802
2803        let pool_ref = pool.clone();
2804        let waiting_a =
2805            tokio::spawn(async move { pool_ref.get_worker(Classification::A).await });
2806        wait_for_waiter(&pool).await;
2807        assert!(!waiting_a.is_finished());
2808
2809        let worker_b = pool.get_worker(Classification::B).await.unwrap();
2810        {
2811            let state = pool.state.lock().unwrap();
2812            assert_eq!(state.current_count, 3);
2813            assert_eq!(state.reserved_classified_count(&Classification::A), 2);
2814            assert_eq!(state.reserved_classified_count(&Classification::B), 1);
2815        }
2816
2817        drop(worker_a1);
2818        let replacement_a = tokio::time::timeout(Duration::from_secs(1), waiting_a)
2819            .await
2820            .unwrap()
2821            .unwrap()
2822            .unwrap();
2823        assert_eq!(replacement_a.classification(), Classification::A);
2824
2825        drop(worker_a2);
2826        drop(replacement_a);
2827        drop(worker_b);
2828    }
2829
2830    #[tokio::test]
2831    async fn test_zero_max_count_per_classification_returns_error() {
2832        let pool = new_limited_mutable_pool(1, 0);
2833
2834        let classified_error = pool
2835            .get_worker(Classification::A)
2836            .await
2837            .err()
2838            .unwrap();
2839        assert_eq!(classified_error.code(), crate::PoolErrorCode::InvalidConfig);
2840    }
2841}