1use std::sync::Arc;
7use std::time::Instant;
8
9use a3s_box_core::config::{BoxConfig, PoolConfig};
10use a3s_box_core::error::{BoxError, Result};
11use a3s_box_core::event::{BoxEvent, EventEmitter};
12use tokio::sync::watch;
13use tokio::sync::Mutex;
14use tokio::task::JoinHandle;
15
16use crate::pool::scaler::PoolScaler;
17use crate::vm::VmManager;
18
19struct WarmVm {
21 vm: VmManager,
23 created_at: Instant,
25}
26
27#[derive(Debug, Clone)]
29pub struct PoolStats {
30 pub idle_count: usize,
32 pub total_created: u64,
34 pub total_acquired: u64,
36 pub total_released: u64,
38 pub total_evicted: u64,
40}
41
42pub struct WarmPool {
58 config: PoolConfig,
60 box_config: BoxConfig,
62 idle: Arc<Mutex<Vec<WarmVm>>>,
64 stats: Arc<Mutex<PoolStats>>,
66 event_emitter: EventEmitter,
68 replenish_handle: Option<JoinHandle<()>>,
70 shutdown_tx: watch::Sender<bool>,
72 shutdown_rx: watch::Receiver<bool>,
74 scaler: Option<Arc<Mutex<PoolScaler>>>,
76 metrics: Option<crate::prom::RuntimeMetrics>,
78 template: Arc<Mutex<TemplateState>>,
84}
85
86#[derive(Clone)]
89struct PoolTemplate {
90 mem_file: String,
91 state_file: String,
92}
93
94const MAX_TEMPLATE_BUILD_FAILURES: u32 = 3;
102
103enum TemplateState {
105 Unbuilt,
107 Ready(PoolTemplate),
109 Failing(u32),
113 Unavailable,
117}
118
119impl WarmPool {
120 pub async fn start(
125 config: PoolConfig,
126 box_config: BoxConfig,
127 event_emitter: EventEmitter,
128 ) -> Result<Self> {
129 if config.max_size == 0 {
130 return Err(BoxError::PoolError(
131 "Pool max_size must be greater than 0".to_string(),
132 ));
133 }
134 if config.min_idle > config.max_size {
135 return Err(BoxError::PoolError(format!(
136 "Pool min_idle ({}) cannot exceed max_size ({})",
137 config.min_idle, config.max_size
138 )));
139 }
140
141 let idle = Arc::new(Mutex::new(Vec::with_capacity(config.max_size)));
142 let stats = Arc::new(Mutex::new(PoolStats {
143 idle_count: 0,
144 total_created: 0,
145 total_acquired: 0,
146 total_released: 0,
147 total_evicted: 0,
148 }));
149 let (shutdown_tx, shutdown_rx) = watch::channel(false);
150
151 let scaler = if config.scaling.enabled {
152 Some(Arc::new(Mutex::new(PoolScaler::new(
153 config.scaling.clone(),
154 config.min_idle,
155 config.max_size,
156 ))))
157 } else {
158 None
159 };
160
161 let mut pool = Self {
162 config,
163 box_config,
164 idle,
165 stats,
166 event_emitter,
167 replenish_handle: None,
168 shutdown_tx,
169 shutdown_rx,
170 scaler,
171 metrics: None,
172 template: Arc::new(Mutex::new(TemplateState::Unbuilt)),
173 };
174
175 pool.fill_to_min().await;
177
178 let handle = pool.spawn_maintenance_loop();
180 pool.replenish_handle = Some(handle);
181
182 tracing::info!(
183 min_idle = pool.config.min_idle,
184 max_size = pool.config.max_size,
185 idle_ttl_secs = pool.config.idle_ttl_secs,
186 "Warm pool started"
187 );
188
189 Ok(pool)
190 }
191
192 pub fn set_metrics(&mut self, metrics: crate::prom::RuntimeMetrics) {
194 metrics.warm_pool_capacity.set(self.config.max_size as i64);
195 self.metrics = Some(metrics);
196 }
197
198 pub async fn acquire(&self) -> Result<VmManager> {
203 {
205 let mut idle = self.idle.lock().await;
206 if let Some(warm_vm) = idle.pop() {
207 let mut stats = self.stats.lock().await;
208 stats.total_acquired += 1;
209 stats.idle_count = idle.len();
210
211 if let Some(ref scaler) = self.scaler {
213 scaler.lock().await.record_acquire(true);
214 }
215
216 if let Some(ref m) = self.metrics {
217 m.warm_pool_hits.inc();
218 m.warm_pool_size.set(idle.len() as i64);
219 }
220
221 self.event_emitter.emit(BoxEvent::with_string(
222 "pool.vm.acquired",
223 format!("Acquired VM {} from pool", warm_vm.vm.box_id()),
224 ));
225
226 tracing::debug!(
227 box_id = %warm_vm.vm.box_id(),
228 idle_remaining = idle.len(),
229 "Acquired VM from warm pool"
230 );
231
232 return Ok(warm_vm.vm);
233 }
234 }
235
236 tracing::info!("No idle VM in pool, booting on demand");
238
239 if let Some(ref scaler) = self.scaler {
241 scaler.lock().await.record_acquire(false);
242 }
243
244 if let Some(ref m) = self.metrics {
245 m.warm_pool_misses.inc();
246 }
247
248 let vm = self.boot_new_vm().await?;
249
250 let mut stats = self.stats.lock().await;
251 stats.total_acquired += 1;
252
253 Ok(vm)
254 }
255
256 pub async fn release(&self, vm: VmManager) -> Result<()> {
260 let mut idle = self.idle.lock().await;
261
262 if *self.shutdown_rx.borrow() {
267 drop(idle);
268 let mut vm = vm;
269 vm.destroy().await?;
270 return Ok(());
271 }
272
273 if idle.len() >= self.config.max_size {
274 drop(idle); let mut vm = vm;
277 vm.destroy().await?;
278
279 tracing::debug!(
280 box_id = %vm.box_id(),
281 "Pool full, destroyed released VM"
282 );
283 return Ok(());
284 }
285
286 let box_id = vm.box_id().to_string();
287 idle.push(WarmVm {
288 vm,
289 created_at: Instant::now(),
290 });
291
292 let mut stats = self.stats.lock().await;
293 stats.total_released += 1;
294 stats.idle_count = idle.len();
295
296 if let Some(ref m) = self.metrics {
297 m.warm_pool_size.set(idle.len() as i64);
298 }
299
300 self.event_emitter.emit(BoxEvent::with_string(
301 "pool.vm.released",
302 format!("Released VM {} back to pool", box_id),
303 ));
304
305 tracing::debug!(
306 box_id = %box_id,
307 idle_count = idle.len(),
308 "Released VM back to warm pool"
309 );
310
311 Ok(())
312 }
313
314 pub async fn stats(&self) -> PoolStats {
316 self.stats.lock().await.clone()
317 }
318
319 pub async fn idle_count(&self) -> usize {
321 self.idle.lock().await.len()
322 }
323
324 pub fn signal_shutdown(&self) {
328 let _ = self.shutdown_tx.send(true);
329 tracing::info!("Warm pool shutdown signaled");
330 }
331
332 pub async fn drain(&mut self) -> Result<()> {
334 let _ = self.shutdown_tx.send(true);
336
337 if let Some(handle) = self.replenish_handle.take() {
339 let _ = handle.await;
340 }
341
342 let mut idle = self.idle.lock().await;
344 let count = idle.len();
345
346 for warm_vm in idle.drain(..) {
347 let mut vm = warm_vm.vm;
348 if let Err(e) = vm.destroy().await {
349 tracing::warn!(
350 box_id = %vm.box_id(),
351 error = %e,
352 "Failed to destroy pooled VM during drain"
353 );
354 }
355 }
356
357 let mut stats = self.stats.lock().await;
358 stats.idle_count = 0;
359
360 self.event_emitter.emit(BoxEvent::empty("pool.drained"));
361
362 tracing::info!(destroyed = count, "Warm pool drained");
363
364 Ok(())
365 }
366
367 pub async fn drain_idle(&self) -> Result<()> {
372 let mut idle = self.idle.lock().await;
373 let count = idle.len();
374 for warm_vm in idle.drain(..) {
375 let mut vm = warm_vm.vm;
376 if let Err(e) = vm.destroy().await {
377 tracing::warn!(
378 box_id = %vm.box_id(),
379 error = %e,
380 "Failed to destroy pooled VM during drain_idle"
381 );
382 }
383 }
384 self.stats.lock().await.idle_count = 0;
385 tracing::info!(destroyed = count, "Warm pool idle VMs drained");
386 Ok(())
387 }
388
389 async fn remove_idle_vms(&self, box_ids: &[String]) {
394 let indices_to_remove: Vec<usize> = {
396 let idle = self.idle.lock().await;
397 idle.iter()
398 .enumerate()
399 .filter(|(_, wm)| box_ids.iter().any(|id| id == wm.vm.box_id()))
400 .map(|(i, _)| i)
401 .collect()
402 };
403
404 if indices_to_remove.is_empty() {
405 return;
406 }
407
408 let mut to_destroy: Vec<WarmVm> = Vec::new();
411 {
412 let mut idle = self.idle.lock().await;
413 for idx in indices_to_remove.into_iter().rev() {
414 if idx < idle.len() {
415 let warm_vm = idle.remove(idx);
416 to_destroy.push(warm_vm);
417 }
418 }
419 }
420
421 {
423 let idle_count = self.idle.lock().await.len();
424 if let Ok(mut stats) = self.stats.try_lock() {
425 stats.idle_count = idle_count;
426 }
427 }
428
429 for warm_vm in to_destroy {
431 let box_id = warm_vm.vm.box_id().to_string();
432 let mut vm = warm_vm.vm;
433 if let Err(e) = vm.destroy().await {
434 tracing::warn!(
435 box_id = %box_id,
436 error = %e,
437 "Failed to destroy VM during fill_to_min rollback"
438 );
439 } else {
440 tracing::debug!(box_id = %box_id, "Destroyed VM during fill_to_min rollback");
441 }
442 }
443 }
444
445 async fn boot_new_vm(&self) -> Result<VmManager> {
447 let vm = Self::boot_or_restore(
448 self.config.snapshot_fork,
449 &self.box_config,
450 &self.event_emitter,
451 &self.template,
452 )
453 .await?;
454
455 let mut stats = self.stats.lock().await;
456 stats.total_created += 1;
457
458 self.event_emitter.emit(BoxEvent::with_string(
459 "pool.vm.created",
460 format!("Booted new VM {}", vm.box_id()),
461 ));
462
463 Ok(vm)
464 }
465
466 async fn boot_or_restore(
469 snapshot_fork: bool,
470 box_config: &BoxConfig,
471 event_emitter: &EventEmitter,
472 template: &Arc<Mutex<TemplateState>>,
473 ) -> Result<VmManager> {
474 if snapshot_fork {
475 match Self::ensure_template(box_config, event_emitter, template).await {
480 Ok(tpl) => {
481 let mut cfg = box_config.clone();
482 cfg.snapshot_mem_file = Some(tpl.mem_file.clone());
483 cfg.restore_from = Some(tpl.state_file.clone());
484 cfg.snapshot_sock = None;
485 let mut vm = VmManager::new(cfg, event_emitter.clone());
486 vm.boot().await?;
487 vm.wait_for_exec_available(std::time::Duration::from_secs(120))
488 .await?;
489 return Ok(vm);
490 }
491 Err(error) => {
492 tracing::debug!(%error, "snapshot-fork unavailable; cold-booting this pool VM");
493 }
494 }
495 }
496 let mut vm = VmManager::new(box_config.clone(), event_emitter.clone());
497 vm.boot().await?;
498 vm.wait_for_exec_available(std::time::Duration::from_secs(120))
499 .await?;
500 Ok(vm)
501 }
502
503 async fn ensure_template(
510 box_config: &BoxConfig,
511 event_emitter: &EventEmitter,
512 template: &Arc<Mutex<TemplateState>>,
513 ) -> Result<PoolTemplate> {
514 let mut guard = template.lock().await;
515 let prior_failures = match &*guard {
516 TemplateState::Ready(t) => return Ok(t.clone()),
517 TemplateState::Unavailable => {
518 return Err(BoxError::PoolError(
519 "snapshot-fork template unavailable (native VM snapshot unsupported)"
520 .to_string(),
521 ));
522 }
523 TemplateState::Failing(n) => *n,
525 TemplateState::Unbuilt => 0,
526 };
527
528 match Self::build_template(box_config, event_emitter).await {
529 Ok(tpl) => {
530 *guard = TemplateState::Ready(tpl.clone());
531 event_emitter.emit(BoxEvent::with_string(
532 "pool.template.built",
533 format!(
534 "Snapshot-fork template built for image {}",
535 box_config.image
536 ),
537 ));
538 Ok(tpl)
539 }
540 Err(error) => {
541 let failures = prior_failures + 1;
546 if failures >= MAX_TEMPLATE_BUILD_FAILURES {
547 tracing::warn!(
548 %error, failures,
549 "snapshot-fork template build failed repeatedly; marking \
550 unavailable — the warm pool will cold-boot"
551 );
552 *guard = TemplateState::Unavailable;
553 } else {
554 tracing::warn!(
555 %error, failures,
556 "snapshot-fork template build failed; will retry on a later fill"
557 );
558 *guard = TemplateState::Failing(failures);
559 }
560 Err(error)
561 }
562 }
563 }
564
565 async fn build_template(
568 box_config: &BoxConfig,
569 event_emitter: &EventEmitter,
570 ) -> Result<PoolTemplate> {
571 let dir = a3s_box_core::dirs_home().join("pool").join(format!(
572 "tpl-{:016x}",
573 crate::vm::fnv1a_hash(&box_config.image)
574 ));
575 std::fs::create_dir_all(&dir).map_err(BoxError::IoError)?;
576
577 let lock_target = dir.clone();
584 let _lock =
585 tokio::task::spawn_blocking(move || crate::file_lock::FileLock::acquire(&lock_target))
586 .await
587 .map_err(|e| BoxError::PoolError(format!("Template lock task failed: {e}")))?
588 .map_err(|e| BoxError::PoolError(format!("Failed to lock template dir: {e}")))?;
589
590 let mem_file = dir.join("template.ram");
591 let sock = dir.join("template.sock");
592 let state_file = dir.join("template.state");
593 let _ = std::fs::remove_file(&sock);
594
595 let mut cfg = box_config.clone();
597 cfg.snapshot_mem_file = Some(mem_file.to_string_lossy().into_owned());
598 cfg.snapshot_sock = Some(sock.to_string_lossy().into_owned());
599 cfg.restore_from = None;
600 let mut src = VmManager::new(cfg, event_emitter.clone());
601 src.boot().await?;
602
603 let snapshot = Self::trigger_snapshot(&sock, &state_file).await;
612 let _ = src.destroy_with_timeout(2000).await;
613 snapshot?;
614
615 Ok(PoolTemplate {
616 mem_file: mem_file.to_string_lossy().into_owned(),
617 state_file: state_file.to_string_lossy().into_owned(),
618 })
619 }
620
621 #[cfg(unix)]
627 async fn trigger_snapshot(sock: &std::path::Path, state_file: &std::path::Path) -> Result<()> {
628 use tokio::io::{AsyncReadExt, AsyncWriteExt};
629 let mut stream = None;
631 for _ in 0..200 {
632 match tokio::net::UnixStream::connect(sock).await {
633 Ok(s) => {
634 stream = Some(s);
635 break;
636 }
637 Err(_) => tokio::time::sleep(std::time::Duration::from_millis(25)).await,
638 }
639 }
640 let mut stream = stream.ok_or_else(|| {
641 BoxError::PoolError(format!("snapshot socket {} never appeared", sock.display()))
642 })?;
643 let cmd = format!("snapshot {}\n", state_file.display());
644 stream
645 .write_all(cmd.as_bytes())
646 .await
647 .map_err(BoxError::IoError)?;
648 let mut buf = [0u8; 64];
649 let n = stream.read(&mut buf).await.map_err(BoxError::IoError)?;
650 let reply = String::from_utf8_lossy(&buf[..n]);
651 if reply.trim() == "ok" {
652 Ok(())
653 } else {
654 Err(BoxError::PoolError(format!(
655 "snapshot trigger failed: {}",
656 reply.trim()
657 )))
658 }
659 }
660
661 #[cfg(not(unix))]
665 async fn trigger_snapshot(
666 _sock: &std::path::Path,
667 _state_file: &std::path::Path,
668 ) -> Result<()> {
669 Err(BoxError::PoolError(
670 "snapshot-fork is only supported on Linux/KVM hosts".to_string(),
671 ))
672 }
673
674 async fn fill_to_min(&self) {
676 let current = self.idle.lock().await.len();
677 let needed = self.config.min_idle.saturating_sub(current);
678
679 if needed == 0 {
680 return;
681 }
682
683 tracing::debug!(
684 current,
685 needed,
686 min_idle = self.config.min_idle,
687 "Replenishing warm pool"
688 );
689
690 let mut added_ids: Vec<String> = Vec::new();
692
693 for _ in 0..needed {
694 match self.boot_new_vm().await {
695 Ok(vm) => {
696 let box_id = vm.box_id().to_string();
697 let mut idle = self.idle.lock().await;
698 idle.push(WarmVm {
699 vm,
700 created_at: Instant::now(),
701 });
702 let mut stats = self.stats.lock().await;
703 stats.idle_count = idle.len();
704 added_ids.push(box_id.clone());
705
706 tracing::debug!(box_id = %box_id, "Added VM to warm pool");
707 }
708 Err(e) => {
709 tracing::warn!(error = %e, "Failed to boot VM for warm pool");
710 if !added_ids.is_empty() {
712 tracing::info!(
713 count = added_ids.len(),
714 "Cleaning up VMs added before fill_to_min failed"
715 );
716 self.remove_idle_vms(&added_ids).await;
717 }
718 break;
719 }
720 }
721 }
722
723 self.event_emitter.emit(BoxEvent::empty("pool.replenish"));
724 }
725
726 fn spawn_maintenance_loop(&self) -> JoinHandle<()> {
733 let idle = Arc::clone(&self.idle);
734 let stats = Arc::clone(&self.stats);
735 let config = self.config.clone();
736 let box_config = self.box_config.clone();
737 let event_emitter = self.event_emitter.clone();
738 let mut shutdown_rx = self.shutdown_rx.clone();
739 let scaler = self.scaler.clone();
740 let template = Arc::clone(&self.template);
741
742 tokio::spawn(async move {
743 let check_interval = std::time::Duration::from_secs(
744 if config.idle_ttl_secs > 0 {
746 (config.idle_ttl_secs / 5).max(5)
747 } else {
748 30
749 },
750 );
751
752 let mut effective_min_idle = config.min_idle;
754
755 loop {
756 tokio::select! {
757 result = shutdown_rx.changed() => {
758 if result.is_ok() && *shutdown_rx.borrow() {
759 tracing::debug!("Pool maintenance loop shutting down");
760 break;
761 }
762 }
763 _ = tokio::time::sleep(check_interval) => {
764 if config.idle_ttl_secs > 0 {
766 Self::evict_expired_static(
767 &idle,
768 &stats,
769 &event_emitter,
770 config.idle_ttl_secs,
771 ).await;
772 }
773
774 if let Some(ref scaler) = scaler {
776 let mut s = scaler.lock().await;
777 let decision = s.evaluate();
778 let new_min = s.current_min_idle();
779 if new_min != effective_min_idle {
780 tracing::info!(
781 old_min_idle = effective_min_idle,
782 new_min_idle = new_min,
783 ?decision,
784 "Autoscaler adjusted min_idle"
785 );
786 event_emitter.emit(BoxEvent::with_string(
787 "pool.autoscale",
788 format!(
789 "min_idle adjusted {} → {} ({:?})",
790 effective_min_idle, new_min, decision
791 ),
792 ));
793 effective_min_idle = new_min;
794 }
795 }
796
797 let current = idle.lock().await.len();
799 if current < effective_min_idle {
800 let needed = effective_min_idle - current;
801 tracing::debug!(current, needed, min_idle = effective_min_idle, "Replenishing warm pool");
802
803 let mut set = tokio::task::JoinSet::new();
810 for _ in 0..needed {
811 let sf = config.snapshot_fork;
812 let bc = box_config.clone();
813 let ee = event_emitter.clone();
814 let tpl = Arc::clone(&template);
815 set.spawn(async move {
816 WarmPool::boot_or_restore(sf, &bc, &ee, &tpl).await
817 });
818 }
819 while let Some(joined) = set.join_next().await {
820 match joined {
821 Ok(Ok(mut vm)) => {
822 let box_id = vm.box_id().to_string();
823 let mut pool = idle.lock().await;
834 if *shutdown_rx.borrow() {
835 drop(pool);
836 tracing::debug!(
837 box_id = %box_id,
838 "Pool shutting down mid-replenish; destroying freshly-booted VM"
839 );
840 let _ = vm.destroy_with_timeout(2000).await;
841 continue;
842 }
843 pool.push(WarmVm {
844 vm,
845 created_at: Instant::now(),
846 });
847 let mut s = stats.lock().await;
848 s.total_created += 1;
849 s.idle_count = pool.len();
850 drop(s);
851 drop(pool);
852
853 event_emitter.emit(BoxEvent::with_string(
854 "pool.vm.created",
855 format!("Replenished VM {}", box_id),
856 ));
857 }
858 Ok(Err(e)) => {
859 tracing::warn!(error = %e, "Failed to replenish warm pool");
860 }
861 Err(e) => {
862 tracing::warn!(error = %e, "Replenish task join error");
863 }
864 }
865 }
866
867 event_emitter.emit(BoxEvent::empty("pool.replenish"));
868 }
869 }
870 }
871 }
872 })
873 }
874
875 async fn evict_expired_static(
877 idle: &Arc<Mutex<Vec<WarmVm>>>,
878 stats: &Arc<Mutex<PoolStats>>,
879 event_emitter: &EventEmitter,
880 idle_ttl_secs: u64,
881 ) {
882 let ttl = std::time::Duration::from_secs(idle_ttl_secs);
883
884 let mut pool = idle.lock().await;
885 let mut kept = Vec::new();
886 let mut expired = Vec::new();
887
888 for warm_vm in pool.drain(..) {
889 if warm_vm.created_at.elapsed() > ttl {
890 expired.push(warm_vm);
891 } else {
892 kept.push(warm_vm);
893 }
894 }
895 *pool = kept;
896 let after_count = pool.len();
897 drop(pool);
898
899 let evicted_count = expired.len();
900 for warm_vm in expired {
901 let mut vm = warm_vm.vm;
902 let _ = vm.destroy().await;
903 }
904
905 if evicted_count > 0 {
906 let mut s = stats.lock().await;
907 s.total_evicted += evicted_count as u64;
908 s.idle_count = after_count;
909
910 event_emitter.emit(BoxEvent::with_string(
911 "pool.vm.evicted",
912 format!("Evicted {} expired VMs", evicted_count),
913 ));
914 }
915 }
916}
917
918#[cfg(test)]
919mod tests {
920 use super::*;
921 use a3s_box_core::config::PoolConfig;
922
923 fn test_pool_config(min_idle: usize, max_size: usize) -> PoolConfig {
924 PoolConfig {
925 enabled: true,
926 min_idle,
927 max_size,
928 idle_ttl_secs: 300,
929 ..Default::default()
930 }
931 }
932
933 fn test_event_emitter() -> EventEmitter {
934 EventEmitter::new(100)
935 }
936
937 #[tokio::test]
940 async fn test_pool_rejects_zero_max_size() {
941 let config = test_pool_config(0, 0);
942 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
943 match result {
944 Err(e) => assert!(e.to_string().contains("max_size must be greater than 0")),
945 Ok(_) => panic!("Expected error for zero max_size"),
946 }
947 }
948
949 #[tokio::test]
950 async fn test_pool_rejects_min_idle_exceeds_max() {
951 let config = test_pool_config(10, 5);
952 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
953 match result {
954 Err(e) => assert!(e.to_string().contains("cannot exceed max_size")),
955 Ok(_) => panic!("Expected error for min_idle > max_size"),
956 }
957 }
958
959 #[test]
962 fn test_pool_stats_default() {
963 let stats = PoolStats {
964 idle_count: 0,
965 total_created: 0,
966 total_acquired: 0,
967 total_released: 0,
968 total_evicted: 0,
969 };
970 assert_eq!(stats.idle_count, 0);
971 assert_eq!(stats.total_created, 0);
972 }
973
974 #[test]
975 fn test_pool_stats_clone() {
976 let stats = PoolStats {
977 idle_count: 3,
978 total_created: 10,
979 total_acquired: 7,
980 total_released: 5,
981 total_evicted: 2,
982 };
983 let cloned = stats.clone();
984 assert_eq!(cloned.idle_count, 3);
985 assert_eq!(cloned.total_created, 10);
986 assert_eq!(cloned.total_acquired, 7);
987 assert_eq!(cloned.total_released, 5);
988 assert_eq!(cloned.total_evicted, 2);
989 }
990
991 #[test]
992 fn test_pool_stats_debug() {
993 let stats = PoolStats {
994 idle_count: 1,
995 total_created: 2,
996 total_acquired: 3,
997 total_released: 4,
998 total_evicted: 5,
999 };
1000 let debug = format!("{:?}", stats);
1001 assert!(debug.contains("idle_count"));
1002 assert!(debug.contains("total_created"));
1003 }
1004
1005 #[test]
1008 fn test_pool_config_roundtrip() {
1009 let config = PoolConfig {
1010 enabled: true,
1011 min_idle: 3,
1012 max_size: 10,
1013 idle_ttl_secs: 600,
1014 ..Default::default()
1015 };
1016
1017 let json = serde_json::to_string(&config).unwrap();
1018 let parsed: PoolConfig = serde_json::from_str(&json).unwrap();
1019
1020 assert!(parsed.enabled);
1021 assert_eq!(parsed.min_idle, 3);
1022 assert_eq!(parsed.max_size, 10);
1023 assert_eq!(parsed.idle_ttl_secs, 600);
1024 }
1025
1026 #[test]
1027 fn test_pool_config_default_values() {
1028 let config = PoolConfig::default();
1029 assert!(!config.enabled);
1030 assert_eq!(config.min_idle, 1);
1031 assert_eq!(config.max_size, 5);
1032 assert_eq!(config.idle_ttl_secs, 300);
1033 }
1034
1035 #[test]
1036 fn test_pool_config_deserialization_with_defaults() {
1037 let json = r#"{"enabled": true}"#;
1038 let config: PoolConfig = serde_json::from_str(json).unwrap();
1039 assert!(config.enabled);
1040 assert_eq!(config.min_idle, 1);
1041 assert_eq!(config.max_size, 5);
1042 assert_eq!(config.idle_ttl_secs, 300);
1043 }
1044
1045 #[tokio::test]
1048 async fn test_pool_accepts_min_idle_equals_max() {
1049 let config = test_pool_config(3, 3);
1050 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
1053 match result {
1055 Err(e) => assert!(!e.to_string().contains("cannot exceed max_size")),
1056 Ok(mut pool) => {
1057 let _ = pool.drain().await;
1058 }
1059 }
1060 }
1061
1062 #[tokio::test]
1063 async fn test_pool_accepts_min_idle_zero() {
1064 let config = test_pool_config(0, 5);
1065 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
1067 match result {
1068 Ok(mut pool) => {
1069 assert_eq!(pool.idle_count().await, 0);
1071 let stats = pool.stats().await;
1072 assert_eq!(stats.idle_count, 0);
1073 assert_eq!(stats.total_created, 0);
1074 let _ = pool.drain().await;
1075 }
1076 Err(e) => {
1077 assert!(!e.to_string().contains("max_size"));
1079 assert!(!e.to_string().contains("min_idle"));
1080 }
1081 }
1082 }
1083
1084 #[tokio::test]
1087 async fn test_pool_stats_initial() {
1088 let config = test_pool_config(0, 5);
1089 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
1090 if let Ok(mut pool) = result {
1091 let stats = pool.stats().await;
1092 assert_eq!(stats.idle_count, 0);
1093 assert_eq!(stats.total_created, 0);
1094 assert_eq!(stats.total_acquired, 0);
1095 assert_eq!(stats.total_released, 0);
1096 assert_eq!(stats.total_evicted, 0);
1097 let _ = pool.drain().await;
1098 }
1099 }
1100
1101 #[tokio::test]
1102 async fn test_pool_idle_count_initial() {
1103 let config = test_pool_config(0, 5);
1104 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
1105 if let Ok(mut pool) = result {
1106 assert_eq!(pool.idle_count().await, 0);
1107 let _ = pool.drain().await;
1108 }
1109 }
1110
1111 #[tokio::test]
1112 async fn test_pool_drain_empty_pool() {
1113 let config = test_pool_config(0, 5);
1114 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
1115 if let Ok(mut pool) = result {
1116 let drain_result = pool.drain().await;
1118 assert!(drain_result.is_ok());
1119
1120 let stats = pool.stats().await;
1121 assert_eq!(stats.idle_count, 0);
1122 }
1123 }
1124
1125 #[tokio::test]
1126 async fn test_pool_drain_emits_event() {
1127 let emitter = test_event_emitter();
1128 let mut receiver = emitter.subscribe();
1129 let config = test_pool_config(0, 5);
1130
1131 let result = WarmPool::start(config, BoxConfig::default(), emitter).await;
1132 if let Ok(mut pool) = result {
1133 pool.drain().await.unwrap();
1134
1135 let mut found_drain_event = false;
1137 while let Ok(event) = receiver.try_recv() {
1139 if event.key == "pool.drained" {
1140 found_drain_event = true;
1141 }
1142 }
1143 assert!(found_drain_event, "Expected pool.drained event");
1144 }
1145 }
1146
1147 #[tokio::test]
1148 async fn test_pool_acquire_from_empty_pool_fails_without_shim() {
1149 let config = test_pool_config(0, 5);
1150 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
1151 if let Ok(pool) = result {
1152 let acquire_result = pool.acquire().await;
1155 assert!(acquire_result.is_err());
1156 }
1157 }
1158
1159 #[test]
1162 #[allow(clippy::unnecessary_min_or_max)]
1163 fn test_maintenance_check_interval_with_ttl() {
1164 let interval = if 300_u64 > 0 {
1166 (300_u64 / 5).max(5)
1167 } else {
1168 30
1169 };
1170 assert_eq!(interval, 60);
1171 }
1172
1173 #[test]
1174 #[allow(clippy::unnecessary_min_or_max)]
1175 fn test_maintenance_check_interval_short_ttl() {
1176 let interval = if 10_u64 > 0 { (10_u64 / 5).max(5) } else { 30 };
1178 assert_eq!(interval, 5);
1179 }
1180
1181 #[test]
1182 #[allow(clippy::unnecessary_min_or_max)]
1183 fn test_maintenance_check_interval_very_short_ttl() {
1184 let interval = if 1_u64 > 0 { (1_u64 / 5).max(5) } else { 30 };
1186 assert_eq!(interval, 5);
1187 }
1188
1189 #[test]
1190 #[allow(
1191 clippy::absurd_extreme_comparisons,
1192 clippy::erasing_op,
1193 clippy::unnecessary_min_or_max,
1194 unused_comparisons
1195 )]
1196 fn test_maintenance_check_interval_no_ttl() {
1197 let interval = if 0_u64 > 0 { (0_u64 / 5).max(5) } else { 30 };
1199 assert_eq!(interval, 30);
1200 }
1201
1202 #[test]
1205 fn test_warm_vm_created_at_is_recent() {
1206 let before = Instant::now();
1207 let created_at = Instant::now();
1208 let after = Instant::now();
1209
1210 assert!(created_at >= before);
1211 assert!(created_at <= after);
1212 }
1213
1214 #[test]
1217 fn test_pool_stats_all_fields() {
1218 let stats = PoolStats {
1219 idle_count: 10,
1220 total_created: 100,
1221 total_acquired: 80,
1222 total_released: 70,
1223 total_evicted: 15,
1224 };
1225
1226 assert_eq!(stats.idle_count, 10);
1227 assert_eq!(stats.total_created, 100);
1228 assert_eq!(stats.total_acquired, 80);
1229 assert_eq!(stats.total_released, 70);
1230 assert_eq!(stats.total_evicted, 15);
1231
1232 let debug = format!("{:?}", stats);
1234 assert!(debug.contains("10"));
1235 assert!(debug.contains("100"));
1236 assert!(debug.contains("80"));
1237 assert!(debug.contains("70"));
1238 assert!(debug.contains("15"));
1239 }
1240
1241 #[tokio::test]
1248 async fn test_pool_set_metrics_attaches() {
1249 let config = test_pool_config(0, 5);
1250 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
1251 match result {
1252 Ok(mut pool) => {
1253 let metrics = crate::prom::RuntimeMetrics::new();
1254 pool.set_metrics(metrics.clone());
1255 assert!(pool.metrics.is_some());
1256 assert_eq!(metrics.warm_pool_hits.get(), 0);
1258 assert_eq!(metrics.warm_pool_misses.get(), 0);
1259 assert_eq!(metrics.warm_pool_size.get(), 0);
1260 let _ = pool.drain().await;
1261 }
1262 Err(_) => {
1263 }
1265 }
1266 }
1267}