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 return Ok(vm);
488 }
489 Err(error) => {
490 tracing::debug!(%error, "snapshot-fork unavailable; cold-booting this pool VM");
491 }
492 }
493 }
494 let mut vm = VmManager::new(box_config.clone(), event_emitter.clone());
495 vm.boot().await?;
496 Ok(vm)
497 }
498
499 async fn ensure_template(
506 box_config: &BoxConfig,
507 event_emitter: &EventEmitter,
508 template: &Arc<Mutex<TemplateState>>,
509 ) -> Result<PoolTemplate> {
510 let mut guard = template.lock().await;
511 let prior_failures = match &*guard {
512 TemplateState::Ready(t) => return Ok(t.clone()),
513 TemplateState::Unavailable => {
514 return Err(BoxError::PoolError(
515 "snapshot-fork template unavailable (native VM snapshot unsupported)"
516 .to_string(),
517 ));
518 }
519 TemplateState::Failing(n) => *n,
521 TemplateState::Unbuilt => 0,
522 };
523
524 match Self::build_template(box_config, event_emitter).await {
525 Ok(tpl) => {
526 *guard = TemplateState::Ready(tpl.clone());
527 event_emitter.emit(BoxEvent::with_string(
528 "pool.template.built",
529 format!(
530 "Snapshot-fork template built for image {}",
531 box_config.image
532 ),
533 ));
534 Ok(tpl)
535 }
536 Err(error) => {
537 let failures = prior_failures + 1;
542 if failures >= MAX_TEMPLATE_BUILD_FAILURES {
543 tracing::warn!(
544 %error, failures,
545 "snapshot-fork template build failed repeatedly; marking \
546 unavailable — the warm pool will cold-boot"
547 );
548 *guard = TemplateState::Unavailable;
549 } else {
550 tracing::warn!(
551 %error, failures,
552 "snapshot-fork template build failed; will retry on a later fill"
553 );
554 *guard = TemplateState::Failing(failures);
555 }
556 Err(error)
557 }
558 }
559 }
560
561 async fn build_template(
564 box_config: &BoxConfig,
565 event_emitter: &EventEmitter,
566 ) -> Result<PoolTemplate> {
567 let dir = a3s_box_core::dirs_home().join("pool").join(format!(
568 "tpl-{:016x}",
569 crate::vm::fnv1a_hash(&box_config.image)
570 ));
571 std::fs::create_dir_all(&dir).map_err(BoxError::IoError)?;
572
573 let lock_target = dir.clone();
580 let _lock =
581 tokio::task::spawn_blocking(move || crate::file_lock::FileLock::acquire(&lock_target))
582 .await
583 .map_err(|e| BoxError::PoolError(format!("Template lock task failed: {e}")))?
584 .map_err(|e| BoxError::PoolError(format!("Failed to lock template dir: {e}")))?;
585
586 let mem_file = dir.join("template.ram");
587 let sock = dir.join("template.sock");
588 let state_file = dir.join("template.state");
589 let _ = std::fs::remove_file(&sock);
590
591 let mut cfg = box_config.clone();
593 cfg.snapshot_mem_file = Some(mem_file.to_string_lossy().into_owned());
594 cfg.snapshot_sock = Some(sock.to_string_lossy().into_owned());
595 cfg.restore_from = None;
596 let mut src = VmManager::new(cfg, event_emitter.clone());
597 src.boot().await?;
598
599 let snapshot = Self::trigger_snapshot(&sock, &state_file).await;
608 let _ = src.destroy_with_timeout(2000).await;
609 snapshot?;
610
611 Ok(PoolTemplate {
612 mem_file: mem_file.to_string_lossy().into_owned(),
613 state_file: state_file.to_string_lossy().into_owned(),
614 })
615 }
616
617 #[cfg(unix)]
623 async fn trigger_snapshot(sock: &std::path::Path, state_file: &std::path::Path) -> Result<()> {
624 use tokio::io::{AsyncReadExt, AsyncWriteExt};
625 let mut stream = None;
627 for _ in 0..200 {
628 match tokio::net::UnixStream::connect(sock).await {
629 Ok(s) => {
630 stream = Some(s);
631 break;
632 }
633 Err(_) => tokio::time::sleep(std::time::Duration::from_millis(25)).await,
634 }
635 }
636 let mut stream = stream.ok_or_else(|| {
637 BoxError::PoolError(format!("snapshot socket {} never appeared", sock.display()))
638 })?;
639 let cmd = format!("snapshot {}\n", state_file.display());
640 stream
641 .write_all(cmd.as_bytes())
642 .await
643 .map_err(BoxError::IoError)?;
644 let mut buf = [0u8; 64];
645 let n = stream.read(&mut buf).await.map_err(BoxError::IoError)?;
646 let reply = String::from_utf8_lossy(&buf[..n]);
647 if reply.trim() == "ok" {
648 Ok(())
649 } else {
650 Err(BoxError::PoolError(format!(
651 "snapshot trigger failed: {}",
652 reply.trim()
653 )))
654 }
655 }
656
657 #[cfg(not(unix))]
661 async fn trigger_snapshot(
662 _sock: &std::path::Path,
663 _state_file: &std::path::Path,
664 ) -> Result<()> {
665 Err(BoxError::PoolError(
666 "snapshot-fork is only supported on Linux/KVM hosts".to_string(),
667 ))
668 }
669
670 async fn fill_to_min(&self) {
672 let current = self.idle.lock().await.len();
673 let needed = self.config.min_idle.saturating_sub(current);
674
675 if needed == 0 {
676 return;
677 }
678
679 tracing::debug!(
680 current,
681 needed,
682 min_idle = self.config.min_idle,
683 "Replenishing warm pool"
684 );
685
686 let mut added_ids: Vec<String> = Vec::new();
688
689 for _ in 0..needed {
690 match self.boot_new_vm().await {
691 Ok(vm) => {
692 let box_id = vm.box_id().to_string();
693 let mut idle = self.idle.lock().await;
694 idle.push(WarmVm {
695 vm,
696 created_at: Instant::now(),
697 });
698 let mut stats = self.stats.lock().await;
699 stats.idle_count = idle.len();
700 added_ids.push(box_id.clone());
701
702 tracing::debug!(box_id = %box_id, "Added VM to warm pool");
703 }
704 Err(e) => {
705 tracing::warn!(error = %e, "Failed to boot VM for warm pool");
706 if !added_ids.is_empty() {
708 tracing::info!(
709 count = added_ids.len(),
710 "Cleaning up VMs added before fill_to_min failed"
711 );
712 self.remove_idle_vms(&added_ids).await;
713 }
714 break;
715 }
716 }
717 }
718
719 self.event_emitter.emit(BoxEvent::empty("pool.replenish"));
720 }
721
722 fn spawn_maintenance_loop(&self) -> JoinHandle<()> {
729 let idle = Arc::clone(&self.idle);
730 let stats = Arc::clone(&self.stats);
731 let config = self.config.clone();
732 let box_config = self.box_config.clone();
733 let event_emitter = self.event_emitter.clone();
734 let mut shutdown_rx = self.shutdown_rx.clone();
735 let scaler = self.scaler.clone();
736 let template = Arc::clone(&self.template);
737
738 tokio::spawn(async move {
739 let check_interval = std::time::Duration::from_secs(
740 if config.idle_ttl_secs > 0 {
742 (config.idle_ttl_secs / 5).max(5)
743 } else {
744 30
745 },
746 );
747
748 let mut effective_min_idle = config.min_idle;
750
751 loop {
752 tokio::select! {
753 result = shutdown_rx.changed() => {
754 if result.is_ok() && *shutdown_rx.borrow() {
755 tracing::debug!("Pool maintenance loop shutting down");
756 break;
757 }
758 }
759 _ = tokio::time::sleep(check_interval) => {
760 if config.idle_ttl_secs > 0 {
762 Self::evict_expired_static(
763 &idle,
764 &stats,
765 &event_emitter,
766 config.idle_ttl_secs,
767 ).await;
768 }
769
770 if let Some(ref scaler) = scaler {
772 let mut s = scaler.lock().await;
773 let decision = s.evaluate();
774 let new_min = s.current_min_idle();
775 if new_min != effective_min_idle {
776 tracing::info!(
777 old_min_idle = effective_min_idle,
778 new_min_idle = new_min,
779 ?decision,
780 "Autoscaler adjusted min_idle"
781 );
782 event_emitter.emit(BoxEvent::with_string(
783 "pool.autoscale",
784 format!(
785 "min_idle adjusted {} → {} ({:?})",
786 effective_min_idle, new_min, decision
787 ),
788 ));
789 effective_min_idle = new_min;
790 }
791 }
792
793 let current = idle.lock().await.len();
795 if current < effective_min_idle {
796 let needed = effective_min_idle - current;
797 tracing::debug!(current, needed, min_idle = effective_min_idle, "Replenishing warm pool");
798
799 let mut set = tokio::task::JoinSet::new();
806 for _ in 0..needed {
807 let sf = config.snapshot_fork;
808 let bc = box_config.clone();
809 let ee = event_emitter.clone();
810 let tpl = Arc::clone(&template);
811 set.spawn(async move {
812 WarmPool::boot_or_restore(sf, &bc, &ee, &tpl).await
813 });
814 }
815 while let Some(joined) = set.join_next().await {
816 match joined {
817 Ok(Ok(mut vm)) => {
818 let box_id = vm.box_id().to_string();
819 let mut pool = idle.lock().await;
830 if *shutdown_rx.borrow() {
831 drop(pool);
832 tracing::debug!(
833 box_id = %box_id,
834 "Pool shutting down mid-replenish; destroying freshly-booted VM"
835 );
836 let _ = vm.destroy_with_timeout(2000).await;
837 continue;
838 }
839 pool.push(WarmVm {
840 vm,
841 created_at: Instant::now(),
842 });
843 let mut s = stats.lock().await;
844 s.total_created += 1;
845 s.idle_count = pool.len();
846 drop(s);
847 drop(pool);
848
849 event_emitter.emit(BoxEvent::with_string(
850 "pool.vm.created",
851 format!("Replenished VM {}", box_id),
852 ));
853 }
854 Ok(Err(e)) => {
855 tracing::warn!(error = %e, "Failed to replenish warm pool");
856 }
857 Err(e) => {
858 tracing::warn!(error = %e, "Replenish task join error");
859 }
860 }
861 }
862
863 event_emitter.emit(BoxEvent::empty("pool.replenish"));
864 }
865 }
866 }
867 }
868 })
869 }
870
871 async fn evict_expired_static(
873 idle: &Arc<Mutex<Vec<WarmVm>>>,
874 stats: &Arc<Mutex<PoolStats>>,
875 event_emitter: &EventEmitter,
876 idle_ttl_secs: u64,
877 ) {
878 let ttl = std::time::Duration::from_secs(idle_ttl_secs);
879
880 let mut pool = idle.lock().await;
881 let mut kept = Vec::new();
882 let mut expired = Vec::new();
883
884 for warm_vm in pool.drain(..) {
885 if warm_vm.created_at.elapsed() > ttl {
886 expired.push(warm_vm);
887 } else {
888 kept.push(warm_vm);
889 }
890 }
891 *pool = kept;
892 let after_count = pool.len();
893 drop(pool);
894
895 let evicted_count = expired.len();
896 for warm_vm in expired {
897 let mut vm = warm_vm.vm;
898 let _ = vm.destroy().await;
899 }
900
901 if evicted_count > 0 {
902 let mut s = stats.lock().await;
903 s.total_evicted += evicted_count as u64;
904 s.idle_count = after_count;
905
906 event_emitter.emit(BoxEvent::with_string(
907 "pool.vm.evicted",
908 format!("Evicted {} expired VMs", evicted_count),
909 ));
910 }
911 }
912}
913
914#[cfg(test)]
915mod tests {
916 use super::*;
917 use a3s_box_core::config::PoolConfig;
918
919 fn test_pool_config(min_idle: usize, max_size: usize) -> PoolConfig {
920 PoolConfig {
921 enabled: true,
922 min_idle,
923 max_size,
924 idle_ttl_secs: 300,
925 ..Default::default()
926 }
927 }
928
929 fn test_event_emitter() -> EventEmitter {
930 EventEmitter::new(100)
931 }
932
933 #[tokio::test]
936 async fn test_pool_rejects_zero_max_size() {
937 let config = test_pool_config(0, 0);
938 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
939 match result {
940 Err(e) => assert!(e.to_string().contains("max_size must be greater than 0")),
941 Ok(_) => panic!("Expected error for zero max_size"),
942 }
943 }
944
945 #[tokio::test]
946 async fn test_pool_rejects_min_idle_exceeds_max() {
947 let config = test_pool_config(10, 5);
948 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
949 match result {
950 Err(e) => assert!(e.to_string().contains("cannot exceed max_size")),
951 Ok(_) => panic!("Expected error for min_idle > max_size"),
952 }
953 }
954
955 #[test]
958 fn test_pool_stats_default() {
959 let stats = PoolStats {
960 idle_count: 0,
961 total_created: 0,
962 total_acquired: 0,
963 total_released: 0,
964 total_evicted: 0,
965 };
966 assert_eq!(stats.idle_count, 0);
967 assert_eq!(stats.total_created, 0);
968 }
969
970 #[test]
971 fn test_pool_stats_clone() {
972 let stats = PoolStats {
973 idle_count: 3,
974 total_created: 10,
975 total_acquired: 7,
976 total_released: 5,
977 total_evicted: 2,
978 };
979 let cloned = stats.clone();
980 assert_eq!(cloned.idle_count, 3);
981 assert_eq!(cloned.total_created, 10);
982 assert_eq!(cloned.total_acquired, 7);
983 assert_eq!(cloned.total_released, 5);
984 assert_eq!(cloned.total_evicted, 2);
985 }
986
987 #[test]
988 fn test_pool_stats_debug() {
989 let stats = PoolStats {
990 idle_count: 1,
991 total_created: 2,
992 total_acquired: 3,
993 total_released: 4,
994 total_evicted: 5,
995 };
996 let debug = format!("{:?}", stats);
997 assert!(debug.contains("idle_count"));
998 assert!(debug.contains("total_created"));
999 }
1000
1001 #[test]
1004 fn test_pool_config_roundtrip() {
1005 let config = PoolConfig {
1006 enabled: true,
1007 min_idle: 3,
1008 max_size: 10,
1009 idle_ttl_secs: 600,
1010 ..Default::default()
1011 };
1012
1013 let json = serde_json::to_string(&config).unwrap();
1014 let parsed: PoolConfig = serde_json::from_str(&json).unwrap();
1015
1016 assert!(parsed.enabled);
1017 assert_eq!(parsed.min_idle, 3);
1018 assert_eq!(parsed.max_size, 10);
1019 assert_eq!(parsed.idle_ttl_secs, 600);
1020 }
1021
1022 #[test]
1023 fn test_pool_config_default_values() {
1024 let config = PoolConfig::default();
1025 assert!(!config.enabled);
1026 assert_eq!(config.min_idle, 1);
1027 assert_eq!(config.max_size, 5);
1028 assert_eq!(config.idle_ttl_secs, 300);
1029 }
1030
1031 #[test]
1032 fn test_pool_config_deserialization_with_defaults() {
1033 let json = r#"{"enabled": true}"#;
1034 let config: PoolConfig = serde_json::from_str(json).unwrap();
1035 assert!(config.enabled);
1036 assert_eq!(config.min_idle, 1);
1037 assert_eq!(config.max_size, 5);
1038 assert_eq!(config.idle_ttl_secs, 300);
1039 }
1040
1041 #[tokio::test]
1044 async fn test_pool_accepts_min_idle_equals_max() {
1045 let config = test_pool_config(3, 3);
1046 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
1049 match result {
1051 Err(e) => assert!(!e.to_string().contains("cannot exceed max_size")),
1052 Ok(mut pool) => {
1053 let _ = pool.drain().await;
1054 }
1055 }
1056 }
1057
1058 #[tokio::test]
1059 async fn test_pool_accepts_min_idle_zero() {
1060 let config = test_pool_config(0, 5);
1061 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
1063 match result {
1064 Ok(mut pool) => {
1065 assert_eq!(pool.idle_count().await, 0);
1067 let stats = pool.stats().await;
1068 assert_eq!(stats.idle_count, 0);
1069 assert_eq!(stats.total_created, 0);
1070 let _ = pool.drain().await;
1071 }
1072 Err(e) => {
1073 assert!(!e.to_string().contains("max_size"));
1075 assert!(!e.to_string().contains("min_idle"));
1076 }
1077 }
1078 }
1079
1080 #[tokio::test]
1083 async fn test_pool_stats_initial() {
1084 let config = test_pool_config(0, 5);
1085 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
1086 if let Ok(mut pool) = result {
1087 let stats = pool.stats().await;
1088 assert_eq!(stats.idle_count, 0);
1089 assert_eq!(stats.total_created, 0);
1090 assert_eq!(stats.total_acquired, 0);
1091 assert_eq!(stats.total_released, 0);
1092 assert_eq!(stats.total_evicted, 0);
1093 let _ = pool.drain().await;
1094 }
1095 }
1096
1097 #[tokio::test]
1098 async fn test_pool_idle_count_initial() {
1099 let config = test_pool_config(0, 5);
1100 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
1101 if let Ok(mut pool) = result {
1102 assert_eq!(pool.idle_count().await, 0);
1103 let _ = pool.drain().await;
1104 }
1105 }
1106
1107 #[tokio::test]
1108 async fn test_pool_drain_empty_pool() {
1109 let config = test_pool_config(0, 5);
1110 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
1111 if let Ok(mut pool) = result {
1112 let drain_result = pool.drain().await;
1114 assert!(drain_result.is_ok());
1115
1116 let stats = pool.stats().await;
1117 assert_eq!(stats.idle_count, 0);
1118 }
1119 }
1120
1121 #[tokio::test]
1122 async fn test_pool_drain_emits_event() {
1123 let emitter = test_event_emitter();
1124 let mut receiver = emitter.subscribe();
1125 let config = test_pool_config(0, 5);
1126
1127 let result = WarmPool::start(config, BoxConfig::default(), emitter).await;
1128 if let Ok(mut pool) = result {
1129 pool.drain().await.unwrap();
1130
1131 let mut found_drain_event = false;
1133 while let Ok(event) = receiver.try_recv() {
1135 if event.key == "pool.drained" {
1136 found_drain_event = true;
1137 }
1138 }
1139 assert!(found_drain_event, "Expected pool.drained event");
1140 }
1141 }
1142
1143 #[tokio::test]
1144 async fn test_pool_acquire_from_empty_pool_fails_without_shim() {
1145 let config = test_pool_config(0, 5);
1146 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
1147 if let Ok(pool) = result {
1148 let acquire_result = pool.acquire().await;
1151 assert!(acquire_result.is_err());
1152 }
1153 }
1154
1155 #[test]
1158 #[allow(clippy::unnecessary_min_or_max)]
1159 fn test_maintenance_check_interval_with_ttl() {
1160 let interval = if 300_u64 > 0 {
1162 (300_u64 / 5).max(5)
1163 } else {
1164 30
1165 };
1166 assert_eq!(interval, 60);
1167 }
1168
1169 #[test]
1170 #[allow(clippy::unnecessary_min_or_max)]
1171 fn test_maintenance_check_interval_short_ttl() {
1172 let interval = if 10_u64 > 0 { (10_u64 / 5).max(5) } else { 30 };
1174 assert_eq!(interval, 5);
1175 }
1176
1177 #[test]
1178 #[allow(clippy::unnecessary_min_or_max)]
1179 fn test_maintenance_check_interval_very_short_ttl() {
1180 let interval = if 1_u64 > 0 { (1_u64 / 5).max(5) } else { 30 };
1182 assert_eq!(interval, 5);
1183 }
1184
1185 #[test]
1186 #[allow(
1187 clippy::absurd_extreme_comparisons,
1188 clippy::erasing_op,
1189 clippy::unnecessary_min_or_max,
1190 unused_comparisons
1191 )]
1192 fn test_maintenance_check_interval_no_ttl() {
1193 let interval = if 0_u64 > 0 { (0_u64 / 5).max(5) } else { 30 };
1195 assert_eq!(interval, 30);
1196 }
1197
1198 #[test]
1201 fn test_warm_vm_created_at_is_recent() {
1202 let before = Instant::now();
1203 let created_at = Instant::now();
1204 let after = Instant::now();
1205
1206 assert!(created_at >= before);
1207 assert!(created_at <= after);
1208 }
1209
1210 #[test]
1213 fn test_pool_stats_all_fields() {
1214 let stats = PoolStats {
1215 idle_count: 10,
1216 total_created: 100,
1217 total_acquired: 80,
1218 total_released: 70,
1219 total_evicted: 15,
1220 };
1221
1222 assert_eq!(stats.idle_count, 10);
1223 assert_eq!(stats.total_created, 100);
1224 assert_eq!(stats.total_acquired, 80);
1225 assert_eq!(stats.total_released, 70);
1226 assert_eq!(stats.total_evicted, 15);
1227
1228 let debug = format!("{:?}", stats);
1230 assert!(debug.contains("10"));
1231 assert!(debug.contains("100"));
1232 assert!(debug.contains("80"));
1233 assert!(debug.contains("70"));
1234 assert!(debug.contains("15"));
1235 }
1236
1237 #[tokio::test]
1244 async fn test_pool_set_metrics_attaches() {
1245 let config = test_pool_config(0, 5);
1246 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
1247 match result {
1248 Ok(mut pool) => {
1249 let metrics = crate::prom::RuntimeMetrics::new();
1250 pool.set_metrics(metrics.clone());
1251 assert!(pool.metrics.is_some());
1252 assert_eq!(metrics.warm_pool_hits.get(), 0);
1254 assert_eq!(metrics.warm_pool_misses.get(), 0);
1255 assert_eq!(metrics.warm_pool_size.get(), 0);
1256 let _ = pool.drain().await;
1257 }
1258 Err(_) => {
1259 }
1261 }
1262 }
1263}