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};
10use tokio::sync::Semaphore;
11
12const DEFAULT_MAX_CONCURRENT_CREATION_COUNT: u16 = 20;
13
14pub trait WorkerClassification: Send + 'static + Clone + Hash + Eq + PartialEq {}
15
16impl<T: Send + 'static + Clone + Hash + Eq + PartialEq> WorkerClassification for T {}
17
18#[derive(Debug, Clone)]
19pub struct ClassifiedWorkerPoolConfig {
24 idle_timeout: Option<Duration>,
25 max_concurrent_creation_count: u16,
29 max_count_per_classification: Option<u16>,
33 max_idle_count_per_classification: Option<u16>,
38}
39
40impl Default for ClassifiedWorkerPoolConfig {
41 fn default() -> Self {
42 Self {
43 idle_timeout: None,
44 max_concurrent_creation_count: DEFAULT_MAX_CONCURRENT_CREATION_COUNT,
45 max_count_per_classification: None,
46 max_idle_count_per_classification: None,
47 }
48 }
49}
50
51impl ClassifiedWorkerPoolConfig {
52 pub fn with_max_concurrent_creation_count(mut self, max_count: u16) -> Self {
56 self.max_concurrent_creation_count = max_count;
57 self
58 }
59
60 pub fn with_idle_timeout(mut self, idle_timeout: Option<Duration>) -> Self {
64 self.idle_timeout = idle_timeout;
65 self
66 }
67
68 pub fn with_max_count_per_classification(mut self, max_count: Option<u16>) -> Self {
72 self.max_count_per_classification = max_count;
73 self
74 }
75
76 pub fn with_max_idle_count_per_classification(mut self, max_idle_count: Option<u16>) -> Self {
80 self.max_idle_count_per_classification = max_idle_count;
81 self
82 }
83}
84
85#[async_trait::async_trait]
86pub trait ClassifiedWorker<C: WorkerClassification>: Send + 'static {
91 fn is_work(&self) -> bool;
92 fn is_valid(&self, c: C) -> bool;
96 fn classification(&self) -> C;
100}
101
102pub struct ClassifiedWorkerGuard<
103 C: WorkerClassification,
104 W: ClassifiedWorker<C>,
105 F: ClassifiedWorkerFactory<C, W>,
106> {
107 pool_ref: ClassifiedWorkerPoolRef<C, W, F>,
108 worker: Option<W>,
109 primary_classification: C,
110}
111
112impl<C: WorkerClassification, W: ClassifiedWorker<C>, F: ClassifiedWorkerFactory<C, W>>
113 ClassifiedWorkerGuard<C, W, F>
114{
115 fn new(
116 worker: W,
117 pool_ref: ClassifiedWorkerPoolRef<C, W, F>,
118 primary_classification: C,
119 ) -> Self {
120 ClassifiedWorkerGuard {
121 pool_ref,
122 worker: Some(worker),
123 primary_classification,
124 }
125 }
126}
127
128impl<C: WorkerClassification, W: ClassifiedWorker<C>, F: ClassifiedWorkerFactory<C, W>> DerefMut
129 for ClassifiedWorkerGuard<C, W, F>
130{
131 fn deref_mut(&mut self) -> &mut Self::Target {
132 self.worker.as_mut().unwrap()
133 }
134}
135
136impl<C: WorkerClassification, W: ClassifiedWorker<C>, F: ClassifiedWorkerFactory<C, W>> Deref
137 for ClassifiedWorkerGuard<C, W, F>
138{
139 type Target = W;
140
141 fn deref(&self) -> &Self::Target {
142 self.worker.as_ref().unwrap()
143 }
144}
145
146impl<C: WorkerClassification, W: ClassifiedWorker<C>, F: ClassifiedWorkerFactory<C, W>> Drop
147 for ClassifiedWorkerGuard<C, W, F>
148{
149 fn drop(&mut self) {
150 if let Some(worker) = self.worker.take() {
151 self.pool_ref
152 .release(worker, self.primary_classification.clone());
153 }
154 }
155}
156
157struct ClassifiedWorkerReservation<
158 C: WorkerClassification,
159 W: ClassifiedWorker<C>,
160 F: ClassifiedWorkerFactory<C, W>,
161> {
162 pool_ref: ClassifiedWorkerPoolRef<C, W, F>,
163 requested_classification: Option<C>,
164 active: bool,
165}
166
167enum ReservationCompletion {
168 Complete,
169 Clearing,
170 ClassificationLimitReached,
171}
172
173impl<C: WorkerClassification, W: ClassifiedWorker<C>, F: ClassifiedWorkerFactory<C, W>>
174 ClassifiedWorkerReservation<C, W, F>
175{
176 fn new(pool_ref: ClassifiedWorkerPoolRef<C, W, F>, classification: Option<C>) -> Self {
177 Self {
178 pool_ref,
179 requested_classification: classification,
180 active: true,
181 }
182 }
183
184 fn complete(mut self, worker_classification: C) -> ReservationCompletion {
185 let (completion, retry_waiters, clear_waiters) = {
186 let mut state = self.pool_ref.state.lock().unwrap();
187 if let Some(classification) = self.requested_classification.as_ref() {
188 state.dec_pending_classified_count(classification.clone());
189 }
190 if state.clearing {
191 state.current_count -= 1;
192 (
193 ReservationCompletion::Clearing,
194 Vec::new(),
195 state.take_clear_waiters_if_done(),
196 )
197 } else if self
198 .pool_ref
199 .classification_limit_reached(&state, &worker_classification)
200 {
201 state.current_count -= 1;
202 (
203 ReservationCompletion::ClassificationLimitReached,
204 state.drain_waiters(),
205 Vec::new(),
206 )
207 } else {
208 state.inc_classified_count(worker_classification);
209 (ReservationCompletion::Complete, Vec::new(), Vec::new())
210 }
211 };
212 self.active = false;
213 ClassifiedWorkerPool::<C, W, F>::notify_retry_waiters(retry_waiters);
214 for waiter in clear_waiters {
215 waiter.notify(());
216 }
217 completion
218 }
219}
220
221impl<C: WorkerClassification, W: ClassifiedWorker<C>, F: ClassifiedWorkerFactory<C, W>> Drop
222 for ClassifiedWorkerReservation<C, W, F>
223{
224 fn drop(&mut self) {
225 if self.active {
226 self.pool_ref
227 .rollback_reservation(self.requested_classification.as_ref());
228 }
229 }
230}
231
232#[async_trait::async_trait]
233pub trait ClassifiedWorkerFactory<C: WorkerClassification, W: ClassifiedWorker<C>>:
234 Send + Sync + 'static
235{
236 async fn create(&self, c: Option<C>) -> PoolResult<W>;
237}
238
239struct WaitingItem<
240 C: WorkerClassification,
241 W: ClassifiedWorker<C>,
242 F: ClassifiedWorkerFactory<C, W>,
243> {
244 future: Notify<ClassifiedWorkerWaitResult<C, W, F>>,
245 condition: Option<C>,
246}
247
248struct IdleWorker<C, W> {
249 worker: W,
250 primary_classification: C,
251 idle_since: Instant,
252}
253
254enum ClassifiedWorkerWaitResult<
255 C: WorkerClassification,
256 W: ClassifiedWorker<C>,
257 F: ClassifiedWorkerFactory<C, W>,
258> {
259 Worker(ClassifiedWorkerGuard<C, W, F>),
260 Retry,
261 Error(PoolError),
262}
263
264struct WorkerPoolState<
265 C: WorkerClassification,
266 W: ClassifiedWorker<C>,
267 F: ClassifiedWorkerFactory<C, W>,
268> {
269 current_count: usize,
270 classified_count_map: HashMap<C, usize>,
271 pending_classified_count_map: HashMap<C, usize>,
272 worker_list: HashMap<C, VecDeque<IdleWorker<C, W>>>,
274 waiting_list: Vec<WaitingItem<C, W, F>>,
275 clearing: bool,
276 clear_waiting_list: Vec<Notify<()>>,
277}
278
279impl<C: WorkerClassification, W: ClassifiedWorker<C>, F: ClassifiedWorkerFactory<C, W>>
280 WorkerPoolState<C, W, F>
281{
282 fn push_idle_worker(&mut self, worker: W, primary_classification: C) {
283 let idle_worker = IdleWorker {
284 worker,
285 primary_classification: primary_classification.clone(),
286 idle_since: Instant::now(),
287 };
288 self.worker_list
289 .entry(primary_classification)
290 .or_default()
291 .push_back(idle_worker);
292 }
293
294 fn remove_idle_worker(&mut self, primary_classification: &C, index: usize) -> IdleWorker<C, W> {
295 let (idle_worker, remove_bucket) = {
296 let workers = self.worker_list.get_mut(primary_classification).unwrap();
297 let idle_worker = workers.remove(index).unwrap();
298 (idle_worker, workers.is_empty())
299 };
300 if remove_bucket {
301 self.worker_list.remove(primary_classification);
302 }
303 idle_worker
304 }
305
306 fn next_idle_worker_classification(&self) -> Option<C> {
307 self.worker_list.keys().next().cloned()
308 }
309
310 fn take_idle_worker_for_classification(
311 &mut self,
312 classification: &C,
313 ) -> Option<IdleWorker<C, W>> {
314 let index = self
315 .worker_list
316 .get(classification)
317 .map(VecDeque::len)
318 .and_then(|len| len.checked_sub(1))?;
319 Some(self.remove_idle_worker(classification, index))
320 }
321
322 fn drain_idle_workers(&mut self) -> Vec<IdleWorker<C, W>> {
323 std::mem::take(&mut self.worker_list)
324 .into_values()
325 .flatten()
326 .collect()
327 }
328
329 fn inc_classified_count(&mut self, c: C) {
330 let count = self.classified_count_map.entry(c).or_insert(0);
331 *count += 1;
332 }
333
334 fn dec_classified_count(&mut self, c: C) {
335 let mut should_remove = false;
336 if let Some(count) = self.classified_count_map.get_mut(&c) {
337 debug_assert!(*count > 0);
338 *count -= 1;
339 should_remove = *count == 0;
340 }
341 if should_remove {
342 self.classified_count_map.remove(&c);
343 }
344 }
345
346 fn inc_pending_classified_count(&mut self, c: C) {
347 let count = self.pending_classified_count_map.entry(c).or_insert(0);
348 *count += 1;
349 }
350
351 fn dec_pending_classified_count(&mut self, c: C) {
352 let mut should_remove = false;
353 if let Some(count) = self.pending_classified_count_map.get_mut(&c) {
354 debug_assert!(*count > 0);
355 *count -= 1;
356 should_remove = *count == 0;
357 }
358 if should_remove {
359 self.pending_classified_count_map.remove(&c);
360 }
361 }
362
363 fn reserved_classified_count(&self, c: &C) -> usize {
364 self.classified_count_map.get(c).copied().unwrap_or(0)
365 + self
366 .pending_classified_count_map
367 .get(c)
368 .copied()
369 .unwrap_or(0)
370 }
371
372 fn take_clear_waiters_if_done(&mut self) -> Vec<Notify<()>> {
373 if self.clearing && self.current_count == 0 {
374 self.clearing = false;
375 self.clear_waiting_list.drain(..).collect()
376 } else {
377 Vec::new()
378 }
379 }
380
381 fn find_matching_waiter_index_for_worker(
382 &self,
383 worker: &W,
384 primary_classification: &C,
385 ) -> Option<usize> {
386 self.waiting_list.iter().position(|waiting| {
387 if waiting.future.is_canceled() {
388 return false;
389 }
390 waiting
391 .condition
392 .as_ref()
393 .map(|condition| {
394 condition == primary_classification && worker.is_valid(condition.clone())
395 })
396 .unwrap_or(true)
397 })
398 }
399
400 fn remove_canceled_waiters(&mut self) {
401 self.waiting_list
402 .retain(|waiting| !waiting.future.is_canceled());
403 }
404
405 fn drain_waiters(&mut self) -> Vec<Notify<ClassifiedWorkerWaitResult<C, W, F>>> {
406 self.waiting_list
407 .drain(..)
408 .map(|waiting| waiting.future)
409 .collect()
410 }
411}
412
413pub struct ClassifiedWorkerPool<
414 C: WorkerClassification,
415 W: ClassifiedWorker<C>,
416 F: ClassifiedWorkerFactory<C, W>,
417> {
418 factory: Arc<F>,
419 config: ClassifiedWorkerPoolConfig,
420 creation_semaphore: Semaphore,
421 state: Mutex<WorkerPoolState<C, W, F>>,
422}
423pub type ClassifiedWorkerPoolRef<C, W, F> = Arc<ClassifiedWorkerPool<C, W, F>>;
424
425#[cfg(test)]
426#[test]
427fn test_classified_worker_pool_config_default_idle_limits() {
428 let config = ClassifiedWorkerPoolConfig::default();
429 assert_eq!(
430 config.max_concurrent_creation_count,
431 DEFAULT_MAX_CONCURRENT_CREATION_COUNT
432 );
433 assert_eq!(config.max_idle_count_per_classification, None);
434}
435
436#[cfg(test)]
437#[test]
438fn test_classified_worker_pool_config_builder() {
439 let timeout = Duration::from_secs(1);
440 let config = ClassifiedWorkerPoolConfig::default()
441 .with_max_concurrent_creation_count(3)
442 .with_idle_timeout(Some(timeout))
443 .with_max_count_per_classification(Some(2))
444 .with_max_idle_count_per_classification(Some(1));
445 assert_eq!(config.idle_timeout, Some(timeout));
446 assert_eq!(config.max_concurrent_creation_count, 3);
447 assert_eq!(config.max_count_per_classification, Some(2));
448 assert_eq!(config.max_idle_count_per_classification, Some(1));
449}
450
451#[cfg(test)]
452#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
453async fn test_classified_worker_pool_limits_concurrent_creation() {
454 use std::sync::atomic::{AtomicUsize, Ordering};
455
456 #[derive(Clone, Debug, Eq, Hash, PartialEq)]
457 struct Classification;
458
459 struct TestWorker;
460
461 impl ClassifiedWorker<Classification> for TestWorker {
462 fn is_work(&self) -> bool {
463 true
464 }
465
466 fn is_valid(&self, _classification: Classification) -> bool {
467 true
468 }
469
470 fn classification(&self) -> Classification {
471 Classification
472 }
473 }
474
475 struct BlockingFactory {
476 active: Arc<AtomicUsize>,
477 max_active: Arc<AtomicUsize>,
478 started: Arc<AtomicUsize>,
479 gate: Arc<Semaphore>,
480 }
481
482 #[async_trait::async_trait]
483 impl ClassifiedWorkerFactory<Classification, TestWorker> for BlockingFactory {
484 async fn create(&self, _classification: Option<Classification>) -> PoolResult<TestWorker> {
485 let active = self.active.fetch_add(1, Ordering::SeqCst) + 1;
486 self.max_active.fetch_max(active, Ordering::SeqCst);
487 self.started.fetch_add(1, Ordering::SeqCst);
488 let permit = self.gate.acquire().await.unwrap();
489 permit.forget();
490 self.active.fetch_sub(1, Ordering::SeqCst);
491 Ok(TestWorker)
492 }
493 }
494
495 let active = Arc::new(AtomicUsize::new(0));
496 let max_active = Arc::new(AtomicUsize::new(0));
497 let started = Arc::new(AtomicUsize::new(0));
498 let gate = Arc::new(Semaphore::new(0));
499 let pool = ClassifiedWorkerPool::new(
500 BlockingFactory {
501 active: active.clone(),
502 max_active: max_active.clone(),
503 started: started.clone(),
504 gate: gate.clone(),
505 },
506 ClassifiedWorkerPoolConfig::default().with_max_concurrent_creation_count(2),
507 );
508
509 let pool_ref = pool.clone();
510 let task1 = tokio::spawn(async move { pool_ref.get_worker().await });
511 let pool_ref = pool.clone();
512 let task2 = tokio::spawn(async move { pool_ref.get_worker().await });
513 let pool_ref = pool.clone();
514 let task3 = tokio::spawn(async move { pool_ref.get_worker().await });
515
516 tokio::time::timeout(Duration::from_secs(1), async {
517 while started.load(Ordering::SeqCst) < 2 {
518 tokio::task::yield_now().await;
519 }
520 })
521 .await
522 .unwrap();
523 tokio::time::sleep(Duration::from_millis(20)).await;
524 assert_eq!(started.load(Ordering::SeqCst), 2);
525
526 gate.add_permits(2);
527 tokio::time::timeout(Duration::from_secs(1), async {
528 while started.load(Ordering::SeqCst) < 3 {
529 tokio::task::yield_now().await;
530 }
531 })
532 .await
533 .unwrap();
534 gate.add_permits(1);
535
536 let (worker1, worker2, worker3) = tokio::time::timeout(Duration::from_secs(1), async {
537 (
538 task1.await.unwrap().unwrap(),
539 task2.await.unwrap().unwrap(),
540 task3.await.unwrap().unwrap(),
541 )
542 })
543 .await
544 .unwrap();
545 assert_eq!(max_active.load(Ordering::SeqCst), 2);
546 drop((worker1, worker2, worker3));
547
548 let zero_pool = ClassifiedWorkerPool::new(
549 BlockingFactory {
550 active,
551 max_active,
552 started,
553 gate,
554 },
555 ClassifiedWorkerPoolConfig::default().with_max_concurrent_creation_count(0),
556 );
557 let error = zero_pool.get_worker().await.err().unwrap();
558 assert_eq!(error.code(), crate::PoolErrorCode::InvalidConfig);
559}
560
561impl<C: WorkerClassification, W: ClassifiedWorker<C>, F: ClassifiedWorkerFactory<C, W>>
562 ClassifiedWorkerPool<C, W, F>
563{
564 fn classification_limit_reached(&self, state: &WorkerPoolState<C, W, F>, c: &C) -> bool {
565 self.config
566 .max_count_per_classification
567 .map(|max_count| state.reserved_classified_count(c) >= usize::from(max_count))
568 .unwrap_or(false)
569 }
570
571 fn validate_created_worker(requested_classification: Option<&C>, worker: &W) -> PoolResult<C> {
572 let worker_classification = worker.classification();
573 if !worker.is_valid(worker_classification.clone()) {
574 return Err(pool_invalid_config_error(
575 "worker primary classification is not valid for itself",
576 ));
577 }
578 if let Some(classification) = requested_classification {
579 if worker_classification != classification.clone() {
580 return Err(pool_invalid_config_error(
581 "factory returned worker with mismatched classification",
582 ));
583 }
584 }
585 Ok(worker_classification)
586 }
587
588 pub fn new(factory: F, config: ClassifiedWorkerPoolConfig) -> ClassifiedWorkerPoolRef<C, W, F> {
590 let creation_semaphore = Semaphore::new(usize::from(config.max_concurrent_creation_count));
591 Arc::new(ClassifiedWorkerPool {
592 factory: Arc::new(factory),
593 config,
594 creation_semaphore,
595 state: Mutex::new(WorkerPoolState {
596 current_count: 0,
597 classified_count_map: HashMap::new(),
598 pending_classified_count_map: HashMap::new(),
599 worker_list: HashMap::new(),
600 waiting_list: Vec::new(),
601 clearing: false,
602 clear_waiting_list: Vec::new(),
603 }),
604 })
605 }
606
607 async fn create_worker(&self, classification: Option<C>) -> PoolResult<W> {
608 let _permit = self
609 .creation_semaphore
610 .acquire()
611 .await
612 .expect("creation semaphore is never closed");
613 if self.state.lock().unwrap().clearing {
614 return Err(pool_cleared_error());
615 }
616 self.factory.create(classification).await
617 }
618
619 fn take_expired_idle_workers(
620 state: &mut WorkerPoolState<C, W, F>,
621 idle_timeout: Option<std::time::Duration>,
622 ) -> Vec<IdleWorker<C, W>> {
623 let Some(idle_timeout) = idle_timeout else {
624 return Vec::new();
625 };
626 let mut removed_workers = Vec::new();
627 let now = Instant::now();
628 for workers in state.worker_list.values_mut() {
629 while workers
630 .front()
631 .map(|idle_worker| now.duration_since(idle_worker.idle_since) >= idle_timeout)
632 .unwrap_or(false)
633 {
634 removed_workers.push(workers.pop_front().unwrap());
635 }
636 }
637 state.worker_list.retain(|_, workers| !workers.is_empty());
638 for idle_worker in &removed_workers {
639 state.current_count -= 1;
640 state.dec_classified_count(idle_worker.primary_classification.clone());
641 }
642 removed_workers
643 }
644
645 fn take_expired_idle_workers_for_classification(
646 state: &mut WorkerPoolState<C, W, F>,
647 classification: &C,
648 idle_timeout: Option<Duration>,
649 ) -> Vec<IdleWorker<C, W>> {
650 let Some(idle_timeout) = idle_timeout else {
651 return Vec::new();
652 };
653 let mut removed_workers = Vec::new();
654 let now = Instant::now();
655 let remove_bucket = if let Some(workers) = state.worker_list.get_mut(classification) {
656 while workers
657 .front()
658 .map(|idle_worker| now.duration_since(idle_worker.idle_since) >= idle_timeout)
659 .unwrap_or(false)
660 {
661 removed_workers.push(workers.pop_front().unwrap());
662 }
663 workers.is_empty()
664 } else {
665 false
666 };
667 if remove_bucket {
668 state.worker_list.remove(classification);
669 }
670 for idle_worker in &removed_workers {
671 state.current_count -= 1;
672 state.dec_classified_count(idle_worker.primary_classification.clone());
673 }
674 removed_workers
675 }
676
677 pub fn cleanup_idle_worker(&self) -> usize {
678 let (removed_workers, clear_waiters) = {
679 let mut state = self.state.lock().unwrap();
680 let removed_workers =
681 Self::take_expired_idle_workers(&mut state, self.config.idle_timeout);
682 let clear_waiters = state.take_clear_waiters_if_done();
683 (removed_workers, clear_waiters)
684 };
685 for waiter in clear_waiters {
686 waiter.notify(());
687 }
688 let removed_count = removed_workers.len();
689 drop(removed_workers);
690 removed_count
691 }
692
693 pub async fn get_worker(
694 self: &ClassifiedWorkerPoolRef<C, W, F>,
695 ) -> PoolResult<ClassifiedWorkerGuard<C, W, F>> {
696 let mut blocked_classification = None;
697 loop {
698 if self.config.max_concurrent_creation_count == 0 {
699 return Err(pool_invalid_config_error(
700 "pool max_concurrent_creation_count is zero",
701 ));
702 }
703 if self.config.max_count_per_classification == Some(0) {
704 return Err(pool_invalid_config_error(
705 "pool max_count_per_classification is zero",
706 ));
707 }
708
709 let (worker, wait, should_create, removed_workers) = {
710 let mut state = self.state.lock().unwrap();
711 if state.clearing {
712 return Err(pool_clearing_error());
713 }
714 state.remove_canceled_waiters();
715
716 let mut removed_workers = Vec::new();
717
718 let worker = loop {
719 let Some(primary_classification) = state.next_idle_worker_classification()
720 else {
721 break None;
722 };
723 removed_workers.extend(Self::take_expired_idle_workers_for_classification(
724 &mut state,
725 &primary_classification,
726 self.config.idle_timeout,
727 ));
728 let Some(idle_worker) =
729 state.take_idle_worker_for_classification(&primary_classification)
730 else {
731 continue;
732 };
733 let expired = self
734 .config
735 .idle_timeout
736 .map(|idle_timeout| idle_worker.idle_since.elapsed() >= idle_timeout)
737 .unwrap_or(false);
738 if expired
739 || !idle_worker.worker.is_work()
740 || idle_worker.worker.classification() != idle_worker.primary_classification
741 || !idle_worker
742 .worker
743 .is_valid(idle_worker.primary_classification.clone())
744 {
745 state.current_count -= 1;
746 state.dec_classified_count(idle_worker.primary_classification.clone());
747 removed_workers.push(idle_worker);
748 continue;
749 }
750 break Some((idle_worker.worker, idle_worker.primary_classification));
751 };
752
753 if worker.is_some() {
754 (worker, None, false, removed_workers)
755 } else if blocked_classification
756 .as_ref()
757 .map(|classification| self.classification_limit_reached(&state, classification))
758 .unwrap_or(false)
759 {
760 let (notify, waiter) = Notify::new();
761 state.waiting_list.push(WaitingItem {
762 future: notify,
763 condition: None,
764 });
765 (None, Some(waiter), false, removed_workers)
766 } else {
767 state.current_count += 1;
768 (None, None, true, removed_workers)
769 }
770 };
771
772 let reservation =
773 should_create.then(|| ClassifiedWorkerReservation::new(self.clone(), None));
774 drop(removed_workers);
775
776 if let Some((worker, primary_classification)) = worker {
777 return Ok(ClassifiedWorkerGuard::new(
778 worker,
779 self.clone(),
780 primary_classification,
781 ));
782 }
783
784 if let Some(wait) = wait {
785 match wait.await {
786 ClassifiedWorkerWaitResult::Worker(worker) => return Ok(worker),
787 ClassifiedWorkerWaitResult::Retry => continue,
788 ClassifiedWorkerWaitResult::Error(err) => return Err(err),
789 }
790 }
791
792 let reservation = reservation.unwrap();
793 let (worker, primary_classification) = match self.create_worker(None).await {
794 Ok(worker) => {
795 let primary_classification = Self::validate_created_worker(None, &worker)?;
796 (worker, primary_classification)
797 }
798 Err(err) => return Err(err),
799 };
800 match reservation.complete(primary_classification.clone()) {
801 ReservationCompletion::Complete => {}
802 ReservationCompletion::Clearing => return Err(pool_cleared_error()),
803 ReservationCompletion::ClassificationLimitReached => {
804 blocked_classification = Some(primary_classification);
805 drop(worker);
806 continue;
807 }
808 }
809 return Ok(ClassifiedWorkerGuard::new(
810 worker,
811 self.clone(),
812 primary_classification,
813 ));
814 }
815 }
816
817 pub async fn get_classified_worker(
818 self: &ClassifiedWorkerPoolRef<C, W, F>,
819 classification: C,
820 ) -> PoolResult<ClassifiedWorkerGuard<C, W, F>> {
821 loop {
822 if self.config.max_concurrent_creation_count == 0 {
823 return Err(pool_invalid_config_error(
824 "pool max_concurrent_creation_count is zero",
825 ));
826 }
827 if self.config.max_count_per_classification == Some(0) {
828 return Err(pool_invalid_config_error(
829 "pool max_count_per_classification is zero",
830 ));
831 }
832
833 let (worker, wait, should_create, removed_workers) = {
834 let mut state = self.state.lock().unwrap();
835 if state.clearing {
836 return Err(pool_clearing_error());
837 }
838 state.remove_canceled_waiters();
839
840 let mut removed_workers = Self::take_expired_idle_workers_for_classification(
841 &mut state,
842 &classification,
843 self.config.idle_timeout,
844 );
845
846 let worker = loop {
847 let Some(idle_worker) =
848 state.take_idle_worker_for_classification(&classification)
849 else {
850 break None;
851 };
852 let expired = self
853 .config
854 .idle_timeout
855 .map(|idle_timeout| idle_worker.idle_since.elapsed() >= idle_timeout)
856 .unwrap_or(false);
857 if expired
858 || !idle_worker.worker.is_work()
859 || idle_worker.worker.classification() != idle_worker.primary_classification
860 || !idle_worker
861 .worker
862 .is_valid(idle_worker.primary_classification.clone())
863 {
864 state.current_count -= 1;
865 state.dec_classified_count(idle_worker.primary_classification.clone());
866 removed_workers.push(idle_worker);
867 continue;
868 }
869 break Some((idle_worker.worker, idle_worker.primary_classification));
870 };
871
872 if worker.is_some() {
873 (worker, None, false, removed_workers)
874 } else if self.classification_limit_reached(&state, &classification) {
875 let (notify, waiter) = Notify::new();
876 state.waiting_list.push(WaitingItem {
877 future: notify,
878 condition: Some(classification.clone()),
879 });
880 (None, Some(waiter), false, removed_workers)
881 } else {
882 state.current_count += 1;
883 state.inc_pending_classified_count(classification.clone());
884 (None, None, true, removed_workers)
885 }
886 };
887
888 let reservation = should_create.then(|| {
889 ClassifiedWorkerReservation::new(self.clone(), Some(classification.clone()))
890 });
891 drop(removed_workers);
892
893 if let Some((worker, primary_classification)) = worker {
894 return Ok(ClassifiedWorkerGuard::new(
895 worker,
896 self.clone(),
897 primary_classification,
898 ));
899 }
900
901 if let Some(wait) = wait {
902 match wait.await {
903 ClassifiedWorkerWaitResult::Worker(worker) => return Ok(worker),
904 ClassifiedWorkerWaitResult::Retry => continue,
905 ClassifiedWorkerWaitResult::Error(err) => return Err(err),
906 }
907 }
908
909 let reservation = reservation.unwrap();
910 let (worker, primary_classification) =
911 match self.create_worker(Some(classification.clone())).await {
912 Ok(worker) => {
913 let primary_classification =
914 Self::validate_created_worker(Some(&classification), &worker)?;
915 (worker, primary_classification)
916 }
917 Err(err) => return Err(err),
918 };
919 match reservation.complete(primary_classification.clone()) {
920 ReservationCompletion::Complete => {}
921 ReservationCompletion::Clearing => return Err(pool_cleared_error()),
922 ReservationCompletion::ClassificationLimitReached => {
923 drop(worker);
924 continue;
925 }
926 }
927 return Ok(ClassifiedWorkerGuard::new(
928 worker,
929 self.clone(),
930 primary_classification,
931 ));
932 }
933 }
934
935 pub async fn clear_all_worker(&self) {
936 let (waiter, waiting_list, clear_waiters, idle_workers) = {
937 let mut state = self.state.lock().unwrap();
938 let idle_workers = if !state.clearing {
939 state.clearing = true;
940 let idle_workers = state.drain_idle_workers();
941 let cur_worker_count = idle_workers.len();
942 state.current_count -= cur_worker_count;
943 for idle_worker in &idle_workers {
944 state.dec_classified_count(idle_worker.primary_classification.clone());
945 }
946 idle_workers
947 } else {
948 Vec::new()
949 };
950
951 let waiting_list = state.waiting_list.drain(..).collect::<Vec<_>>();
952 if state.current_count == 0 {
953 let clear_waiters = state.take_clear_waiters_if_done();
954 (None, waiting_list, clear_waiters, idle_workers)
955 } else {
956 let (notify, waiter) = Notify::new();
957 state.clear_waiting_list.push(notify);
958 (Some(waiter), waiting_list, Vec::new(), idle_workers)
959 }
960 };
961 for waiting in waiting_list {
962 waiting
963 .future
964 .notify(ClassifiedWorkerWaitResult::Error(pool_cleared_error()));
965 }
966 for waiter in clear_waiters {
967 waiter.notify(());
968 }
969 drop(idle_workers);
970 if let Some(waiter) = waiter {
971 waiter.await;
972 }
973 }
974
975 fn notify_retry_waiters(waiters: Vec<Notify<ClassifiedWorkerWaitResult<C, W, F>>>) {
976 for waiter in waiters {
977 waiter.notify(ClassifiedWorkerWaitResult::Retry);
978 }
979 }
980
981 fn rollback_reservation(&self, classification: Option<&C>) {
982 let (retry_waiters, clear_waiters) = {
983 let mut state = self.state.lock().unwrap();
984 state.current_count -= 1;
985 if let Some(classification) = classification {
986 state.dec_pending_classified_count(classification.clone());
987 }
988 let retry_waiters = state.drain_waiters();
989 let clear_waiters = state.take_clear_waiters_if_done();
990 (retry_waiters, clear_waiters)
991 };
992 Self::notify_retry_waiters(retry_waiters);
993 for waiter in clear_waiters {
994 waiter.notify(());
995 }
996 }
997
998 fn release(self: &ClassifiedWorkerPoolRef<C, W, F>, work: W, primary_classification: C) {
999 enum ReleaseAction<
1000 C: WorkerClassification,
1001 W: ClassifiedWorker<C>,
1002 F: ClassifiedWorkerFactory<C, W>,
1003 > {
1004 None,
1005 Notify(
1006 Notify<ClassifiedWorkerWaitResult<C, W, F>>,
1007 ClassifiedWorkerGuard<C, W, F>,
1008 ),
1009 Retry(Vec<Notify<ClassifiedWorkerWaitResult<C, W, F>>>),
1010 }
1011
1012 let primary_classification_valid = work.classification() == primary_classification
1013 && work.is_valid(primary_classification.clone());
1014 let mut clear_waiters = Vec::new();
1015 let mut removed_workers = Vec::new();
1016 let action = {
1017 let mut state = self.state.lock().unwrap();
1018 state.remove_canceled_waiters();
1019 if state.clearing {
1020 state.current_count -= 1;
1021 state.dec_classified_count(primary_classification);
1022 clear_waiters = state.take_clear_waiters_if_done();
1023 ReleaseAction::None
1024 } else if !primary_classification_valid {
1025 state.current_count -= 1;
1026 state.dec_classified_count(primary_classification);
1027 let waiters = state.drain_waiters();
1028 if !waiters.is_empty() {
1029 ReleaseAction::Retry(waiters)
1030 } else {
1031 ReleaseAction::None
1032 }
1033 } else if work.is_work() {
1034 if let Some(index) =
1035 state.find_matching_waiter_index_for_worker(&work, &primary_classification)
1036 {
1037 let waiting_item = state.waiting_list.remove(index);
1038 ReleaseAction::Notify(
1039 waiting_item.future,
1040 ClassifiedWorkerGuard::new(work, self.clone(), primary_classification),
1041 )
1042 } else {
1043 state.push_idle_worker(work, primary_classification.clone());
1044 if let Some(max_idle_count_per_classification) =
1045 self.config.max_idle_count_per_classification
1046 {
1047 while state
1048 .worker_list
1049 .get(&primary_classification)
1050 .map(VecDeque::len)
1051 .unwrap_or(0)
1052 > usize::from(max_idle_count_per_classification)
1053 {
1054 let idle_worker = state.remove_idle_worker(&primary_classification, 0);
1055 state.current_count -= 1;
1056 state.dec_classified_count(idle_worker.primary_classification.clone());
1057 removed_workers.push(idle_worker);
1058 }
1059 }
1060 ReleaseAction::None
1061 }
1062 } else {
1063 state.dec_classified_count(primary_classification);
1064 state.current_count -= 1;
1065 let waiters = state.drain_waiters();
1066 if !waiters.is_empty() {
1067 ReleaseAction::Retry(waiters)
1068 } else {
1069 clear_waiters = state.take_clear_waiters_if_done();
1070 ReleaseAction::None
1071 }
1072 }
1073 };
1074
1075 for waiter in clear_waiters {
1076 waiter.notify(());
1077 }
1078 drop(removed_workers);
1079
1080 match action {
1081 ReleaseAction::None => {}
1082 ReleaseAction::Notify(waiting, worker) => {
1083 waiting.notify(ClassifiedWorkerWaitResult::Worker(worker));
1084 }
1085 ReleaseAction::Retry(waiters) => {
1086 Self::notify_retry_waiters(waiters);
1087 }
1088 }
1089 }
1090}
1091
1092#[cfg(test)]
1093mod idle_limit_tests {
1094 use super::*;
1095 use std::sync::atomic::{AtomicUsize, Ordering};
1096
1097 #[derive(Clone, Debug, Eq, Hash, PartialEq)]
1098 enum Classification {
1099 A,
1100 B,
1101 }
1102
1103 struct TestWorker {
1104 id: usize,
1105 classification: Classification,
1106 }
1107
1108 impl ClassifiedWorker<Classification> for TestWorker {
1109 fn is_work(&self) -> bool {
1110 true
1111 }
1112
1113 fn is_valid(&self, classification: Classification) -> bool {
1114 self.classification == classification
1115 || (self.classification == Classification::A && classification == Classification::B)
1116 }
1117
1118 fn classification(&self) -> Classification {
1119 self.classification.clone()
1120 }
1121 }
1122
1123 struct TestFactory(AtomicUsize);
1124
1125 #[async_trait::async_trait]
1126 impl ClassifiedWorkerFactory<Classification, TestWorker> for TestFactory {
1127 async fn create(&self, classification: Option<Classification>) -> PoolResult<TestWorker> {
1128 Ok(TestWorker {
1129 id: self.0.fetch_add(1, Ordering::SeqCst),
1130 classification: classification.unwrap_or(Classification::A),
1131 })
1132 }
1133 }
1134
1135 #[tokio::test]
1136 async fn classification_idle_limits_preserve_mru_workers_per_bucket() {
1137 let pool = ClassifiedWorkerPool::new(
1138 TestFactory(AtomicUsize::new(0)),
1139 ClassifiedWorkerPoolConfig {
1140 idle_timeout: None,
1141 max_count_per_classification: None,
1142 max_idle_count_per_classification: Some(1),
1143 ..Default::default()
1144 },
1145 );
1146 let a0 = pool.get_classified_worker(Classification::A).await.unwrap();
1147 let a1 = pool.get_classified_worker(Classification::A).await.unwrap();
1148 let b2 = pool.get_classified_worker(Classification::B).await.unwrap();
1149 drop(a0);
1150 drop(b2);
1151 drop(a1);
1152
1153 let a = pool.get_classified_worker(Classification::A).await.unwrap();
1154 let b = pool.get_classified_worker(Classification::B).await.unwrap();
1155 assert_eq!(a.id, 1);
1156 assert_eq!(b.id, 2);
1157 }
1158
1159 #[tokio::test]
1160 async fn acquisition_cleans_expired_worker_behind_bucket_mru() {
1161 let pool = ClassifiedWorkerPool::new(
1162 TestFactory(AtomicUsize::new(0)),
1163 ClassifiedWorkerPoolConfig::default()
1164 .with_idle_timeout(Some(Duration::from_millis(20))),
1165 );
1166 let a0 = pool.get_classified_worker(Classification::A).await.unwrap();
1167 let a1 = pool.get_classified_worker(Classification::A).await.unwrap();
1168
1169 drop(a0);
1170 tokio::time::sleep(Duration::from_millis(30)).await;
1171 drop(a1);
1172
1173 let a = pool.get_classified_worker(Classification::A).await.unwrap();
1174 assert_eq!(a.id, 1);
1175 assert_eq!(pool.cleanup_idle_worker(), 0);
1176 }
1177
1178 #[tokio::test]
1179 async fn classified_acquisition_only_cleans_and_reuses_requested_bucket() {
1180 let pool = ClassifiedWorkerPool::new(
1181 TestFactory(AtomicUsize::new(0)),
1182 ClassifiedWorkerPoolConfig::default()
1183 .with_idle_timeout(Some(Duration::from_millis(20))),
1184 );
1185 let a = pool.get_classified_worker(Classification::A).await.unwrap();
1186 drop(a);
1187 tokio::time::sleep(Duration::from_millis(30)).await;
1188
1189 let b = pool.get_classified_worker(Classification::B).await.unwrap();
1190 assert_eq!(b.id, 1);
1191 assert_eq!(b.classification(), Classification::B);
1192 assert_eq!(pool.cleanup_idle_worker(), 1);
1193 }
1194
1195 #[tokio::test]
1196 async fn generic_acquisition_only_cleans_visited_buckets() {
1197 let pool = ClassifiedWorkerPool::new(
1198 TestFactory(AtomicUsize::new(0)),
1199 ClassifiedWorkerPoolConfig::default()
1200 .with_idle_timeout(Some(Duration::from_millis(20))),
1201 );
1202 let a0 = pool.get_classified_worker(Classification::A).await.unwrap();
1203 let a1 = pool.get_classified_worker(Classification::A).await.unwrap();
1204 let b0 = pool.get_classified_worker(Classification::B).await.unwrap();
1205 let b1 = pool.get_classified_worker(Classification::B).await.unwrap();
1206 drop(a0);
1207 drop(b0);
1208 tokio::time::sleep(Duration::from_millis(30)).await;
1209 drop(a1);
1210 drop(b1);
1211
1212 let worker = pool.get_worker().await.unwrap();
1213 assert_eq!(pool.cleanup_idle_worker(), 1);
1214 drop(worker);
1215 }
1216
1217 #[tokio::test]
1218 async fn returned_worker_only_wakes_waiter_for_its_primary_classification() {
1219 let pool = ClassifiedWorkerPool::new(
1220 TestFactory(AtomicUsize::new(0)),
1221 ClassifiedWorkerPoolConfig::default().with_max_count_per_classification(Some(1)),
1222 );
1223 let a = pool.get_classified_worker(Classification::A).await.unwrap();
1224 let b = pool.get_classified_worker(Classification::B).await.unwrap();
1225 let pool_ref = pool.clone();
1226 let waiting_b =
1227 tokio::spawn(async move { pool_ref.get_classified_worker(Classification::B).await });
1228 tokio::time::sleep(Duration::from_millis(20)).await;
1229
1230 drop(a);
1231 tokio::task::yield_now().await;
1232 assert!(!waiting_b.is_finished());
1233
1234 drop(b);
1235 let b = tokio::time::timeout(Duration::from_secs(1), waiting_b)
1236 .await
1237 .unwrap()
1238 .unwrap()
1239 .unwrap();
1240 assert_eq!(b.classification(), Classification::B);
1241 }
1242
1243 #[tokio::test]
1244 async fn zero_per_classification_idle_limit_disables_idle_cache() {
1245 let pool = ClassifiedWorkerPool::new(
1246 TestFactory(AtomicUsize::new(0)),
1247 ClassifiedWorkerPoolConfig {
1248 max_idle_count_per_classification: Some(0),
1249 ..Default::default()
1250 },
1251 );
1252 let a = pool.get_classified_worker(Classification::A).await.unwrap();
1253 assert_eq!(a.id, 0);
1254 drop(a);
1255 let a = pool.get_classified_worker(Classification::A).await.unwrap();
1256 assert_eq!(a.id, 1);
1257 }
1258}
1259
1260#[cfg(test)]
1261fn new_classified_worker_pool<
1262 C: WorkerClassification,
1263 W: ClassifiedWorker<C>,
1264 F: ClassifiedWorkerFactory<C, W>,
1265>(
1266 max_count: u16,
1267 factory: F,
1268 mut config: ClassifiedWorkerPoolConfig,
1269) -> ClassifiedWorkerPoolRef<C, W, F> {
1270 if config.max_count_per_classification.is_none() {
1271 config.max_count_per_classification = Some(max_count);
1272 }
1273 ClassifiedWorkerPool::new(factory, config)
1274}
1275
1276#[tokio::test]
1277async fn test_pool() {
1278 struct TestWorker {
1279 work: bool,
1280 classification: TestWorkerClassification,
1281 }
1282
1283 #[derive(Clone, Debug, Eq, PartialEq, Hash)]
1284 enum TestWorkerClassification {
1285 A,
1286 B,
1287 }
1288 #[async_trait::async_trait]
1289 impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
1290 fn is_work(&self) -> bool {
1291 self.work
1292 }
1293
1294 fn is_valid(&self, c: TestWorkerClassification) -> bool {
1295 self.classification == c
1296 }
1297
1298 fn classification(&self) -> TestWorkerClassification {
1299 self.classification.clone()
1300 }
1301 }
1302
1303 struct TestWorkerFactory;
1304
1305 #[async_trait::async_trait]
1306 impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
1307 async fn create(
1308 &self,
1309 classification: Option<TestWorkerClassification>,
1310 ) -> PoolResult<TestWorker> {
1311 if let Some(classification) = classification {
1312 Ok(TestWorker {
1313 work: true,
1314 classification,
1315 })
1316 } else {
1317 Ok(TestWorker {
1318 work: true,
1319 classification: TestWorkerClassification::A,
1320 })
1321 }
1322 }
1323 }
1324
1325 let pool = ClassifiedWorkerPool::new(
1326 TestWorkerFactory,
1327 ClassifiedWorkerPoolConfig {
1328 max_count_per_classification: Some(2),
1329 ..Default::default()
1330 },
1331 );
1332
1333 let worker_a1 = pool.get_worker().await.unwrap();
1334 let worker_a2 = pool.get_worker().await.unwrap();
1335 let worker_b1 = pool
1336 .get_classified_worker(TestWorkerClassification::B)
1337 .await
1338 .unwrap();
1339 let worker_b2 = pool
1340 .get_classified_worker(TestWorkerClassification::B)
1341 .await
1342 .unwrap();
1343
1344 let pool_ref = pool.clone();
1345 let classified_waiter = tokio::spawn(async move {
1346 pool_ref
1347 .get_classified_worker(TestWorkerClassification::B)
1348 .await
1349 });
1350 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
1351 assert!(!classified_waiter.is_finished());
1352
1353 drop(worker_b1);
1354 let worker_b = tokio::time::timeout(std::time::Duration::from_secs(1), classified_waiter)
1355 .await
1356 .unwrap()
1357 .unwrap()
1358 .unwrap();
1359 drop(worker_a1);
1360 drop(worker_a2);
1361 drop(worker_b2);
1362 drop(worker_b);
1363
1364 let worker_b1 = pool
1365 .get_classified_worker(TestWorkerClassification::B)
1366 .await
1367 .unwrap();
1368 let worker_b2 = pool
1369 .get_classified_worker(TestWorkerClassification::B)
1370 .await
1371 .unwrap();
1372 let worker1 = pool.get_worker().await.unwrap();
1373 let worker2 = pool.get_worker().await.unwrap();
1374
1375 let pool_ref = pool.clone();
1376 let generic_waiter = tokio::spawn(async move { pool_ref.get_worker().await });
1377 let pool_ref = pool.clone();
1378 let classified_waiter = tokio::spawn(async move {
1379 pool_ref
1380 .get_classified_worker(TestWorkerClassification::B)
1381 .await
1382 });
1383 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
1384 assert!(!generic_waiter.is_finished());
1385 assert!(!classified_waiter.is_finished());
1386
1387 let pool_ref = pool.clone();
1388 let clear_task = tokio::spawn(async move {
1389 pool_ref.clear_all_worker().await;
1390 });
1391 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
1392
1393 assert!(generic_waiter.await.unwrap().is_err());
1394 assert!(classified_waiter.await.unwrap().is_err());
1395
1396 drop(worker1);
1397 drop(worker2);
1398 drop(worker_b1);
1399 drop(worker_b2);
1400
1401 tokio::time::timeout(std::time::Duration::from_secs(1), clear_task)
1402 .await
1403 .unwrap()
1404 .unwrap();
1405}
1406
1407#[tokio::test]
1408async fn test_clear_all_worker_waits_for_inflight_create() {
1409 use std::sync::atomic::{AtomicUsize, Ordering};
1410 use std::sync::Arc;
1411
1412 #[derive(Clone, Debug, Eq, PartialEq, Hash)]
1413 enum TestWorkerClassification {
1414 A,
1415 }
1416
1417 struct TestWorker {
1418 classification: TestWorkerClassification,
1419 }
1420
1421 #[async_trait::async_trait]
1422 impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
1423 fn is_work(&self) -> bool {
1424 true
1425 }
1426
1427 fn is_valid(&self, c: TestWorkerClassification) -> bool {
1428 self.classification == c
1429 }
1430
1431 fn classification(&self) -> TestWorkerClassification {
1432 self.classification.clone()
1433 }
1434 }
1435
1436 struct TestWorkerFactory {
1437 create_count: Arc<AtomicUsize>,
1438 }
1439
1440 #[async_trait::async_trait]
1441 impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
1442 async fn create(
1443 &self,
1444 classification: Option<TestWorkerClassification>,
1445 ) -> PoolResult<TestWorker> {
1446 self.create_count.fetch_add(1, Ordering::SeqCst);
1447 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1448 Ok(TestWorker {
1449 classification: classification.unwrap_or(TestWorkerClassification::A),
1450 })
1451 }
1452 }
1453
1454 let create_count = Arc::new(AtomicUsize::new(0));
1455 let pool = new_classified_worker_pool(
1456 1,
1457 TestWorkerFactory {
1458 create_count: create_count.clone(),
1459 },
1460 Default::default(),
1461 );
1462
1463 let pool_ref = pool.clone();
1464 let worker_task = tokio::spawn(async move { pool_ref.get_worker().await });
1465 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1466
1467 pool.clear_all_worker().await;
1468
1469 let worker = worker_task.await.unwrap();
1470 assert!(worker.is_err());
1471 assert_eq!(create_count.load(Ordering::SeqCst), 1);
1472}
1473
1474#[tokio::test]
1475async fn test_concurrent_clear_all_worker() {
1476 #[derive(Clone, Debug, Eq, PartialEq, Hash)]
1477 enum TestWorkerClassification {
1478 A,
1479 }
1480
1481 struct TestWorker {
1482 classification: TestWorkerClassification,
1483 }
1484
1485 #[async_trait::async_trait]
1486 impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
1487 fn is_work(&self) -> bool {
1488 true
1489 }
1490
1491 fn is_valid(&self, c: TestWorkerClassification) -> bool {
1492 self.classification == c
1493 }
1494
1495 fn classification(&self) -> TestWorkerClassification {
1496 self.classification.clone()
1497 }
1498 }
1499
1500 struct TestWorkerFactory;
1501
1502 #[async_trait::async_trait]
1503 impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
1504 async fn create(
1505 &self,
1506 classification: Option<TestWorkerClassification>,
1507 ) -> PoolResult<TestWorker> {
1508 Ok(TestWorker {
1509 classification: classification.unwrap_or(TestWorkerClassification::A),
1510 })
1511 }
1512 }
1513
1514 let pool = ClassifiedWorkerPool::new(
1515 TestWorkerFactory,
1516 ClassifiedWorkerPoolConfig {
1517 max_count_per_classification: Some(1),
1518 ..Default::default()
1519 },
1520 );
1521 let worker = pool.get_worker().await.unwrap();
1522
1523 let pool_ref = pool.clone();
1524 let clear_task1 = tokio::spawn(async move {
1525 pool_ref.clear_all_worker().await;
1526 });
1527
1528 let pool_ref = pool.clone();
1529 let clear_task2 = tokio::spawn(async move {
1530 pool_ref.clear_all_worker().await;
1531 });
1532
1533 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1534 drop(worker);
1535
1536 tokio::time::timeout(std::time::Duration::from_secs(1), async {
1537 clear_task1.await.unwrap();
1538 clear_task2.await.unwrap();
1539 })
1540 .await
1541 .unwrap();
1542}
1543
1544#[tokio::test]
1545async fn test_zero_max_count_per_classification_returns_error() {
1546 #[derive(Clone, Debug, Eq, PartialEq, Hash)]
1547 enum TestWorkerClassification {
1548 A,
1549 }
1550
1551 struct TestWorker {
1552 classification: TestWorkerClassification,
1553 }
1554
1555 #[async_trait::async_trait]
1556 impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
1557 fn is_work(&self) -> bool {
1558 true
1559 }
1560
1561 fn is_valid(&self, c: TestWorkerClassification) -> bool {
1562 self.classification == c
1563 }
1564
1565 fn classification(&self) -> TestWorkerClassification {
1566 self.classification.clone()
1567 }
1568 }
1569
1570 struct TestWorkerFactory;
1571
1572 #[async_trait::async_trait]
1573 impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
1574 async fn create(
1575 &self,
1576 classification: Option<TestWorkerClassification>,
1577 ) -> PoolResult<TestWorker> {
1578 Ok(TestWorker {
1579 classification: classification.unwrap_or(TestWorkerClassification::A),
1580 })
1581 }
1582 }
1583
1584 let pool = ClassifiedWorkerPool::new(
1585 TestWorkerFactory,
1586 ClassifiedWorkerPoolConfig {
1587 max_count_per_classification: Some(0),
1588 ..Default::default()
1589 },
1590 );
1591 let worker = pool.get_worker().await;
1592 assert!(worker.is_err());
1593 assert_eq!(
1594 worker.err().unwrap().code(),
1595 crate::PoolErrorCode::InvalidConfig
1596 );
1597}
1598
1599#[tokio::test]
1600async fn test_default_config_has_no_per_classification_count_limit() {
1601 #[derive(Clone, Debug, Eq, PartialEq, Hash)]
1602 enum TestWorkerClassification {
1603 A,
1604 }
1605
1606 struct TestWorker;
1607
1608 #[async_trait::async_trait]
1609 impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
1610 fn is_work(&self) -> bool {
1611 true
1612 }
1613
1614 fn is_valid(&self, _classification: TestWorkerClassification) -> bool {
1615 true
1616 }
1617
1618 fn classification(&self) -> TestWorkerClassification {
1619 TestWorkerClassification::A
1620 }
1621 }
1622
1623 struct TestWorkerFactory;
1624
1625 #[async_trait::async_trait]
1626 impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
1627 async fn create(
1628 &self,
1629 _classification: Option<TestWorkerClassification>,
1630 ) -> PoolResult<TestWorker> {
1631 Ok(TestWorker)
1632 }
1633 }
1634
1635 let pool = ClassifiedWorkerPool::new(TestWorkerFactory, Default::default());
1636 let worker1 = pool.get_worker().await.unwrap();
1637 let worker2 = pool.get_worker().await.unwrap();
1638
1639 assert_eq!(pool.state.lock().unwrap().current_count, 2);
1640
1641 drop(worker1);
1642 drop(worker2);
1643}
1644
1645#[tokio::test]
1646async fn test_classified_pool_waits_when_classification_already_has_worker() {
1647 #[derive(Clone, Debug, Eq, PartialEq, Hash)]
1648 enum TestWorkerClassification {
1649 A,
1650 B,
1651 }
1652
1653 struct TestWorker {
1654 classification: TestWorkerClassification,
1655 }
1656
1657 #[async_trait::async_trait]
1658 impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
1659 fn is_work(&self) -> bool {
1660 true
1661 }
1662
1663 fn is_valid(&self, c: TestWorkerClassification) -> bool {
1664 self.classification == c
1665 }
1666
1667 fn classification(&self) -> TestWorkerClassification {
1668 self.classification.clone()
1669 }
1670 }
1671
1672 struct TestWorkerFactory;
1673
1674 #[async_trait::async_trait]
1675 impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
1676 async fn create(
1677 &self,
1678 classification: Option<TestWorkerClassification>,
1679 ) -> PoolResult<TestWorker> {
1680 Ok(TestWorker {
1681 classification: classification.unwrap_or(TestWorkerClassification::A),
1682 })
1683 }
1684 }
1685
1686 let pool = ClassifiedWorkerPool::new(
1687 TestWorkerFactory,
1688 ClassifiedWorkerPoolConfig {
1689 max_count_per_classification: Some(1),
1690 ..Default::default()
1691 },
1692 );
1693 let _worker = pool
1694 .get_classified_worker(TestWorkerClassification::B)
1695 .await
1696 .unwrap();
1697
1698 let pool_ref = pool.clone();
1699 let result = tokio::time::timeout(std::time::Duration::from_millis(100), async move {
1700 pool_ref
1701 .get_classified_worker(TestWorkerClassification::B)
1702 .await
1703 })
1704 .await;
1705
1706 assert!(result.is_err());
1707}
1708
1709#[tokio::test]
1710async fn test_different_classification_is_not_blocked_by_per_classification_limit() {
1711 use std::sync::atomic::{AtomicUsize, Ordering};
1712 use std::sync::Arc;
1713
1714 #[derive(Clone, Debug, Eq, PartialEq, Hash)]
1715 enum TestWorkerClassification {
1716 A,
1717 B,
1718 }
1719
1720 struct TestWorker {
1721 id: usize,
1722 classification: TestWorkerClassification,
1723 }
1724
1725 #[async_trait::async_trait]
1726 impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
1727 fn is_work(&self) -> bool {
1728 true
1729 }
1730
1731 fn is_valid(&self, c: TestWorkerClassification) -> bool {
1732 self.classification == c
1733 }
1734
1735 fn classification(&self) -> TestWorkerClassification {
1736 self.classification.clone()
1737 }
1738 }
1739
1740 struct TestWorkerFactory {
1741 create_count: Arc<AtomicUsize>,
1742 }
1743
1744 #[async_trait::async_trait]
1745 impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
1746 async fn create(
1747 &self,
1748 classification: Option<TestWorkerClassification>,
1749 ) -> PoolResult<TestWorker> {
1750 let id = self.create_count.fetch_add(1, Ordering::SeqCst);
1751 Ok(TestWorker {
1752 id,
1753 classification: classification.unwrap_or(TestWorkerClassification::A),
1754 })
1755 }
1756 }
1757
1758 let create_count = Arc::new(AtomicUsize::new(0));
1759 let pool = new_classified_worker_pool(
1760 1,
1761 TestWorkerFactory {
1762 create_count: create_count.clone(),
1763 },
1764 Default::default(),
1765 );
1766
1767 let worker_a = pool
1768 .get_classified_worker(TestWorkerClassification::A)
1769 .await
1770 .unwrap();
1771 let worker_b = pool
1772 .get_classified_worker(TestWorkerClassification::B)
1773 .await
1774 .unwrap();
1775
1776 assert_eq!(worker_a.id, 0);
1777 assert_eq!(worker_b.id, 1);
1778 assert_eq!(worker_b.classification(), TestWorkerClassification::B);
1779 assert_eq!(create_count.load(Ordering::SeqCst), 2);
1780}
1781
1782#[tokio::test]
1783async fn test_classified_create_failure_fails_same_classification_waiters() {
1784 #[derive(Clone, Debug, Eq, PartialEq, Hash)]
1785 enum TestWorkerClassification {
1786 B,
1787 }
1788
1789 struct TestWorker {
1790 classification: TestWorkerClassification,
1791 }
1792
1793 #[async_trait::async_trait]
1794 impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
1795 fn is_work(&self) -> bool {
1796 true
1797 }
1798
1799 fn is_valid(&self, c: TestWorkerClassification) -> bool {
1800 self.classification == c
1801 }
1802
1803 fn classification(&self) -> TestWorkerClassification {
1804 self.classification.clone()
1805 }
1806 }
1807
1808 struct TestWorkerFactory;
1809
1810 #[async_trait::async_trait]
1811 impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
1812 async fn create(
1813 &self,
1814 _classification: Option<TestWorkerClassification>,
1815 ) -> PoolResult<TestWorker> {
1816 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1817 Err(crate::pool_invalid_config_error("create failed"))
1818 }
1819 }
1820
1821 let pool = ClassifiedWorkerPool::new(
1822 TestWorkerFactory,
1823 ClassifiedWorkerPoolConfig {
1824 max_count_per_classification: Some(1),
1825 ..Default::default()
1826 },
1827 );
1828
1829 let pool_ref = pool.clone();
1830 let worker1 = tokio::spawn(async move {
1831 pool_ref
1832 .get_classified_worker(TestWorkerClassification::B)
1833 .await
1834 });
1835 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
1836
1837 let pool_ref = pool.clone();
1838 let worker2 = tokio::spawn(async move {
1839 pool_ref
1840 .get_classified_worker(TestWorkerClassification::B)
1841 .await
1842 });
1843
1844 let (worker1, worker2) = tokio::time::timeout(std::time::Duration::from_secs(1), async {
1845 (worker1.await.unwrap(), worker2.await.unwrap())
1846 })
1847 .await
1848 .unwrap();
1849
1850 assert_eq!(
1851 worker1.err().unwrap().code(),
1852 crate::PoolErrorCode::InvalidConfig
1853 );
1854 assert_eq!(
1855 worker2.err().unwrap().code(),
1856 crate::PoolErrorCode::InvalidConfig
1857 );
1858}
1859
1860#[tokio::test]
1861async fn test_classified_create_failure_wakes_generic_waiter_to_create() {
1862 use std::sync::atomic::{AtomicUsize, Ordering};
1863 use std::sync::Arc;
1864
1865 #[derive(Clone, Debug, Eq, PartialEq, Hash)]
1866 enum TestWorkerClassification {
1867 A,
1868 }
1869
1870 struct TestWorker {
1871 id: usize,
1872 classification: TestWorkerClassification,
1873 }
1874
1875 #[async_trait::async_trait]
1876 impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
1877 fn is_work(&self) -> bool {
1878 true
1879 }
1880
1881 fn is_valid(&self, c: TestWorkerClassification) -> bool {
1882 self.classification == c
1883 }
1884
1885 fn classification(&self) -> TestWorkerClassification {
1886 self.classification.clone()
1887 }
1888 }
1889
1890 struct TestWorkerFactory {
1891 create_count: Arc<AtomicUsize>,
1892 }
1893
1894 #[async_trait::async_trait]
1895 impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
1896 async fn create(
1897 &self,
1898 classification: Option<TestWorkerClassification>,
1899 ) -> PoolResult<TestWorker> {
1900 let id = self.create_count.fetch_add(1, Ordering::SeqCst);
1901 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1902 if id == 0 && classification == Some(TestWorkerClassification::A) {
1903 Err(crate::pool_invalid_config_error("create failed"))
1904 } else {
1905 Ok(TestWorker {
1906 id,
1907 classification: classification.unwrap_or(TestWorkerClassification::A),
1908 })
1909 }
1910 }
1911 }
1912
1913 let create_count = Arc::new(AtomicUsize::new(0));
1914 let pool = new_classified_worker_pool(
1915 1,
1916 TestWorkerFactory {
1917 create_count: create_count.clone(),
1918 },
1919 Default::default(),
1920 );
1921
1922 let pool_ref = pool.clone();
1923 let classified = tokio::spawn(async move {
1924 pool_ref
1925 .get_classified_worker(TestWorkerClassification::A)
1926 .await
1927 });
1928 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1929
1930 let pool_ref = pool.clone();
1931 let generic = tokio::spawn(async move { pool_ref.get_worker().await });
1932
1933 let (classified, generic) = tokio::time::timeout(std::time::Duration::from_secs(1), async {
1934 (classified.await.unwrap(), generic.await.unwrap())
1935 })
1936 .await
1937 .unwrap();
1938
1939 assert_eq!(
1940 classified.err().unwrap().code(),
1941 crate::PoolErrorCode::InvalidConfig
1942 );
1943 let generic = generic.unwrap();
1944 assert_eq!(generic.id, 1);
1945 assert_eq!(create_count.load(Ordering::SeqCst), 2);
1946}
1947
1948#[tokio::test]
1949async fn test_classified_retry_notification_skips_canceled_waiter() {
1950 #[derive(Clone, Debug, Eq, PartialEq, Hash)]
1951 enum TestWorkerClassification {
1952 A,
1953 }
1954
1955 struct TestWorker {
1956 classification: TestWorkerClassification,
1957 }
1958
1959 #[async_trait::async_trait]
1960 impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
1961 fn is_work(&self) -> bool {
1962 true
1963 }
1964
1965 fn is_valid(&self, c: TestWorkerClassification) -> bool {
1966 self.classification == c
1967 }
1968
1969 fn classification(&self) -> TestWorkerClassification {
1970 self.classification.clone()
1971 }
1972 }
1973
1974 struct TestWorkerFactory;
1975
1976 #[async_trait::async_trait]
1977 impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
1978 async fn create(
1979 &self,
1980 classification: Option<TestWorkerClassification>,
1981 ) -> PoolResult<TestWorker> {
1982 Ok(TestWorker {
1983 classification: classification.unwrap_or(TestWorkerClassification::A),
1984 })
1985 }
1986 }
1987
1988 let (canceled_notify, canceled_waiter) = Notify::new();
1989 drop(canceled_waiter);
1990 let (notify, waiter) = Notify::new();
1991
1992 ClassifiedWorkerPool::<TestWorkerClassification, TestWorker, TestWorkerFactory>::notify_retry_waiters(
1993 vec![canceled_notify, notify],
1994 );
1995
1996 let result = tokio::time::timeout(std::time::Duration::from_secs(1), waiter)
1997 .await
1998 .unwrap();
1999 assert!(matches!(result, ClassifiedWorkerWaitResult::Retry));
2000}
2001
2002#[tokio::test]
2003async fn test_classified_request_replaces_non_matching_idle_worker() {
2004 use std::sync::atomic::{AtomicUsize, Ordering};
2005 use std::sync::Arc;
2006
2007 #[derive(Clone, Debug, Eq, PartialEq, Hash)]
2008 enum TestWorkerClassification {
2009 A,
2010 B,
2011 }
2012
2013 struct TestWorker {
2014 id: usize,
2015 classification: TestWorkerClassification,
2016 }
2017
2018 #[async_trait::async_trait]
2019 impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
2020 fn is_work(&self) -> bool {
2021 true
2022 }
2023
2024 fn is_valid(&self, c: TestWorkerClassification) -> bool {
2025 self.classification == c
2026 }
2027
2028 fn classification(&self) -> TestWorkerClassification {
2029 self.classification.clone()
2030 }
2031 }
2032
2033 struct TestWorkerFactory {
2034 create_count: Arc<AtomicUsize>,
2035 }
2036
2037 #[async_trait::async_trait]
2038 impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
2039 async fn create(
2040 &self,
2041 classification: Option<TestWorkerClassification>,
2042 ) -> PoolResult<TestWorker> {
2043 let id = self.create_count.fetch_add(1, Ordering::SeqCst);
2044 Ok(TestWorker {
2045 id,
2046 classification: classification.unwrap_or(TestWorkerClassification::A),
2047 })
2048 }
2049 }
2050
2051 let create_count = Arc::new(AtomicUsize::new(0));
2052 let pool = new_classified_worker_pool(
2053 1,
2054 TestWorkerFactory {
2055 create_count: create_count.clone(),
2056 },
2057 Default::default(),
2058 );
2059
2060 {
2061 let worker = pool
2062 .get_classified_worker(TestWorkerClassification::A)
2063 .await
2064 .unwrap();
2065 assert_eq!(worker.id, 0);
2066 }
2067
2068 let worker = pool
2069 .get_classified_worker(TestWorkerClassification::B)
2070 .await
2071 .unwrap();
2072 assert_eq!(worker.id, 1);
2073 assert_eq!(worker.classification(), TestWorkerClassification::B);
2074 assert_eq!(create_count.load(Ordering::SeqCst), 2);
2075}
2076
2077#[tokio::test]
2078async fn test_other_classification_does_not_wait_for_returned_non_matching_worker() {
2079 use std::sync::atomic::{AtomicUsize, Ordering};
2080 use std::sync::Arc;
2081
2082 #[derive(Clone, Debug, Eq, PartialEq, Hash)]
2083 enum TestWorkerClassification {
2084 A,
2085 B,
2086 }
2087
2088 struct TestWorker {
2089 id: usize,
2090 classification: TestWorkerClassification,
2091 }
2092
2093 #[async_trait::async_trait]
2094 impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
2095 fn is_work(&self) -> bool {
2096 true
2097 }
2098
2099 fn is_valid(&self, c: TestWorkerClassification) -> bool {
2100 self.classification == c
2101 }
2102
2103 fn classification(&self) -> TestWorkerClassification {
2104 self.classification.clone()
2105 }
2106 }
2107
2108 struct TestWorkerFactory {
2109 create_count: Arc<AtomicUsize>,
2110 }
2111
2112 #[async_trait::async_trait]
2113 impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
2114 async fn create(
2115 &self,
2116 classification: Option<TestWorkerClassification>,
2117 ) -> PoolResult<TestWorker> {
2118 let id = self.create_count.fetch_add(1, Ordering::SeqCst);
2119 Ok(TestWorker {
2120 id,
2121 classification: classification.unwrap_or(TestWorkerClassification::A),
2122 })
2123 }
2124 }
2125
2126 let create_count = Arc::new(AtomicUsize::new(0));
2127 let pool = new_classified_worker_pool(
2128 2,
2129 TestWorkerFactory {
2130 create_count: create_count.clone(),
2131 },
2132 Default::default(),
2133 );
2134 let worker_a = pool
2135 .get_classified_worker(TestWorkerClassification::A)
2136 .await
2137 .unwrap();
2138 let _worker_b = pool
2139 .get_classified_worker(TestWorkerClassification::B)
2140 .await
2141 .unwrap();
2142
2143 let pool_ref = pool.clone();
2144 let waiter = tokio::spawn(async move {
2145 pool_ref
2146 .get_classified_worker(TestWorkerClassification::B)
2147 .await
2148 });
2149 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
2150 assert!(waiter.is_finished());
2151
2152 let worker = tokio::time::timeout(std::time::Duration::from_secs(1), waiter)
2153 .await
2154 .unwrap()
2155 .unwrap()
2156 .unwrap();
2157 assert_eq!(worker.id, 2);
2158 assert_eq!(worker.classification(), TestWorkerClassification::B);
2159 assert_eq!(create_count.load(Ordering::SeqCst), 3);
2160 drop(worker_a);
2161}
2162
2163#[tokio::test]
2164async fn test_other_classification_does_not_wait_for_unwork_non_matching_worker() {
2165 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
2166 use std::sync::Arc;
2167
2168 #[derive(Clone, Debug, Eq, PartialEq, Hash)]
2169 enum TestWorkerClassification {
2170 A,
2171 B,
2172 }
2173
2174 struct TestWorker {
2175 id: usize,
2176 work: AtomicBool,
2177 classification: TestWorkerClassification,
2178 }
2179
2180 #[async_trait::async_trait]
2181 impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
2182 fn is_work(&self) -> bool {
2183 self.work.load(Ordering::SeqCst)
2184 }
2185
2186 fn is_valid(&self, c: TestWorkerClassification) -> bool {
2187 self.classification == c
2188 }
2189
2190 fn classification(&self) -> TestWorkerClassification {
2191 self.classification.clone()
2192 }
2193 }
2194
2195 struct TestWorkerFactory {
2196 create_count: Arc<AtomicUsize>,
2197 }
2198
2199 #[async_trait::async_trait]
2200 impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
2201 async fn create(
2202 &self,
2203 classification: Option<TestWorkerClassification>,
2204 ) -> PoolResult<TestWorker> {
2205 let id = self.create_count.fetch_add(1, Ordering::SeqCst);
2206 Ok(TestWorker {
2207 id,
2208 work: AtomicBool::new(true),
2209 classification: classification.unwrap_or(TestWorkerClassification::A),
2210 })
2211 }
2212 }
2213
2214 let create_count = Arc::new(AtomicUsize::new(0));
2215 let pool = new_classified_worker_pool(
2216 2,
2217 TestWorkerFactory {
2218 create_count: create_count.clone(),
2219 },
2220 Default::default(),
2221 );
2222 let worker_a = pool
2223 .get_classified_worker(TestWorkerClassification::A)
2224 .await
2225 .unwrap();
2226 let _worker_b = pool
2227 .get_classified_worker(TestWorkerClassification::B)
2228 .await
2229 .unwrap();
2230
2231 let pool_ref = pool.clone();
2232 let waiter = tokio::spawn(async move {
2233 pool_ref
2234 .get_classified_worker(TestWorkerClassification::B)
2235 .await
2236 });
2237 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
2238 assert!(waiter.is_finished());
2239
2240 worker_a.work.store(false, Ordering::SeqCst);
2241 let worker = tokio::time::timeout(std::time::Duration::from_secs(1), waiter)
2242 .await
2243 .unwrap()
2244 .unwrap()
2245 .unwrap();
2246 assert_eq!(worker.id, 2);
2247 assert_eq!(worker.classification(), TestWorkerClassification::B);
2248 assert_eq!(create_count.load(Ordering::SeqCst), 3);
2249 drop(worker_a);
2250}
2251
2252#[tokio::test]
2253async fn test_factory_must_return_matching_classification() {
2254 use std::sync::atomic::{AtomicUsize, Ordering};
2255 use std::sync::Arc;
2256
2257 #[derive(Clone, Debug, Eq, PartialEq, Hash)]
2258 enum TestWorkerClassification {
2259 A,
2260 B,
2261 }
2262
2263 struct TestWorker {
2264 classification: TestWorkerClassification,
2265 }
2266
2267 #[async_trait::async_trait]
2268 impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
2269 fn is_work(&self) -> bool {
2270 true
2271 }
2272
2273 fn is_valid(&self, c: TestWorkerClassification) -> bool {
2274 self.classification == c
2275 }
2276
2277 fn classification(&self) -> TestWorkerClassification {
2278 self.classification.clone()
2279 }
2280 }
2281
2282 struct TestWorkerFactory {
2283 create_count: Arc<AtomicUsize>,
2284 }
2285
2286 #[async_trait::async_trait]
2287 impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
2288 async fn create(
2289 &self,
2290 classification: Option<TestWorkerClassification>,
2291 ) -> PoolResult<TestWorker> {
2292 let count = self.create_count.fetch_add(1, Ordering::SeqCst);
2293 let classification = if count == 0 {
2294 TestWorkerClassification::A
2295 } else {
2296 classification.unwrap_or(TestWorkerClassification::A)
2297 };
2298 Ok(TestWorker { classification })
2299 }
2300 }
2301
2302 let create_count = Arc::new(AtomicUsize::new(0));
2303 let pool = new_classified_worker_pool(
2304 1,
2305 TestWorkerFactory {
2306 create_count: create_count.clone(),
2307 },
2308 Default::default(),
2309 );
2310 let worker = pool
2311 .get_classified_worker(TestWorkerClassification::B)
2312 .await;
2313 assert!(worker.is_err());
2314 assert_eq!(
2315 worker.err().unwrap().code(),
2316 crate::PoolErrorCode::InvalidConfig
2317 );
2318
2319 let worker = pool
2320 .get_classified_worker(TestWorkerClassification::B)
2321 .await;
2322 assert!(worker.is_ok());
2323 assert_eq!(create_count.load(Ordering::SeqCst), 2);
2324}
2325
2326#[tokio::test(flavor = "multi_thread")]
2327async fn test_classified_and_generic_waiters_both_progress() {
2328 use std::sync::mpsc;
2329
2330 #[derive(Clone, Debug, Eq, PartialEq, Hash)]
2331 enum TestWorkerClassification {
2332 B,
2333 }
2334
2335 struct TestWorker {
2336 classification: TestWorkerClassification,
2337 }
2338
2339 #[async_trait::async_trait]
2340 impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
2341 fn is_work(&self) -> bool {
2342 true
2343 }
2344
2345 fn is_valid(&self, c: TestWorkerClassification) -> bool {
2346 self.classification == c
2347 }
2348
2349 fn classification(&self) -> TestWorkerClassification {
2350 self.classification.clone()
2351 }
2352 }
2353
2354 struct TestWorkerFactory;
2355
2356 #[async_trait::async_trait]
2357 impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
2358 async fn create(
2359 &self,
2360 classification: Option<TestWorkerClassification>,
2361 ) -> PoolResult<TestWorker> {
2362 Ok(TestWorker {
2363 classification: classification.unwrap_or(TestWorkerClassification::B),
2364 })
2365 }
2366 }
2367
2368 let pool = ClassifiedWorkerPool::new(
2369 TestWorkerFactory,
2370 ClassifiedWorkerPoolConfig {
2371 max_count_per_classification: Some(1),
2372 ..Default::default()
2373 },
2374 );
2375 let worker = pool
2376 .get_classified_worker(TestWorkerClassification::B)
2377 .await
2378 .unwrap();
2379
2380 let (tx, rx) = mpsc::channel();
2381
2382 let pool_ref = pool.clone();
2383 let tx_classified = tx.clone();
2384 let classified_task = tokio::spawn(async move {
2385 let _worker = pool_ref
2386 .get_classified_worker(TestWorkerClassification::B)
2387 .await
2388 .unwrap();
2389 tx_classified.send("classified").unwrap();
2390 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2391 });
2392
2393 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
2394
2395 let pool_ref = pool.clone();
2396 let generic_task = tokio::spawn(async move {
2397 let _worker = pool_ref.get_worker().await.unwrap();
2398 tx.send("generic").unwrap();
2399 });
2400
2401 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
2402 drop(worker);
2403
2404 classified_task.await.unwrap();
2405 generic_task.await.unwrap();
2406 let first = rx.recv_timeout(std::time::Duration::from_secs(2)).unwrap();
2407 let second = rx.recv_timeout(std::time::Duration::from_secs(2)).unwrap();
2408 assert_ne!(first, second);
2409}
2410
2411#[tokio::test]
2412async fn test_generic_factory_worker_must_be_valid_for_its_primary_classification() {
2413 #[derive(Clone, Debug, Eq, PartialEq, Hash)]
2414 enum TestWorkerClassification {
2415 A,
2416 B,
2417 }
2418
2419 struct TestWorker {
2420 classification: TestWorkerClassification,
2421 }
2422
2423 #[async_trait::async_trait]
2424 impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
2425 fn is_work(&self) -> bool {
2426 true
2427 }
2428
2429 fn is_valid(&self, c: TestWorkerClassification) -> bool {
2430 c == TestWorkerClassification::B
2431 }
2432
2433 fn classification(&self) -> TestWorkerClassification {
2434 self.classification.clone()
2435 }
2436 }
2437
2438 struct TestWorkerFactory;
2439
2440 #[async_trait::async_trait]
2441 impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
2442 async fn create(
2443 &self,
2444 _classification: Option<TestWorkerClassification>,
2445 ) -> PoolResult<TestWorker> {
2446 Ok(TestWorker {
2447 classification: TestWorkerClassification::A,
2448 })
2449 }
2450 }
2451
2452 let pool = ClassifiedWorkerPool::new(
2453 TestWorkerFactory,
2454 ClassifiedWorkerPoolConfig {
2455 max_count_per_classification: Some(1),
2456 ..Default::default()
2457 },
2458 );
2459 let worker = pool.get_worker().await;
2460 assert!(worker.is_err());
2461 assert_eq!(
2462 worker.err().unwrap().code(),
2463 crate::PoolErrorCode::InvalidConfig
2464 );
2465}
2466
2467#[tokio::test]
2468async fn test_classified_idle_worker_timeout_releases_worker() {
2469 use std::sync::atomic::{AtomicUsize, Ordering};
2470 use std::sync::Arc;
2471
2472 #[derive(Clone, Debug, Eq, PartialEq, Hash)]
2473 enum TestWorkerClassification {
2474 A,
2475 B,
2476 }
2477
2478 struct TestWorker {
2479 id: usize,
2480 classification: TestWorkerClassification,
2481 }
2482
2483 #[async_trait::async_trait]
2484 impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
2485 fn is_work(&self) -> bool {
2486 true
2487 }
2488
2489 fn is_valid(&self, c: TestWorkerClassification) -> bool {
2490 self.classification == c
2491 }
2492
2493 fn classification(&self) -> TestWorkerClassification {
2494 self.classification.clone()
2495 }
2496 }
2497
2498 struct TestWorkerFactory {
2499 create_count: Arc<AtomicUsize>,
2500 }
2501
2502 #[async_trait::async_trait]
2503 impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
2504 async fn create(
2505 &self,
2506 classification: Option<TestWorkerClassification>,
2507 ) -> PoolResult<TestWorker> {
2508 let id = self.create_count.fetch_add(1, Ordering::SeqCst);
2509 Ok(TestWorker {
2510 id,
2511 classification: classification.unwrap_or(TestWorkerClassification::A),
2512 })
2513 }
2514 }
2515
2516 let create_count = Arc::new(AtomicUsize::new(0));
2517 let pool = new_classified_worker_pool(
2518 1,
2519 TestWorkerFactory {
2520 create_count: create_count.clone(),
2521 },
2522 ClassifiedWorkerPoolConfig {
2523 idle_timeout: Some(std::time::Duration::from_millis(30)),
2524 ..Default::default()
2525 },
2526 );
2527
2528 {
2529 let worker = pool
2530 .get_classified_worker(TestWorkerClassification::B)
2531 .await
2532 .unwrap();
2533 assert_eq!(worker.id, 0);
2534 assert_eq!(worker.classification(), TestWorkerClassification::B);
2535 }
2536
2537 tokio::time::sleep(std::time::Duration::from_millis(80)).await;
2538
2539 let worker = pool
2540 .get_classified_worker(TestWorkerClassification::A)
2541 .await
2542 .unwrap();
2543 assert_eq!(worker.id, 1);
2544 assert_eq!(worker.classification(), TestWorkerClassification::A);
2545 assert_eq!(create_count.load(Ordering::SeqCst), 2);
2546}
2547
2548#[tokio::test]
2549async fn test_get_classified_worker_uses_most_recent_matching_idle_worker() {
2550 use std::sync::atomic::{AtomicUsize, Ordering};
2551 use std::sync::Arc;
2552
2553 #[derive(Clone, Debug, Eq, PartialEq, Hash)]
2554 enum TestWorkerClassification {
2555 A,
2556 }
2557
2558 struct TestWorker {
2559 id: usize,
2560 classification: TestWorkerClassification,
2561 }
2562
2563 #[async_trait::async_trait]
2564 impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
2565 fn is_work(&self) -> bool {
2566 true
2567 }
2568
2569 fn is_valid(&self, c: TestWorkerClassification) -> bool {
2570 self.classification == c
2571 }
2572
2573 fn classification(&self) -> TestWorkerClassification {
2574 self.classification.clone()
2575 }
2576 }
2577
2578 struct TestWorkerFactory {
2579 create_count: Arc<AtomicUsize>,
2580 }
2581
2582 #[async_trait::async_trait]
2583 impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
2584 async fn create(
2585 &self,
2586 classification: Option<TestWorkerClassification>,
2587 ) -> PoolResult<TestWorker> {
2588 let id = self.create_count.fetch_add(1, Ordering::SeqCst);
2589 Ok(TestWorker {
2590 id,
2591 classification: classification.unwrap_or(TestWorkerClassification::A),
2592 })
2593 }
2594 }
2595
2596 let create_count = Arc::new(AtomicUsize::new(0));
2597 let pool = new_classified_worker_pool(
2598 2,
2599 TestWorkerFactory {
2600 create_count: create_count.clone(),
2601 },
2602 Default::default(),
2603 );
2604
2605 let worker1 = pool
2606 .get_classified_worker(TestWorkerClassification::A)
2607 .await
2608 .unwrap();
2609 let worker2 = pool
2610 .get_classified_worker(TestWorkerClassification::A)
2611 .await
2612 .unwrap();
2613 assert_eq!(worker1.id, 0);
2614 assert_eq!(worker2.id, 1);
2615
2616 drop(worker1);
2617 drop(worker2);
2618
2619 let worker = pool
2620 .get_classified_worker(TestWorkerClassification::A)
2621 .await
2622 .unwrap();
2623 assert_eq!(worker.id, 1);
2624 assert_eq!(create_count.load(Ordering::SeqCst), 2);
2625}
2626
2627#[tokio::test]
2628async fn test_classified_cleanup_idle_worker_can_be_triggered_externally() {
2629 use std::sync::atomic::{AtomicUsize, Ordering};
2630 use std::sync::Arc;
2631
2632 #[derive(Clone, Debug, Eq, PartialEq, Hash)]
2633 enum TestWorkerClassification {
2634 A,
2635 B,
2636 }
2637
2638 struct TestWorker {
2639 id: usize,
2640 classification: TestWorkerClassification,
2641 }
2642
2643 #[async_trait::async_trait]
2644 impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
2645 fn is_work(&self) -> bool {
2646 true
2647 }
2648
2649 fn is_valid(&self, c: TestWorkerClassification) -> bool {
2650 self.classification == c
2651 }
2652
2653 fn classification(&self) -> TestWorkerClassification {
2654 self.classification.clone()
2655 }
2656 }
2657
2658 struct TestWorkerFactory {
2659 create_count: Arc<AtomicUsize>,
2660 }
2661
2662 #[async_trait::async_trait]
2663 impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
2664 async fn create(
2665 &self,
2666 classification: Option<TestWorkerClassification>,
2667 ) -> PoolResult<TestWorker> {
2668 let id = self.create_count.fetch_add(1, Ordering::SeqCst);
2669 Ok(TestWorker {
2670 id,
2671 classification: classification.unwrap_or(TestWorkerClassification::A),
2672 })
2673 }
2674 }
2675
2676 let create_count = Arc::new(AtomicUsize::new(0));
2677 let pool = new_classified_worker_pool(
2678 1,
2679 TestWorkerFactory {
2680 create_count: create_count.clone(),
2681 },
2682 ClassifiedWorkerPoolConfig {
2683 idle_timeout: Some(std::time::Duration::from_millis(30)),
2684 ..Default::default()
2685 },
2686 );
2687
2688 {
2689 let worker = pool
2690 .get_classified_worker(TestWorkerClassification::B)
2691 .await
2692 .unwrap();
2693 assert_eq!(worker.id, 0);
2694 assert_eq!(worker.classification(), TestWorkerClassification::B);
2695 }
2696
2697 tokio::time::sleep(std::time::Duration::from_millis(80)).await;
2698
2699 assert_eq!(pool.cleanup_idle_worker(), 1);
2700
2701 let worker = pool
2702 .get_classified_worker(TestWorkerClassification::A)
2703 .await
2704 .unwrap();
2705 assert_eq!(worker.id, 1);
2706 assert_eq!(worker.classification(), TestWorkerClassification::A);
2707 assert_eq!(create_count.load(Ordering::SeqCst), 2);
2708}
2709
2710#[tokio::test]
2711async fn test_canceled_classified_create_rolls_back_reservation() {
2712 use std::sync::atomic::{AtomicBool, Ordering};
2713
2714 #[derive(Clone, Debug, Eq, PartialEq, Hash)]
2715 enum TestWorkerClassification {
2716 A,
2717 }
2718
2719 struct TestWorker;
2720
2721 #[async_trait::async_trait]
2722 impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
2723 fn is_work(&self) -> bool {
2724 true
2725 }
2726
2727 fn is_valid(&self, _classification: TestWorkerClassification) -> bool {
2728 true
2729 }
2730
2731 fn classification(&self) -> TestWorkerClassification {
2732 TestWorkerClassification::A
2733 }
2734 }
2735
2736 struct TestWorkerFactory {
2737 create_started: Arc<AtomicBool>,
2738 allow_create: Arc<AtomicBool>,
2739 }
2740
2741 #[async_trait::async_trait]
2742 impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
2743 async fn create(
2744 &self,
2745 _classification: Option<TestWorkerClassification>,
2746 ) -> PoolResult<TestWorker> {
2747 self.create_started.store(true, Ordering::SeqCst);
2748 while !self.allow_create.load(Ordering::SeqCst) {
2749 tokio::task::yield_now().await;
2750 }
2751 Ok(TestWorker)
2752 }
2753 }
2754
2755 let create_started = Arc::new(AtomicBool::new(false));
2756 let allow_create = Arc::new(AtomicBool::new(false));
2757 let pool = new_classified_worker_pool(
2758 1,
2759 TestWorkerFactory {
2760 create_started: create_started.clone(),
2761 allow_create: allow_create.clone(),
2762 },
2763 Default::default(),
2764 );
2765
2766 let pool_ref = pool.clone();
2767 let create_task = tokio::spawn(async move {
2768 pool_ref
2769 .get_classified_worker(TestWorkerClassification::A)
2770 .await
2771 });
2772 while !create_started.load(Ordering::SeqCst) {
2773 tokio::task::yield_now().await;
2774 }
2775 create_task.abort();
2776 assert!(matches!(create_task.await, Err(err) if err.is_cancelled()));
2777
2778 allow_create.store(true, Ordering::SeqCst);
2779 let worker = tokio::time::timeout(
2780 std::time::Duration::from_secs(1),
2781 pool.get_classified_worker(TestWorkerClassification::A),
2782 )
2783 .await
2784 .unwrap()
2785 .unwrap();
2786 drop(worker);
2787
2788 tokio::time::timeout(std::time::Duration::from_secs(1), pool.clear_all_worker())
2789 .await
2790 .unwrap();
2791}
2792
2793#[tokio::test]
2794async fn test_mutating_worker_classification_removes_returned_worker() {
2795 #[derive(Clone, Debug, Eq, PartialEq, Hash)]
2796 enum TestWorkerClassification {
2797 A,
2798 B,
2799 }
2800
2801 struct TestWorker {
2802 work: bool,
2803 classification: TestWorkerClassification,
2804 }
2805
2806 #[async_trait::async_trait]
2807 impl ClassifiedWorker<TestWorkerClassification> for TestWorker {
2808 fn is_work(&self) -> bool {
2809 self.work
2810 }
2811
2812 fn is_valid(&self, classification: TestWorkerClassification) -> bool {
2813 self.classification == classification
2814 }
2815
2816 fn classification(&self) -> TestWorkerClassification {
2817 self.classification.clone()
2818 }
2819 }
2820
2821 struct TestWorkerFactory;
2822
2823 #[async_trait::async_trait]
2824 impl ClassifiedWorkerFactory<TestWorkerClassification, TestWorker> for TestWorkerFactory {
2825 async fn create(
2826 &self,
2827 classification: Option<TestWorkerClassification>,
2828 ) -> PoolResult<TestWorker> {
2829 Ok(TestWorker {
2830 work: true,
2831 classification: classification.unwrap_or(TestWorkerClassification::A),
2832 })
2833 }
2834 }
2835
2836 let pool = new_classified_worker_pool(
2837 1,
2838 TestWorkerFactory,
2839 ClassifiedWorkerPoolConfig {
2840 idle_timeout: None,
2841 max_count_per_classification: Some(1),
2842 ..Default::default()
2843 },
2844 );
2845 let mut worker = pool
2846 .get_classified_worker(TestWorkerClassification::A)
2847 .await
2848 .unwrap();
2849 worker.classification = TestWorkerClassification::B;
2850 drop(worker);
2851
2852 {
2853 let state = pool.state.lock().unwrap();
2854 assert_eq!(state.current_count, 0);
2855 assert!(state.classified_count_map.is_empty());
2856 }
2857
2858 let worker = tokio::time::timeout(
2859 std::time::Duration::from_secs(1),
2860 pool.get_classified_worker(TestWorkerClassification::A),
2861 )
2862 .await
2863 .unwrap()
2864 .unwrap();
2865 assert_eq!(worker.classification(), TestWorkerClassification::A);
2866}
2867
2868#[cfg(test)]
2869mod affected_path_tests {
2870 use super::*;
2871 use std::collections::VecDeque;
2872 use std::sync::atomic::{AtomicBool, Ordering};
2873 use std::sync::mpsc;
2874
2875 #[derive(Clone, Debug, Eq, PartialEq, Hash)]
2876 enum Classification {
2877 A,
2878 B,
2879 C,
2880 }
2881
2882 struct BlockingWorker {
2883 classification: Classification,
2884 }
2885
2886 #[async_trait::async_trait]
2887 impl ClassifiedWorker<Classification> for BlockingWorker {
2888 fn is_work(&self) -> bool {
2889 true
2890 }
2891
2892 fn is_valid(&self, classification: Classification) -> bool {
2893 self.classification == classification
2894 }
2895
2896 fn classification(&self) -> Classification {
2897 self.classification.clone()
2898 }
2899 }
2900
2901 struct BlockingFactory {
2902 create_started: Arc<AtomicBool>,
2903 allow_create: Arc<AtomicBool>,
2904 }
2905
2906 #[async_trait::async_trait]
2907 impl ClassifiedWorkerFactory<Classification, BlockingWorker> for BlockingFactory {
2908 async fn create(
2909 &self,
2910 classification: Option<Classification>,
2911 ) -> PoolResult<BlockingWorker> {
2912 self.create_started.store(true, Ordering::SeqCst);
2913 while !self.allow_create.load(Ordering::SeqCst) {
2914 tokio::task::yield_now().await;
2915 }
2916 Ok(BlockingWorker {
2917 classification: classification.unwrap_or(Classification::A),
2918 })
2919 }
2920 }
2921
2922 async fn wait_for_create(create_started: &AtomicBool) {
2923 while !create_started.load(Ordering::SeqCst) {
2924 tokio::task::yield_now().await;
2925 }
2926 }
2927
2928 fn new_blocking_pool(
2929 max_count: u16,
2930 ) -> (
2931 ClassifiedWorkerPoolRef<Classification, BlockingWorker, BlockingFactory>,
2932 Arc<AtomicBool>,
2933 Arc<AtomicBool>,
2934 ) {
2935 let create_started = Arc::new(AtomicBool::new(false));
2936 let allow_create = Arc::new(AtomicBool::new(false));
2937 let pool = new_classified_worker_pool(
2938 max_count,
2939 BlockingFactory {
2940 create_started: create_started.clone(),
2941 allow_create: allow_create.clone(),
2942 },
2943 Default::default(),
2944 );
2945 (pool, create_started, allow_create)
2946 }
2947
2948 #[tokio::test]
2949 async fn test_canceled_generic_create_rolls_back_classified_pool_reservation() {
2950 let (pool, create_started, allow_create) = new_blocking_pool(1);
2951 let pool_ref = pool.clone();
2952 let create_task = tokio::spawn(async move { pool_ref.get_worker().await });
2953 wait_for_create(&create_started).await;
2954 create_task.abort();
2955 assert!(matches!(create_task.await, Err(err) if err.is_cancelled()));
2956
2957 allow_create.store(true, Ordering::SeqCst);
2958 let worker = tokio::time::timeout(Duration::from_secs(1), pool.get_worker())
2959 .await
2960 .unwrap()
2961 .unwrap();
2962 drop(worker);
2963 tokio::time::timeout(Duration::from_secs(1), pool.clear_all_worker())
2964 .await
2965 .unwrap();
2966 }
2967
2968 #[tokio::test]
2969 async fn test_canceled_replacement_create_rolls_back_classified_pool_reservation() {
2970 let (pool, create_started, allow_create) = new_blocking_pool(1);
2971 allow_create.store(true, Ordering::SeqCst);
2972 let worker = pool.get_classified_worker(Classification::A).await.unwrap();
2973 drop(worker);
2974
2975 create_started.store(false, Ordering::SeqCst);
2976 allow_create.store(false, Ordering::SeqCst);
2977 let pool_ref = pool.clone();
2978 let create_task =
2979 tokio::spawn(async move { pool_ref.get_classified_worker(Classification::B).await });
2980 wait_for_create(&create_started).await;
2981 create_task.abort();
2982 assert!(matches!(create_task.await, Err(err) if err.is_cancelled()));
2983
2984 allow_create.store(true, Ordering::SeqCst);
2985 let worker = tokio::time::timeout(
2986 Duration::from_secs(1),
2987 pool.get_classified_worker(Classification::B),
2988 )
2989 .await
2990 .unwrap()
2991 .unwrap();
2992 drop(worker);
2993 tokio::time::timeout(Duration::from_secs(1), pool.clear_all_worker())
2994 .await
2995 .unwrap();
2996 }
2997
2998 #[tokio::test]
2999 async fn test_canceled_overcommit_create_rolls_back_classified_pool_reservation() {
3000 let (pool, create_started, allow_create) = new_blocking_pool(1);
3001 allow_create.store(true, Ordering::SeqCst);
3002 let worker_a = pool.get_classified_worker(Classification::A).await.unwrap();
3003
3004 create_started.store(false, Ordering::SeqCst);
3005 allow_create.store(false, Ordering::SeqCst);
3006 let pool_ref = pool.clone();
3007 let create_task =
3008 tokio::spawn(async move { pool_ref.get_classified_worker(Classification::B).await });
3009 wait_for_create(&create_started).await;
3010 create_task.abort();
3011 assert!(matches!(create_task.await, Err(err) if err.is_cancelled()));
3012
3013 {
3014 let state = pool.state.lock().unwrap();
3015 assert_eq!(state.current_count, 1);
3016 assert_eq!(state.reserved_classified_count(&Classification::B), 0);
3017 }
3018
3019 allow_create.store(true, Ordering::SeqCst);
3020 let worker_b = tokio::time::timeout(
3021 Duration::from_secs(1),
3022 pool.get_classified_worker(Classification::B),
3023 )
3024 .await
3025 .unwrap()
3026 .unwrap();
3027 drop(worker_b);
3028 drop(worker_a);
3029 tokio::time::timeout(Duration::from_secs(1), pool.clear_all_worker())
3030 .await
3031 .unwrap();
3032 }
3033
3034 type DropCallback = Box<dyn FnOnce() + Send>;
3035
3036 struct DropProbeWorker {
3037 working: Arc<AtomicBool>,
3038 classification: Classification,
3039 on_drop: Option<DropCallback>,
3040 }
3041
3042 #[async_trait::async_trait]
3043 impl ClassifiedWorker<Classification> for DropProbeWorker {
3044 fn is_work(&self) -> bool {
3045 self.working.load(Ordering::SeqCst)
3046 }
3047
3048 fn is_valid(&self, classification: Classification) -> bool {
3049 self.classification == classification
3050 }
3051
3052 fn classification(&self) -> Classification {
3053 self.classification.clone()
3054 }
3055 }
3056
3057 impl Drop for DropProbeWorker {
3058 fn drop(&mut self) {
3059 if let Some(on_drop) = self.on_drop.take() {
3060 on_drop();
3061 }
3062 }
3063 }
3064
3065 struct DropProbeSpec {
3066 working: Arc<AtomicBool>,
3067 on_drop: Option<DropCallback>,
3068 }
3069
3070 struct DropProbeFactory {
3071 specs: Arc<Mutex<VecDeque<DropProbeSpec>>>,
3072 }
3073
3074 #[async_trait::async_trait]
3075 impl ClassifiedWorkerFactory<Classification, DropProbeWorker> for DropProbeFactory {
3076 async fn create(
3077 &self,
3078 classification: Option<Classification>,
3079 ) -> PoolResult<DropProbeWorker> {
3080 let spec = self.specs.lock().unwrap().pop_front().unwrap();
3081 Ok(DropProbeWorker {
3082 working: spec.working,
3083 classification: classification.unwrap_or(Classification::A),
3084 on_drop: spec.on_drop,
3085 })
3086 }
3087 }
3088
3089 type DropProbePool = ClassifiedWorkerPoolRef<Classification, DropProbeWorker, DropProbeFactory>;
3090
3091 fn new_drop_probe_pool(
3092 idle_timeout: Option<Duration>,
3093 max_idle_count: Option<u16>,
3094 ) -> (DropProbePool, Arc<Mutex<VecDeque<DropProbeSpec>>>) {
3095 let specs = Arc::new(Mutex::new(VecDeque::new()));
3096 let pool = new_classified_worker_pool(
3097 1,
3098 DropProbeFactory {
3099 specs: specs.clone(),
3100 },
3101 ClassifiedWorkerPoolConfig {
3102 max_idle_count_per_classification: max_idle_count,
3103 idle_timeout,
3104 ..Default::default()
3105 },
3106 );
3107 (pool, specs)
3108 }
3109
3110 fn drop_lock_probe(
3111 pool: &DropProbePool,
3112 working: Arc<AtomicBool>,
3113 ) -> (DropProbeSpec, mpsc::Receiver<bool>) {
3114 let (tx, rx) = mpsc::channel();
3115 let pool_ref = Arc::downgrade(pool);
3116 let on_drop = Box::new(move || {
3117 let pool_ref = pool_ref.upgrade().unwrap();
3118 tx.send(pool_ref.state.try_lock().is_ok()).unwrap();
3119 });
3120 (
3121 DropProbeSpec {
3122 working,
3123 on_drop: Some(on_drop),
3124 },
3125 rx,
3126 )
3127 }
3128
3129 fn plain_drop_probe_spec() -> DropProbeSpec {
3130 DropProbeSpec {
3131 working: Arc::new(AtomicBool::new(true)),
3132 on_drop: None,
3133 }
3134 }
3135
3136 #[derive(Copy, Clone)]
3137 enum IdleDropPath {
3138 Cleanup,
3139 IdleLimit,
3140 GenericInvalidScan,
3141 ClassifiedInvalidScan,
3142 Clear,
3143 }
3144
3145 async fn assert_idle_drop_path_runs_outside_lock(path: IdleDropPath) {
3146 let idle_timeout = matches!(path, IdleDropPath::Cleanup).then_some(Duration::ZERO);
3147 let max_idle_count = matches!(path, IdleDropPath::IdleLimit).then_some(0);
3148 let (pool, specs) = new_drop_probe_pool(idle_timeout, max_idle_count);
3149 let working = Arc::new(AtomicBool::new(true));
3150 let (spec, drop_result) = drop_lock_probe(&pool, working.clone());
3151 specs.lock().unwrap().push_back(spec);
3152
3153 let worker = pool.get_classified_worker(Classification::A).await.unwrap();
3154 drop(worker);
3155
3156 match path {
3157 IdleDropPath::Cleanup => {
3158 assert_eq!(pool.cleanup_idle_worker(), 1);
3159 }
3160 IdleDropPath::IdleLimit => {
3161 }
3163 IdleDropPath::GenericInvalidScan => {
3164 working.store(false, Ordering::SeqCst);
3165 specs.lock().unwrap().push_back(plain_drop_probe_spec());
3166 let worker = pool.get_worker().await.unwrap();
3167 drop(worker);
3168 }
3169 IdleDropPath::ClassifiedInvalidScan => {
3170 working.store(false, Ordering::SeqCst);
3171 specs.lock().unwrap().push_back(plain_drop_probe_spec());
3172 let worker = pool.get_classified_worker(Classification::A).await.unwrap();
3173 drop(worker);
3174 }
3175 IdleDropPath::Clear => pool.clear_all_worker().await,
3176 }
3177
3178 assert!(drop_result.recv_timeout(Duration::from_secs(1)).unwrap());
3179 }
3180
3181 #[tokio::test]
3182 async fn test_all_classified_idle_drop_paths_run_outside_state_lock() {
3183 for path in [
3184 IdleDropPath::Cleanup,
3185 IdleDropPath::IdleLimit,
3186 IdleDropPath::GenericInvalidScan,
3187 IdleDropPath::ClassifiedInvalidScan,
3188 IdleDropPath::Clear,
3189 ] {
3190 assert_idle_drop_path_runs_outside_lock(path).await;
3191 }
3192 }
3193
3194 struct MutableWorker {
3195 working: Arc<AtomicBool>,
3196 valid: Arc<AtomicBool>,
3197 classification: Classification,
3198 }
3199
3200 #[async_trait::async_trait]
3201 impl ClassifiedWorker<Classification> for MutableWorker {
3202 fn is_work(&self) -> bool {
3203 self.working.load(Ordering::SeqCst)
3204 }
3205
3206 fn is_valid(&self, classification: Classification) -> bool {
3207 self.valid.load(Ordering::SeqCst) && self.classification == classification
3208 }
3209
3210 fn classification(&self) -> Classification {
3211 self.classification.clone()
3212 }
3213 }
3214
3215 struct MutableWorkerFactory;
3216
3217 #[async_trait::async_trait]
3218 impl ClassifiedWorkerFactory<Classification, MutableWorker> for MutableWorkerFactory {
3219 async fn create(
3220 &self,
3221 classification: Option<Classification>,
3222 ) -> PoolResult<MutableWorker> {
3223 Ok(MutableWorker {
3224 working: Arc::new(AtomicBool::new(true)),
3225 valid: Arc::new(AtomicBool::new(true)),
3226 classification: classification.unwrap_or(Classification::A),
3227 })
3228 }
3229 }
3230
3231 type MutablePool = ClassifiedWorkerPoolRef<Classification, MutableWorker, MutableWorkerFactory>;
3232
3233 fn new_mutable_pool(max_count: u16, idle_timeout: Option<Duration>) -> MutablePool {
3234 new_classified_worker_pool(
3235 max_count,
3236 MutableWorkerFactory,
3237 ClassifiedWorkerPoolConfig {
3238 idle_timeout,
3239 ..Default::default()
3240 },
3241 )
3242 }
3243
3244 fn new_limited_mutable_pool(max_count: u16, max_count_per_classification: u16) -> MutablePool {
3245 new_classified_worker_pool(
3246 max_count,
3247 MutableWorkerFactory,
3248 ClassifiedWorkerPoolConfig {
3249 idle_timeout: None,
3250 max_count_per_classification: Some(max_count_per_classification),
3251 ..Default::default()
3252 },
3253 )
3254 }
3255
3256 fn assert_accounting_empty(pool: &MutablePool) {
3257 let state = pool.state.lock().unwrap();
3258 assert_eq!(state.current_count, 0);
3259 assert!(state.classified_count_map.is_empty());
3260 assert!(state.pending_classified_count_map.is_empty());
3261 }
3262
3263 fn assert_only_classification(
3264 pool: &MutablePool,
3265 classification: Classification,
3266 count: usize,
3267 ) {
3268 let state = pool.state.lock().unwrap();
3269 assert_eq!(state.current_count, count);
3270 assert_eq!(state.classified_count_map.len(), 1);
3271 assert_eq!(
3272 state.classified_count_map.get(&classification).copied(),
3273 Some(count)
3274 );
3275 }
3276
3277 #[derive(Copy, Clone)]
3278 enum IdleAccountingPath {
3279 Cleanup,
3280 Clear,
3281 GenericInvalidScan,
3282 ClassifiedInvalidScan,
3283 }
3284
3285 async fn assert_idle_accounting_path(path: IdleAccountingPath) {
3286 let idle_timeout = matches!(path, IdleAccountingPath::Cleanup).then_some(Duration::ZERO);
3287 let pool = new_mutable_pool(1, idle_timeout);
3288 let worker = pool.get_classified_worker(Classification::A).await.unwrap();
3289 let working = worker.working.clone();
3290 drop(worker);
3291
3292 match path {
3293 IdleAccountingPath::Cleanup => {
3294 assert_eq!(pool.cleanup_idle_worker(), 1);
3295 assert_accounting_empty(&pool);
3296 }
3297 IdleAccountingPath::Clear => {
3298 pool.clear_all_worker().await;
3299 assert_accounting_empty(&pool);
3300 }
3301 IdleAccountingPath::GenericInvalidScan => {
3302 working.store(false, Ordering::SeqCst);
3303 let worker = pool.get_worker().await.unwrap();
3304 assert_only_classification(&pool, Classification::A, 1);
3305 worker.working.store(false, Ordering::SeqCst);
3306 drop(worker);
3307 assert_accounting_empty(&pool);
3308 }
3309 IdleAccountingPath::ClassifiedInvalidScan => {
3310 working.store(false, Ordering::SeqCst);
3311 let worker = pool.get_classified_worker(Classification::A).await.unwrap();
3312 assert_only_classification(&pool, Classification::A, 1);
3313 worker.working.store(false, Ordering::SeqCst);
3314 drop(worker);
3315 assert_accounting_empty(&pool);
3316 }
3317 }
3318 }
3319
3320 #[tokio::test]
3321 async fn test_all_idle_accounting_paths() {
3322 for path in [
3323 IdleAccountingPath::Cleanup,
3324 IdleAccountingPath::Clear,
3325 IdleAccountingPath::GenericInvalidScan,
3326 IdleAccountingPath::ClassifiedInvalidScan,
3327 ] {
3328 assert_idle_accounting_path(path).await;
3329 }
3330 }
3331
3332 async fn wait_for_waiter(pool: &MutablePool) {
3333 loop {
3334 if !pool.state.lock().unwrap().waiting_list.is_empty() {
3335 return;
3336 }
3337 tokio::task::yield_now().await;
3338 }
3339 }
3340
3341 #[tokio::test]
3342 async fn test_canceled_waiter_is_removed_when_worker_returns() {
3343 let pool = new_limited_mutable_pool(1, 1);
3344 let worker_a = pool.get_classified_worker(Classification::A).await.unwrap();
3345
3346 let pool_ref = pool.clone();
3347 let waiter =
3348 tokio::spawn(async move { pool_ref.get_classified_worker(Classification::A).await });
3349 wait_for_waiter(&pool).await;
3350 waiter.abort();
3351 assert!(matches!(waiter.await, Err(err) if err.is_cancelled()));
3352
3353 drop(worker_a);
3354
3355 let state = pool.state.lock().unwrap();
3356 assert!(state.waiting_list.is_empty());
3357 assert_eq!(
3358 state.worker_list.values().map(VecDeque::len).sum::<usize>(),
3359 1
3360 );
3361 }
3362
3363 #[tokio::test]
3364 async fn test_worker_invalid_for_primary_classification_wakes_waiter() {
3365 let pool = new_limited_mutable_pool(1, 1);
3366 let worker_a = pool.get_classified_worker(Classification::A).await.unwrap();
3367 let valid = worker_a.valid.clone();
3368
3369 let pool_ref = pool.clone();
3370 let waiting_a =
3371 tokio::spawn(async move { pool_ref.get_classified_worker(Classification::A).await });
3372 wait_for_waiter(&pool).await;
3373
3374 valid.store(false, Ordering::SeqCst);
3375 drop(worker_a);
3376
3377 let replacement_a = tokio::time::timeout(Duration::from_secs(1), waiting_a)
3378 .await
3379 .unwrap()
3380 .unwrap()
3381 .unwrap();
3382 assert_eq!(replacement_a.classification(), Classification::A);
3383 }
3384
3385 #[tokio::test]
3386 async fn test_idle_worker_invalid_for_primary_classification_is_replaced() {
3387 let pool = new_limited_mutable_pool(1, 1);
3388 let worker_a = pool.get_classified_worker(Classification::A).await.unwrap();
3389 let valid = worker_a.valid.clone();
3390 drop(worker_a);
3391
3392 valid.store(false, Ordering::SeqCst);
3393
3394 let replacement_a = tokio::time::timeout(
3395 Duration::from_secs(1),
3396 pool.get_classified_worker(Classification::A),
3397 )
3398 .await
3399 .unwrap()
3400 .unwrap();
3401 assert_eq!(replacement_a.classification(), Classification::A);
3402 }
3403
3404 #[tokio::test]
3405 async fn test_capped_waiter_does_not_replace_other_classification_worker() {
3406 let pool = new_limited_mutable_pool(2, 1);
3407 let worker_a = pool.get_classified_worker(Classification::A).await.unwrap();
3408 let worker_b = pool.get_classified_worker(Classification::B).await.unwrap();
3409
3410 let pool_ref = pool.clone();
3411 let waiting_a =
3412 tokio::spawn(async move { pool_ref.get_classified_worker(Classification::A).await });
3413 wait_for_waiter(&pool).await;
3414
3415 drop(worker_b);
3416 tokio::task::yield_now().await;
3417
3418 {
3419 let state = pool.state.lock().unwrap();
3420 assert_eq!(state.current_count, 2);
3421 assert_eq!(
3422 state.worker_list.values().map(VecDeque::len).sum::<usize>(),
3423 1
3424 );
3425 assert_eq!(
3426 state
3427 .worker_list
3428 .get(&Classification::B)
3429 .and_then(VecDeque::front)
3430 .unwrap()
3431 .primary_classification,
3432 Classification::B
3433 );
3434 }
3435 assert!(!waiting_a.is_finished());
3436
3437 drop(worker_a);
3438 let replacement_a = tokio::time::timeout(Duration::from_secs(1), waiting_a)
3439 .await
3440 .unwrap()
3441 .unwrap()
3442 .unwrap();
3443 assert_eq!(replacement_a.classification(), Classification::A);
3444 }
3445
3446 #[tokio::test]
3447 async fn test_changed_classification_worker_is_removed_and_generic_waiter_retries() {
3448 let pool = new_mutable_pool(1, None);
3449 let mut worker = pool.get_classified_worker(Classification::A).await.unwrap();
3450 worker.classification = Classification::B;
3451
3452 let pool_ref = pool.clone();
3453 let waiter = tokio::spawn(async move { pool_ref.get_worker().await });
3454 wait_for_waiter(&pool).await;
3455 drop(worker);
3456
3457 let worker = waiter.await.unwrap().unwrap();
3458 assert_eq!(worker.classification(), Classification::A);
3459 worker.working.store(false, Ordering::SeqCst);
3460 drop(worker);
3461 assert_accounting_empty(&pool);
3462 }
3463
3464 #[tokio::test]
3465 async fn test_cached_classification_is_used_while_clearing() {
3466 let pool = new_mutable_pool(1, None);
3467 let mut worker = pool.get_classified_worker(Classification::A).await.unwrap();
3468 worker.classification = Classification::B;
3469
3470 let pool_ref = pool.clone();
3471 let clear_task = tokio::spawn(async move { pool_ref.clear_all_worker().await });
3472 loop {
3473 if pool.state.lock().unwrap().clearing {
3474 break;
3475 }
3476 tokio::task::yield_now().await;
3477 }
3478 drop(worker);
3479 clear_task.await.unwrap();
3480 assert_accounting_empty(&pool);
3481 }
3482
3483 #[tokio::test]
3484 async fn test_changed_classification_worker_does_not_wake_other_waiter() {
3485 let pool = new_mutable_pool(1, None);
3486 let mut worker_a = pool.get_classified_worker(Classification::A).await.unwrap();
3487 let worker_b = pool.get_classified_worker(Classification::B).await.unwrap();
3488 worker_a.classification = Classification::C;
3489
3490 let pool_ref = pool.clone();
3491 let waiter =
3492 tokio::spawn(async move { pool_ref.get_classified_worker(Classification::B).await });
3493 wait_for_waiter(&pool).await;
3494 drop(worker_a);
3495 tokio::task::yield_now().await;
3496 assert!(!waiter.is_finished());
3497
3498 drop(worker_b);
3499 let replacement_b = waiter.await.unwrap().unwrap();
3500 assert_only_classification(&pool, Classification::B, 1);
3501 replacement_b.working.store(false, Ordering::SeqCst);
3502 drop(replacement_b);
3503 assert_accounting_empty(&pool);
3504 }
3505
3506 #[tokio::test]
3507 async fn test_changed_classification_worker_is_removed_independently() {
3508 let pool = new_mutable_pool(1, None);
3509 let worker_a = pool.get_classified_worker(Classification::A).await.unwrap();
3510 let mut worker_b = pool.get_classified_worker(Classification::B).await.unwrap();
3511 worker_b.classification = Classification::C;
3512 drop(worker_b);
3513
3514 assert_only_classification(&pool, Classification::A, 1);
3515 worker_a.working.store(false, Ordering::SeqCst);
3516 drop(worker_a);
3517 assert_accounting_empty(&pool);
3518 }
3519
3520 #[tokio::test]
3521 async fn test_max_count_per_classification_blocks_only_that_classification() {
3522 let pool = new_limited_mutable_pool(4, 2);
3523 let worker_a1 = pool.get_classified_worker(Classification::A).await.unwrap();
3524 let worker_a2 = pool.get_classified_worker(Classification::A).await.unwrap();
3525
3526 let pool_ref = pool.clone();
3527 let waiting_a =
3528 tokio::spawn(async move { pool_ref.get_classified_worker(Classification::A).await });
3529 wait_for_waiter(&pool).await;
3530 assert!(!waiting_a.is_finished());
3531
3532 let worker_b = pool.get_classified_worker(Classification::B).await.unwrap();
3533 {
3534 let state = pool.state.lock().unwrap();
3535 assert_eq!(state.current_count, 3);
3536 assert_eq!(state.reserved_classified_count(&Classification::A), 2);
3537 assert_eq!(state.reserved_classified_count(&Classification::B), 1);
3538 }
3539
3540 drop(worker_a1);
3541 let replacement_a = tokio::time::timeout(Duration::from_secs(1), waiting_a)
3542 .await
3543 .unwrap()
3544 .unwrap()
3545 .unwrap();
3546 assert_eq!(replacement_a.classification(), Classification::A);
3547
3548 drop(worker_a2);
3549 drop(replacement_a);
3550 drop(worker_b);
3551 }
3552
3553 #[tokio::test]
3554 async fn test_generic_get_does_not_exceed_classification_limit() {
3555 let pool = new_limited_mutable_pool(3, 1);
3556 let worker_a = pool.get_classified_worker(Classification::A).await.unwrap();
3557
3558 let pool_ref = pool.clone();
3559 let generic = tokio::spawn(async move { pool_ref.get_worker().await });
3560 wait_for_waiter(&pool).await;
3561 assert!(!generic.is_finished());
3562 {
3563 let state = pool.state.lock().unwrap();
3564 assert_eq!(state.current_count, 1);
3565 assert_eq!(state.reserved_classified_count(&Classification::A), 1);
3566 }
3567
3568 let worker_b = pool.get_classified_worker(Classification::B).await.unwrap();
3569 drop(worker_b);
3570 let generic_worker = tokio::time::timeout(Duration::from_secs(1), generic)
3571 .await
3572 .unwrap()
3573 .unwrap()
3574 .unwrap();
3575 assert_eq!(generic_worker.classification(), Classification::B);
3576
3577 drop(worker_a);
3578 drop(generic_worker);
3579 }
3580
3581 #[tokio::test]
3582 async fn test_zero_max_count_per_classification_returns_error() {
3583 let pool = new_limited_mutable_pool(1, 0);
3584
3585 let generic_error = pool.get_worker().await.err().unwrap();
3586 assert_eq!(generic_error.code(), crate::PoolErrorCode::InvalidConfig);
3587
3588 let classified_error = pool
3589 .get_classified_worker(Classification::A)
3590 .await
3591 .err()
3592 .unwrap();
3593 assert_eq!(classified_error.code(), crate::PoolErrorCode::InvalidConfig);
3594 }
3595}