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 idle.len() >= self.config.max_size {
263 drop(idle); let mut vm = vm;
266 vm.destroy().await?;
267
268 tracing::debug!(
269 box_id = %vm.box_id(),
270 "Pool full, destroyed released VM"
271 );
272 return Ok(());
273 }
274
275 let box_id = vm.box_id().to_string();
276 idle.push(WarmVm {
277 vm,
278 created_at: Instant::now(),
279 });
280
281 let mut stats = self.stats.lock().await;
282 stats.total_released += 1;
283 stats.idle_count = idle.len();
284
285 if let Some(ref m) = self.metrics {
286 m.warm_pool_size.set(idle.len() as i64);
287 }
288
289 self.event_emitter.emit(BoxEvent::with_string(
290 "pool.vm.released",
291 format!("Released VM {} back to pool", box_id),
292 ));
293
294 tracing::debug!(
295 box_id = %box_id,
296 idle_count = idle.len(),
297 "Released VM back to warm pool"
298 );
299
300 Ok(())
301 }
302
303 pub async fn stats(&self) -> PoolStats {
305 self.stats.lock().await.clone()
306 }
307
308 pub async fn idle_count(&self) -> usize {
310 self.idle.lock().await.len()
311 }
312
313 pub fn signal_shutdown(&self) {
317 let _ = self.shutdown_tx.send(true);
318 tracing::info!("Warm pool shutdown signaled");
319 }
320
321 pub async fn drain(&mut self) -> Result<()> {
323 let _ = self.shutdown_tx.send(true);
325
326 if let Some(handle) = self.replenish_handle.take() {
328 let _ = handle.await;
329 }
330
331 let mut idle = self.idle.lock().await;
333 let count = idle.len();
334
335 for warm_vm in idle.drain(..) {
336 let mut vm = warm_vm.vm;
337 if let Err(e) = vm.destroy().await {
338 tracing::warn!(
339 box_id = %vm.box_id(),
340 error = %e,
341 "Failed to destroy pooled VM during drain"
342 );
343 }
344 }
345
346 let mut stats = self.stats.lock().await;
347 stats.idle_count = 0;
348
349 self.event_emitter.emit(BoxEvent::empty("pool.drained"));
350
351 tracing::info!(destroyed = count, "Warm pool drained");
352
353 Ok(())
354 }
355
356 pub async fn drain_idle(&self) -> Result<()> {
361 let mut idle = self.idle.lock().await;
362 let count = idle.len();
363 for warm_vm in idle.drain(..) {
364 let mut vm = warm_vm.vm;
365 if let Err(e) = vm.destroy().await {
366 tracing::warn!(
367 box_id = %vm.box_id(),
368 error = %e,
369 "Failed to destroy pooled VM during drain_idle"
370 );
371 }
372 }
373 self.stats.lock().await.idle_count = 0;
374 tracing::info!(destroyed = count, "Warm pool idle VMs drained");
375 Ok(())
376 }
377
378 async fn remove_idle_vms(&self, box_ids: &[String]) {
383 let indices_to_remove: Vec<usize> = {
385 let idle = self.idle.lock().await;
386 idle.iter()
387 .enumerate()
388 .filter(|(_, wm)| box_ids.iter().any(|id| id == wm.vm.box_id()))
389 .map(|(i, _)| i)
390 .collect()
391 };
392
393 if indices_to_remove.is_empty() {
394 return;
395 }
396
397 let mut to_destroy: Vec<WarmVm> = Vec::new();
400 {
401 let mut idle = self.idle.lock().await;
402 for idx in indices_to_remove.into_iter().rev() {
403 if idx < idle.len() {
404 let warm_vm = idle.remove(idx);
405 to_destroy.push(warm_vm);
406 }
407 }
408 }
409
410 {
412 let idle_count = self.idle.lock().await.len();
413 if let Ok(mut stats) = self.stats.try_lock() {
414 stats.idle_count = idle_count;
415 }
416 }
417
418 for warm_vm in to_destroy {
420 let box_id = warm_vm.vm.box_id().to_string();
421 let mut vm = warm_vm.vm;
422 if let Err(e) = vm.destroy().await {
423 tracing::warn!(
424 box_id = %box_id,
425 error = %e,
426 "Failed to destroy VM during fill_to_min rollback"
427 );
428 } else {
429 tracing::debug!(box_id = %box_id, "Destroyed VM during fill_to_min rollback");
430 }
431 }
432 }
433
434 async fn boot_new_vm(&self) -> Result<VmManager> {
436 let vm = Self::boot_or_restore(
437 self.config.snapshot_fork,
438 &self.box_config,
439 &self.event_emitter,
440 &self.template,
441 )
442 .await?;
443
444 let mut stats = self.stats.lock().await;
445 stats.total_created += 1;
446
447 self.event_emitter.emit(BoxEvent::with_string(
448 "pool.vm.created",
449 format!("Booted new VM {}", vm.box_id()),
450 ));
451
452 Ok(vm)
453 }
454
455 async fn boot_or_restore(
458 snapshot_fork: bool,
459 box_config: &BoxConfig,
460 event_emitter: &EventEmitter,
461 template: &Arc<Mutex<TemplateState>>,
462 ) -> Result<VmManager> {
463 if snapshot_fork {
464 match Self::ensure_template(box_config, event_emitter, template).await {
469 Ok(tpl) => {
470 let mut cfg = box_config.clone();
471 cfg.snapshot_mem_file = Some(tpl.mem_file.clone());
472 cfg.restore_from = Some(tpl.state_file.clone());
473 cfg.snapshot_sock = None;
474 let mut vm = VmManager::new(cfg, event_emitter.clone());
475 vm.boot().await?;
476 return Ok(vm);
477 }
478 Err(error) => {
479 tracing::debug!(%error, "snapshot-fork unavailable; cold-booting this pool VM");
480 }
481 }
482 }
483 let mut vm = VmManager::new(box_config.clone(), event_emitter.clone());
484 vm.boot().await?;
485 Ok(vm)
486 }
487
488 async fn ensure_template(
495 box_config: &BoxConfig,
496 event_emitter: &EventEmitter,
497 template: &Arc<Mutex<TemplateState>>,
498 ) -> Result<PoolTemplate> {
499 let mut guard = template.lock().await;
500 let prior_failures = match &*guard {
501 TemplateState::Ready(t) => return Ok(t.clone()),
502 TemplateState::Unavailable => {
503 return Err(BoxError::PoolError(
504 "snapshot-fork template unavailable (native VM snapshot unsupported)"
505 .to_string(),
506 ));
507 }
508 TemplateState::Failing(n) => *n,
510 TemplateState::Unbuilt => 0,
511 };
512
513 match Self::build_template(box_config, event_emitter).await {
514 Ok(tpl) => {
515 *guard = TemplateState::Ready(tpl.clone());
516 event_emitter.emit(BoxEvent::with_string(
517 "pool.template.built",
518 format!(
519 "Snapshot-fork template built for image {}",
520 box_config.image
521 ),
522 ));
523 Ok(tpl)
524 }
525 Err(error) => {
526 let failures = prior_failures + 1;
531 if failures >= MAX_TEMPLATE_BUILD_FAILURES {
532 tracing::warn!(
533 %error, failures,
534 "snapshot-fork template build failed repeatedly; marking \
535 unavailable — the warm pool will cold-boot"
536 );
537 *guard = TemplateState::Unavailable;
538 } else {
539 tracing::warn!(
540 %error, failures,
541 "snapshot-fork template build failed; will retry on a later fill"
542 );
543 *guard = TemplateState::Failing(failures);
544 }
545 Err(error)
546 }
547 }
548 }
549
550 async fn build_template(
553 box_config: &BoxConfig,
554 event_emitter: &EventEmitter,
555 ) -> Result<PoolTemplate> {
556 let dir = a3s_box_core::dirs_home().join("pool").join(format!(
557 "tpl-{:016x}",
558 crate::vm::fnv1a_hash(&box_config.image)
559 ));
560 std::fs::create_dir_all(&dir).map_err(BoxError::IoError)?;
561
562 let lock_target = dir.clone();
569 let _lock =
570 tokio::task::spawn_blocking(move || crate::file_lock::FileLock::acquire(&lock_target))
571 .await
572 .map_err(|e| BoxError::PoolError(format!("Template lock task failed: {e}")))?
573 .map_err(|e| BoxError::PoolError(format!("Failed to lock template dir: {e}")))?;
574
575 let mem_file = dir.join("template.ram");
576 let sock = dir.join("template.sock");
577 let state_file = dir.join("template.state");
578 let _ = std::fs::remove_file(&sock);
579
580 let mut cfg = box_config.clone();
582 cfg.snapshot_mem_file = Some(mem_file.to_string_lossy().into_owned());
583 cfg.snapshot_sock = Some(sock.to_string_lossy().into_owned());
584 cfg.restore_from = None;
585 let mut src = VmManager::new(cfg, event_emitter.clone());
586 src.boot().await?;
587
588 let snapshot = Self::trigger_snapshot(&sock, &state_file).await;
597 let _ = src.destroy_with_timeout(2000).await;
598 snapshot?;
599
600 Ok(PoolTemplate {
601 mem_file: mem_file.to_string_lossy().into_owned(),
602 state_file: state_file.to_string_lossy().into_owned(),
603 })
604 }
605
606 #[cfg(unix)]
612 async fn trigger_snapshot(sock: &std::path::Path, state_file: &std::path::Path) -> Result<()> {
613 use tokio::io::{AsyncReadExt, AsyncWriteExt};
614 let mut stream = None;
616 for _ in 0..200 {
617 match tokio::net::UnixStream::connect(sock).await {
618 Ok(s) => {
619 stream = Some(s);
620 break;
621 }
622 Err(_) => tokio::time::sleep(std::time::Duration::from_millis(25)).await,
623 }
624 }
625 let mut stream = stream.ok_or_else(|| {
626 BoxError::PoolError(format!("snapshot socket {} never appeared", sock.display()))
627 })?;
628 let cmd = format!("snapshot {}\n", state_file.display());
629 stream
630 .write_all(cmd.as_bytes())
631 .await
632 .map_err(BoxError::IoError)?;
633 let mut buf = [0u8; 64];
634 let n = stream.read(&mut buf).await.map_err(BoxError::IoError)?;
635 let reply = String::from_utf8_lossy(&buf[..n]);
636 if reply.trim() == "ok" {
637 Ok(())
638 } else {
639 Err(BoxError::PoolError(format!(
640 "snapshot trigger failed: {}",
641 reply.trim()
642 )))
643 }
644 }
645
646 #[cfg(not(unix))]
650 async fn trigger_snapshot(
651 _sock: &std::path::Path,
652 _state_file: &std::path::Path,
653 ) -> Result<()> {
654 Err(BoxError::PoolError(
655 "snapshot-fork is only supported on Linux/KVM hosts".to_string(),
656 ))
657 }
658
659 async fn fill_to_min(&self) {
661 let current = self.idle.lock().await.len();
662 let needed = self.config.min_idle.saturating_sub(current);
663
664 if needed == 0 {
665 return;
666 }
667
668 tracing::debug!(
669 current,
670 needed,
671 min_idle = self.config.min_idle,
672 "Replenishing warm pool"
673 );
674
675 let mut added_ids: Vec<String> = Vec::new();
677
678 for _ in 0..needed {
679 match self.boot_new_vm().await {
680 Ok(vm) => {
681 let box_id = vm.box_id().to_string();
682 let mut idle = self.idle.lock().await;
683 idle.push(WarmVm {
684 vm,
685 created_at: Instant::now(),
686 });
687 let mut stats = self.stats.lock().await;
688 stats.idle_count = idle.len();
689 added_ids.push(box_id.clone());
690
691 tracing::debug!(box_id = %box_id, "Added VM to warm pool");
692 }
693 Err(e) => {
694 tracing::warn!(error = %e, "Failed to boot VM for warm pool");
695 if !added_ids.is_empty() {
697 tracing::info!(
698 count = added_ids.len(),
699 "Cleaning up VMs added before fill_to_min failed"
700 );
701 self.remove_idle_vms(&added_ids).await;
702 }
703 break;
704 }
705 }
706 }
707
708 self.event_emitter.emit(BoxEvent::empty("pool.replenish"));
709 }
710
711 fn spawn_maintenance_loop(&self) -> JoinHandle<()> {
718 let idle = Arc::clone(&self.idle);
719 let stats = Arc::clone(&self.stats);
720 let config = self.config.clone();
721 let box_config = self.box_config.clone();
722 let event_emitter = self.event_emitter.clone();
723 let mut shutdown_rx = self.shutdown_rx.clone();
724 let scaler = self.scaler.clone();
725 let template = Arc::clone(&self.template);
726
727 tokio::spawn(async move {
728 let check_interval = std::time::Duration::from_secs(
729 if config.idle_ttl_secs > 0 {
731 (config.idle_ttl_secs / 5).max(5)
732 } else {
733 30
734 },
735 );
736
737 let mut effective_min_idle = config.min_idle;
739
740 loop {
741 tokio::select! {
742 result = shutdown_rx.changed() => {
743 if result.is_ok() && *shutdown_rx.borrow() {
744 tracing::debug!("Pool maintenance loop shutting down");
745 break;
746 }
747 }
748 _ = tokio::time::sleep(check_interval) => {
749 if config.idle_ttl_secs > 0 {
751 Self::evict_expired_static(
752 &idle,
753 &stats,
754 &event_emitter,
755 config.idle_ttl_secs,
756 ).await;
757 }
758
759 if let Some(ref scaler) = scaler {
761 let mut s = scaler.lock().await;
762 let decision = s.evaluate();
763 let new_min = s.current_min_idle();
764 if new_min != effective_min_idle {
765 tracing::info!(
766 old_min_idle = effective_min_idle,
767 new_min_idle = new_min,
768 ?decision,
769 "Autoscaler adjusted min_idle"
770 );
771 event_emitter.emit(BoxEvent::with_string(
772 "pool.autoscale",
773 format!(
774 "min_idle adjusted {} → {} ({:?})",
775 effective_min_idle, new_min, decision
776 ),
777 ));
778 effective_min_idle = new_min;
779 }
780 }
781
782 let current = idle.lock().await.len();
784 if current < effective_min_idle {
785 let needed = effective_min_idle - current;
786 tracing::debug!(current, needed, min_idle = effective_min_idle, "Replenishing warm pool");
787
788 let mut set = tokio::task::JoinSet::new();
795 for _ in 0..needed {
796 let sf = config.snapshot_fork;
797 let bc = box_config.clone();
798 let ee = event_emitter.clone();
799 let tpl = Arc::clone(&template);
800 set.spawn(async move {
801 WarmPool::boot_or_restore(sf, &bc, &ee, &tpl).await
802 });
803 }
804 while let Some(joined) = set.join_next().await {
805 match joined {
806 Ok(Ok(mut vm)) => {
807 let box_id = vm.box_id().to_string();
808 if *shutdown_rx.borrow() {
814 tracing::debug!(
815 box_id = %box_id,
816 "Pool shutting down mid-replenish; destroying freshly-booted VM"
817 );
818 let _ = vm.destroy_with_timeout(2000).await;
819 continue;
820 }
821 let mut pool = idle.lock().await;
822 pool.push(WarmVm {
823 vm,
824 created_at: Instant::now(),
825 });
826 let mut s = stats.lock().await;
827 s.total_created += 1;
828 s.idle_count = pool.len();
829 drop(s);
830 drop(pool);
831
832 event_emitter.emit(BoxEvent::with_string(
833 "pool.vm.created",
834 format!("Replenished VM {}", box_id),
835 ));
836 }
837 Ok(Err(e)) => {
838 tracing::warn!(error = %e, "Failed to replenish warm pool");
839 }
840 Err(e) => {
841 tracing::warn!(error = %e, "Replenish task join error");
842 }
843 }
844 }
845
846 event_emitter.emit(BoxEvent::empty("pool.replenish"));
847 }
848 }
849 }
850 }
851 })
852 }
853
854 async fn evict_expired_static(
856 idle: &Arc<Mutex<Vec<WarmVm>>>,
857 stats: &Arc<Mutex<PoolStats>>,
858 event_emitter: &EventEmitter,
859 idle_ttl_secs: u64,
860 ) {
861 let ttl = std::time::Duration::from_secs(idle_ttl_secs);
862
863 let mut pool = idle.lock().await;
864 let mut kept = Vec::new();
865 let mut expired = Vec::new();
866
867 for warm_vm in pool.drain(..) {
868 if warm_vm.created_at.elapsed() > ttl {
869 expired.push(warm_vm);
870 } else {
871 kept.push(warm_vm);
872 }
873 }
874 *pool = kept;
875 let after_count = pool.len();
876 drop(pool);
877
878 let evicted_count = expired.len();
879 for warm_vm in expired {
880 let mut vm = warm_vm.vm;
881 let _ = vm.destroy().await;
882 }
883
884 if evicted_count > 0 {
885 let mut s = stats.lock().await;
886 s.total_evicted += evicted_count as u64;
887 s.idle_count = after_count;
888
889 event_emitter.emit(BoxEvent::with_string(
890 "pool.vm.evicted",
891 format!("Evicted {} expired VMs", evicted_count),
892 ));
893 }
894 }
895}
896
897#[cfg(test)]
898mod tests {
899 use super::*;
900 use a3s_box_core::config::PoolConfig;
901
902 fn test_pool_config(min_idle: usize, max_size: usize) -> PoolConfig {
903 PoolConfig {
904 enabled: true,
905 min_idle,
906 max_size,
907 idle_ttl_secs: 300,
908 ..Default::default()
909 }
910 }
911
912 fn test_event_emitter() -> EventEmitter {
913 EventEmitter::new(100)
914 }
915
916 #[tokio::test]
919 async fn test_pool_rejects_zero_max_size() {
920 let config = test_pool_config(0, 0);
921 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
922 match result {
923 Err(e) => assert!(e.to_string().contains("max_size must be greater than 0")),
924 Ok(_) => panic!("Expected error for zero max_size"),
925 }
926 }
927
928 #[tokio::test]
929 async fn test_pool_rejects_min_idle_exceeds_max() {
930 let config = test_pool_config(10, 5);
931 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
932 match result {
933 Err(e) => assert!(e.to_string().contains("cannot exceed max_size")),
934 Ok(_) => panic!("Expected error for min_idle > max_size"),
935 }
936 }
937
938 #[test]
941 fn test_pool_stats_default() {
942 let stats = PoolStats {
943 idle_count: 0,
944 total_created: 0,
945 total_acquired: 0,
946 total_released: 0,
947 total_evicted: 0,
948 };
949 assert_eq!(stats.idle_count, 0);
950 assert_eq!(stats.total_created, 0);
951 }
952
953 #[test]
954 fn test_pool_stats_clone() {
955 let stats = PoolStats {
956 idle_count: 3,
957 total_created: 10,
958 total_acquired: 7,
959 total_released: 5,
960 total_evicted: 2,
961 };
962 let cloned = stats.clone();
963 assert_eq!(cloned.idle_count, 3);
964 assert_eq!(cloned.total_created, 10);
965 assert_eq!(cloned.total_acquired, 7);
966 assert_eq!(cloned.total_released, 5);
967 assert_eq!(cloned.total_evicted, 2);
968 }
969
970 #[test]
971 fn test_pool_stats_debug() {
972 let stats = PoolStats {
973 idle_count: 1,
974 total_created: 2,
975 total_acquired: 3,
976 total_released: 4,
977 total_evicted: 5,
978 };
979 let debug = format!("{:?}", stats);
980 assert!(debug.contains("idle_count"));
981 assert!(debug.contains("total_created"));
982 }
983
984 #[test]
987 fn test_pool_config_roundtrip() {
988 let config = PoolConfig {
989 enabled: true,
990 min_idle: 3,
991 max_size: 10,
992 idle_ttl_secs: 600,
993 ..Default::default()
994 };
995
996 let json = serde_json::to_string(&config).unwrap();
997 let parsed: PoolConfig = serde_json::from_str(&json).unwrap();
998
999 assert!(parsed.enabled);
1000 assert_eq!(parsed.min_idle, 3);
1001 assert_eq!(parsed.max_size, 10);
1002 assert_eq!(parsed.idle_ttl_secs, 600);
1003 }
1004
1005 #[test]
1006 fn test_pool_config_default_values() {
1007 let config = PoolConfig::default();
1008 assert!(!config.enabled);
1009 assert_eq!(config.min_idle, 1);
1010 assert_eq!(config.max_size, 5);
1011 assert_eq!(config.idle_ttl_secs, 300);
1012 }
1013
1014 #[test]
1015 fn test_pool_config_deserialization_with_defaults() {
1016 let json = r#"{"enabled": true}"#;
1017 let config: PoolConfig = serde_json::from_str(json).unwrap();
1018 assert!(config.enabled);
1019 assert_eq!(config.min_idle, 1);
1020 assert_eq!(config.max_size, 5);
1021 assert_eq!(config.idle_ttl_secs, 300);
1022 }
1023
1024 #[tokio::test]
1027 async fn test_pool_accepts_min_idle_equals_max() {
1028 let config = test_pool_config(3, 3);
1029 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
1032 match result {
1034 Err(e) => assert!(!e.to_string().contains("cannot exceed max_size")),
1035 Ok(mut pool) => {
1036 let _ = pool.drain().await;
1037 }
1038 }
1039 }
1040
1041 #[tokio::test]
1042 async fn test_pool_accepts_min_idle_zero() {
1043 let config = test_pool_config(0, 5);
1044 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
1046 match result {
1047 Ok(mut pool) => {
1048 assert_eq!(pool.idle_count().await, 0);
1050 let stats = pool.stats().await;
1051 assert_eq!(stats.idle_count, 0);
1052 assert_eq!(stats.total_created, 0);
1053 let _ = pool.drain().await;
1054 }
1055 Err(e) => {
1056 assert!(!e.to_string().contains("max_size"));
1058 assert!(!e.to_string().contains("min_idle"));
1059 }
1060 }
1061 }
1062
1063 #[tokio::test]
1066 async fn test_pool_stats_initial() {
1067 let config = test_pool_config(0, 5);
1068 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
1069 if let Ok(mut pool) = result {
1070 let stats = pool.stats().await;
1071 assert_eq!(stats.idle_count, 0);
1072 assert_eq!(stats.total_created, 0);
1073 assert_eq!(stats.total_acquired, 0);
1074 assert_eq!(stats.total_released, 0);
1075 assert_eq!(stats.total_evicted, 0);
1076 let _ = pool.drain().await;
1077 }
1078 }
1079
1080 #[tokio::test]
1081 async fn test_pool_idle_count_initial() {
1082 let config = test_pool_config(0, 5);
1083 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
1084 if let Ok(mut pool) = result {
1085 assert_eq!(pool.idle_count().await, 0);
1086 let _ = pool.drain().await;
1087 }
1088 }
1089
1090 #[tokio::test]
1091 async fn test_pool_drain_empty_pool() {
1092 let config = test_pool_config(0, 5);
1093 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
1094 if let Ok(mut pool) = result {
1095 let drain_result = pool.drain().await;
1097 assert!(drain_result.is_ok());
1098
1099 let stats = pool.stats().await;
1100 assert_eq!(stats.idle_count, 0);
1101 }
1102 }
1103
1104 #[tokio::test]
1105 async fn test_pool_drain_emits_event() {
1106 let emitter = test_event_emitter();
1107 let mut receiver = emitter.subscribe();
1108 let config = test_pool_config(0, 5);
1109
1110 let result = WarmPool::start(config, BoxConfig::default(), emitter).await;
1111 if let Ok(mut pool) = result {
1112 pool.drain().await.unwrap();
1113
1114 let mut found_drain_event = false;
1116 while let Ok(event) = receiver.try_recv() {
1118 if event.key == "pool.drained" {
1119 found_drain_event = true;
1120 }
1121 }
1122 assert!(found_drain_event, "Expected pool.drained event");
1123 }
1124 }
1125
1126 #[tokio::test]
1127 async fn test_pool_acquire_from_empty_pool_fails_without_shim() {
1128 let config = test_pool_config(0, 5);
1129 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
1130 if let Ok(pool) = result {
1131 let acquire_result = pool.acquire().await;
1134 assert!(acquire_result.is_err());
1135 }
1136 }
1137
1138 #[test]
1141 #[allow(clippy::unnecessary_min_or_max)]
1142 fn test_maintenance_check_interval_with_ttl() {
1143 let interval = if 300_u64 > 0 {
1145 (300_u64 / 5).max(5)
1146 } else {
1147 30
1148 };
1149 assert_eq!(interval, 60);
1150 }
1151
1152 #[test]
1153 #[allow(clippy::unnecessary_min_or_max)]
1154 fn test_maintenance_check_interval_short_ttl() {
1155 let interval = if 10_u64 > 0 { (10_u64 / 5).max(5) } else { 30 };
1157 assert_eq!(interval, 5);
1158 }
1159
1160 #[test]
1161 #[allow(clippy::unnecessary_min_or_max)]
1162 fn test_maintenance_check_interval_very_short_ttl() {
1163 let interval = if 1_u64 > 0 { (1_u64 / 5).max(5) } else { 30 };
1165 assert_eq!(interval, 5);
1166 }
1167
1168 #[test]
1169 #[allow(
1170 clippy::absurd_extreme_comparisons,
1171 clippy::erasing_op,
1172 clippy::unnecessary_min_or_max,
1173 unused_comparisons
1174 )]
1175 fn test_maintenance_check_interval_no_ttl() {
1176 let interval = if 0_u64 > 0 { (0_u64 / 5).max(5) } else { 30 };
1178 assert_eq!(interval, 30);
1179 }
1180
1181 #[test]
1184 fn test_warm_vm_created_at_is_recent() {
1185 let before = Instant::now();
1186 let created_at = Instant::now();
1187 let after = Instant::now();
1188
1189 assert!(created_at >= before);
1190 assert!(created_at <= after);
1191 }
1192
1193 #[test]
1196 fn test_pool_stats_all_fields() {
1197 let stats = PoolStats {
1198 idle_count: 10,
1199 total_created: 100,
1200 total_acquired: 80,
1201 total_released: 70,
1202 total_evicted: 15,
1203 };
1204
1205 assert_eq!(stats.idle_count, 10);
1206 assert_eq!(stats.total_created, 100);
1207 assert_eq!(stats.total_acquired, 80);
1208 assert_eq!(stats.total_released, 70);
1209 assert_eq!(stats.total_evicted, 15);
1210
1211 let debug = format!("{:?}", stats);
1213 assert!(debug.contains("10"));
1214 assert!(debug.contains("100"));
1215 assert!(debug.contains("80"));
1216 assert!(debug.contains("70"));
1217 assert!(debug.contains("15"));
1218 }
1219
1220 #[tokio::test]
1227 async fn test_pool_set_metrics_attaches() {
1228 let config = test_pool_config(0, 5);
1229 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
1230 match result {
1231 Ok(mut pool) => {
1232 let metrics = crate::prom::RuntimeMetrics::new();
1233 pool.set_metrics(metrics.clone());
1234 assert!(pool.metrics.is_some());
1235 assert_eq!(metrics.warm_pool_hits.get(), 0);
1237 assert_eq!(metrics.warm_pool_misses.get(), 0);
1238 assert_eq!(metrics.warm_pool_size.get(), 0);
1239 let _ = pool.drain().await;
1240 }
1241 Err(_) => {
1242 }
1244 }
1245 }
1246}