1use std::future::Future;
7use std::pin::Pin;
8use std::sync::Arc;
9use std::time::Instant;
10
11use a3s_box_core::config::{BoxConfig, PoolConfig};
12use a3s_box_core::error::{BoxError, Result};
13use a3s_box_core::event::{BoxEvent, EventEmitter};
14use tokio::sync::watch;
15use tokio::sync::Mutex;
16use tokio::task::JoinHandle;
17
18use crate::pool::scaler::PoolScaler;
19use crate::vm::VmManager;
20
21struct WarmVm {
23 vm: VmManager,
25 created_at: Instant,
27}
28
29type BootVmFuture<'a> = Pin<Box<dyn Future<Output = Result<VmManager>> + Send + 'a>>;
30
31#[derive(Debug, Clone)]
33pub struct PoolStats {
34 pub idle_count: usize,
36 pub total_created: u64,
38 pub total_acquired: u64,
40 pub total_released: u64,
42 pub total_evicted: u64,
44}
45
46pub struct WarmPool {
62 config: PoolConfig,
64 box_config: BoxConfig,
66 idle: Arc<Mutex<Vec<WarmVm>>>,
68 stats: Arc<Mutex<PoolStats>>,
70 event_emitter: EventEmitter,
72 replenish_handle: Option<JoinHandle<()>>,
74 shutdown_tx: watch::Sender<bool>,
76 shutdown_rx: watch::Receiver<bool>,
78 scaler: Option<Arc<Mutex<PoolScaler>>>,
80 metrics: Option<crate::prom::RuntimeMetrics>,
82 template: Arc<Mutex<TemplateState>>,
88}
89
90#[derive(Clone)]
93struct PoolTemplate {
94 mem_file: String,
95 state_file: String,
96 rootfs_cache_key: Option<String>,
97}
98
99const MAX_TEMPLATE_BUILD_FAILURES: u32 = 3;
107
108enum TemplateState {
110 Unbuilt,
112 Ready(PoolTemplate),
114 Failing(u32),
118 Unavailable,
122}
123
124impl WarmPool {
125 pub async fn start(
130 config: PoolConfig,
131 box_config: BoxConfig,
132 event_emitter: EventEmitter,
133 ) -> Result<Self> {
134 if config.max_size == 0 {
135 return Err(BoxError::PoolError(
136 "Pool max_size must be greater than 0".to_string(),
137 ));
138 }
139 if config.min_idle > config.max_size {
140 return Err(BoxError::PoolError(format!(
141 "Pool min_idle ({}) cannot exceed max_size ({})",
142 config.min_idle, config.max_size
143 )));
144 }
145
146 let idle = Arc::new(Mutex::new(Vec::with_capacity(config.max_size)));
147 let stats = Arc::new(Mutex::new(PoolStats {
148 idle_count: 0,
149 total_created: 0,
150 total_acquired: 0,
151 total_released: 0,
152 total_evicted: 0,
153 }));
154 let (shutdown_tx, shutdown_rx) = watch::channel(false);
155
156 let scaler = if config.scaling.enabled {
157 Some(Arc::new(Mutex::new(PoolScaler::new(
158 config.scaling.clone(),
159 config.min_idle,
160 config.max_size,
161 ))))
162 } else {
163 None
164 };
165
166 let mut pool = Self {
167 config,
168 box_config,
169 idle,
170 stats,
171 event_emitter,
172 replenish_handle: None,
173 shutdown_tx,
174 shutdown_rx,
175 scaler,
176 metrics: None,
177 template: Arc::new(Mutex::new(TemplateState::Unbuilt)),
178 };
179
180 pool.fill_to_min().await;
182
183 let handle = pool.spawn_maintenance_loop();
185 pool.replenish_handle = Some(handle);
186
187 tracing::info!(
188 min_idle = pool.config.min_idle,
189 max_size = pool.config.max_size,
190 idle_ttl_secs = pool.config.idle_ttl_secs,
191 "Warm pool started"
192 );
193
194 Ok(pool)
195 }
196
197 pub fn set_metrics(&mut self, metrics: crate::prom::RuntimeMetrics) {
199 metrics.warm_pool_capacity.set(self.config.max_size as i64);
200 self.metrics = Some(metrics);
201 }
202
203 pub async fn acquire(&self) -> Result<VmManager> {
208 {
210 let mut idle = self.idle.lock().await;
211 if let Some(warm_vm) = idle.pop() {
212 let mut stats = self.stats.lock().await;
213 stats.total_acquired += 1;
214 stats.idle_count = idle.len();
215
216 if let Some(ref scaler) = self.scaler {
218 scaler.lock().await.record_acquire(true);
219 }
220
221 if let Some(ref m) = self.metrics {
222 m.warm_pool_hits.inc();
223 m.warm_pool_size.set(idle.len() as i64);
224 }
225
226 self.event_emitter.emit(BoxEvent::with_string(
227 "pool.vm.acquired",
228 format!("Acquired VM {} from pool", warm_vm.vm.box_id()),
229 ));
230
231 tracing::debug!(
232 box_id = %warm_vm.vm.box_id(),
233 idle_remaining = idle.len(),
234 "Acquired VM from warm pool"
235 );
236
237 return Ok(warm_vm.vm);
238 }
239 }
240
241 tracing::info!("No idle VM in pool, booting on demand");
243
244 if let Some(ref scaler) = self.scaler {
246 scaler.lock().await.record_acquire(false);
247 }
248
249 if let Some(ref m) = self.metrics {
250 m.warm_pool_misses.inc();
251 }
252
253 let vm = self.boot_new_vm().await?;
254
255 let mut stats = self.stats.lock().await;
256 stats.total_acquired += 1;
257
258 Ok(vm)
259 }
260
261 pub async fn release(&self, vm: VmManager) -> Result<()> {
265 let mut idle = self.idle.lock().await;
266
267 if *self.shutdown_rx.borrow() {
272 drop(idle);
273 let mut vm = vm;
274 vm.destroy().await?;
275 return Ok(());
276 }
277
278 if idle.len() >= self.config.max_size {
279 drop(idle); let mut vm = vm;
282 vm.destroy().await?;
283
284 tracing::debug!(
285 box_id = %vm.box_id(),
286 "Pool full, destroyed released VM"
287 );
288 return Ok(());
289 }
290
291 let box_id = vm.box_id().to_string();
292 idle.push(WarmVm {
293 vm,
294 created_at: Instant::now(),
295 });
296
297 let mut stats = self.stats.lock().await;
298 stats.total_released += 1;
299 stats.idle_count = idle.len();
300
301 if let Some(ref m) = self.metrics {
302 m.warm_pool_size.set(idle.len() as i64);
303 }
304
305 self.event_emitter.emit(BoxEvent::with_string(
306 "pool.vm.released",
307 format!("Released VM {} back to pool", box_id),
308 ));
309
310 tracing::debug!(
311 box_id = %box_id,
312 idle_count = idle.len(),
313 "Released VM back to warm pool"
314 );
315
316 Ok(())
317 }
318
319 pub async fn stats(&self) -> PoolStats {
321 self.stats.lock().await.clone()
322 }
323
324 pub async fn idle_count(&self) -> usize {
326 self.idle.lock().await.len()
327 }
328
329 pub fn signal_shutdown(&self) {
333 let _ = self.shutdown_tx.send(true);
334 tracing::info!("Warm pool shutdown signaled");
335 }
336
337 pub async fn drain(&mut self) -> Result<()> {
339 let _ = self.shutdown_tx.send(true);
341
342 if let Some(handle) = self.replenish_handle.take() {
344 let _ = handle.await;
345 }
346
347 let mut idle = self.idle.lock().await;
349 let count = idle.len();
350
351 for warm_vm in idle.drain(..) {
352 let mut vm = warm_vm.vm;
353 if let Err(e) = vm.destroy().await {
354 tracing::warn!(
355 box_id = %vm.box_id(),
356 error = %e,
357 "Failed to destroy pooled VM during drain"
358 );
359 }
360 }
361
362 let mut stats = self.stats.lock().await;
363 stats.idle_count = 0;
364
365 self.event_emitter.emit(BoxEvent::empty("pool.drained"));
366
367 tracing::info!(destroyed = count, "Warm pool drained");
368
369 Ok(())
370 }
371
372 pub async fn drain_idle(&self) -> Result<()> {
377 let mut idle = self.idle.lock().await;
378 let count = idle.len();
379 for warm_vm in idle.drain(..) {
380 let mut vm = warm_vm.vm;
381 if let Err(e) = vm.destroy().await {
382 tracing::warn!(
383 box_id = %vm.box_id(),
384 error = %e,
385 "Failed to destroy pooled VM during drain_idle"
386 );
387 }
388 }
389 self.stats.lock().await.idle_count = 0;
390 tracing::info!(destroyed = count, "Warm pool idle VMs drained");
391 Ok(())
392 }
393
394 async fn remove_idle_vms(&self, box_ids: &[String]) {
399 let indices_to_remove: Vec<usize> = {
401 let idle = self.idle.lock().await;
402 idle.iter()
403 .enumerate()
404 .filter(|(_, wm)| box_ids.iter().any(|id| id == wm.vm.box_id()))
405 .map(|(i, _)| i)
406 .collect()
407 };
408
409 if indices_to_remove.is_empty() {
410 return;
411 }
412
413 let mut to_destroy: Vec<WarmVm> = Vec::new();
416 {
417 let mut idle = self.idle.lock().await;
418 for idx in indices_to_remove.into_iter().rev() {
419 if idx < idle.len() {
420 let warm_vm = idle.remove(idx);
421 to_destroy.push(warm_vm);
422 }
423 }
424 }
425
426 {
428 let idle_count = self.idle.lock().await.len();
429 if let Ok(mut stats) = self.stats.try_lock() {
430 stats.idle_count = idle_count;
431 }
432 }
433
434 for warm_vm in to_destroy {
436 let box_id = warm_vm.vm.box_id().to_string();
437 let mut vm = warm_vm.vm;
438 if let Err(e) = vm.destroy().await {
439 tracing::warn!(
440 box_id = %box_id,
441 error = %e,
442 "Failed to destroy VM during fill_to_min rollback"
443 );
444 } else {
445 tracing::debug!(box_id = %box_id, "Destroyed VM during fill_to_min rollback");
446 }
447 }
448 }
449
450 async fn boot_new_vm(&self) -> Result<VmManager> {
452 let vm = Self::boot_or_restore(
453 self.config.snapshot_fork,
454 &self.box_config,
455 &self.event_emitter,
456 &self.template,
457 )
458 .await?;
459
460 let mut stats = self.stats.lock().await;
461 stats.total_created += 1;
462
463 self.event_emitter.emit(BoxEvent::with_string(
464 "pool.vm.created",
465 format!("Booted new VM {}", vm.box_id()),
466 ));
467
468 Ok(vm)
469 }
470
471 fn boot_or_restore<'a>(
474 snapshot_fork: bool,
475 box_config: &'a BoxConfig,
476 event_emitter: &'a EventEmitter,
477 template: &'a Arc<Mutex<TemplateState>>,
478 ) -> BootVmFuture<'a> {
479 Box::pin(async move {
480 if snapshot_fork {
481 match Self::ensure_template(box_config, event_emitter, template).await {
486 Ok(tpl) => {
487 let mut cfg = box_config.clone();
488 cfg.snapshot_mem_file = Some(tpl.mem_file.clone());
489 cfg.restore_from = Some(tpl.state_file.clone());
490 cfg.snapshot_sock = None;
491 let mut vm = VmManager::new(cfg, event_emitter.clone());
492 vm.restore_rootfs_cache_key = tpl.rootfs_cache_key.clone();
493 let restored = async {
494 vm.boot().await?;
495 vm.wait_for_exec_available(std::time::Duration::from_secs(120))
496 .await
497 }
498 .await;
499 match restored {
500 Ok(()) => return Ok(vm),
501 Err(error) => {
502 let _ = vm.destroy_with_timeout(2000).await;
503 tracing::warn!(
504 %error,
505 "snapshot-fork restore failed; cold-booting this pool VM"
506 );
507 }
508 }
509 }
510 Err(error) => {
511 tracing::debug!(%error, "snapshot-fork unavailable; cold-booting this pool VM");
512 }
513 }
514 }
515 let mut vm = VmManager::new(box_config.clone(), event_emitter.clone());
516 vm.boot().await?;
517 vm.wait_for_exec_available(std::time::Duration::from_secs(120))
518 .await?;
519 Ok(vm)
520 })
521 }
522
523 async fn ensure_template(
530 box_config: &BoxConfig,
531 event_emitter: &EventEmitter,
532 template: &Arc<Mutex<TemplateState>>,
533 ) -> Result<PoolTemplate> {
534 let mut guard = template.lock().await;
535 let prior_failures = match &*guard {
536 TemplateState::Ready(t) => return Ok(t.clone()),
537 TemplateState::Unavailable => {
538 return Err(BoxError::PoolError(
539 "snapshot-fork template unavailable (native VM snapshot unsupported)"
540 .to_string(),
541 ));
542 }
543 TemplateState::Failing(n) => *n,
545 TemplateState::Unbuilt => 0,
546 };
547
548 match Self::build_template(box_config, event_emitter).await {
549 Ok(tpl) => {
550 *guard = TemplateState::Ready(tpl.clone());
551 event_emitter.emit(BoxEvent::with_string(
552 "pool.template.built",
553 format!(
554 "Snapshot-fork template built for image {}",
555 box_config.image
556 ),
557 ));
558 Ok(tpl)
559 }
560 Err(error) => {
561 let failures = prior_failures + 1;
566 if failures >= MAX_TEMPLATE_BUILD_FAILURES {
567 tracing::warn!(
568 %error, failures,
569 "snapshot-fork template build failed repeatedly; marking \
570 unavailable — the warm pool will cold-boot"
571 );
572 *guard = TemplateState::Unavailable;
573 } else {
574 tracing::warn!(
575 %error, failures,
576 "snapshot-fork template build failed; will retry on a later fill"
577 );
578 *guard = TemplateState::Failing(failures);
579 }
580 Err(error)
581 }
582 }
583 }
584
585 async fn build_template(
588 box_config: &BoxConfig,
589 event_emitter: &EventEmitter,
590 ) -> Result<PoolTemplate> {
591 let dir = a3s_box_core::dirs_home().join("pool").join(format!(
592 "tpl-{:016x}",
593 crate::vm::fnv1a_hash(&box_config.image)
594 ));
595 std::fs::create_dir_all(&dir).map_err(BoxError::IoError)?;
596
597 let lock_target = dir.clone();
604 let _lock =
605 tokio::task::spawn_blocking(move || crate::file_lock::FileLock::acquire(&lock_target))
606 .await
607 .map_err(|e| BoxError::PoolError(format!("Template lock task failed: {e}")))?
608 .map_err(|e| BoxError::PoolError(format!("Failed to lock template dir: {e}")))?;
609
610 let mem_file = dir.join("template.ram");
611 let sock = dir.join("template.sock");
612 let state_file = dir.join("template.state");
613 let _ = std::fs::remove_file(&sock);
614
615 let mut cfg = box_config.clone();
617 cfg.snapshot_mem_file = Some(mem_file.to_string_lossy().into_owned());
618 cfg.snapshot_sock = Some(sock.to_string_lossy().into_owned());
619 cfg.restore_from = None;
620 let mut src = VmManager::new(cfg, event_emitter.clone());
621 src.boot().await?;
622 let rootfs_cache_key = match src.current_rootfs_cache_key() {
623 Ok(key) => key,
624 Err(error) => {
625 let _ = src.destroy_with_timeout(2000).await;
626 return Err(error);
627 }
628 };
629
630 let snapshot = Self::trigger_snapshot(&sock, &state_file).await;
639 let _ = src.destroy_with_timeout(2000).await;
640 snapshot?;
641
642 Ok(PoolTemplate {
643 mem_file: mem_file.to_string_lossy().into_owned(),
644 state_file: state_file.to_string_lossy().into_owned(),
645 rootfs_cache_key,
646 })
647 }
648
649 #[cfg(unix)]
655 async fn trigger_snapshot(sock: &std::path::Path, state_file: &std::path::Path) -> Result<()> {
656 use tokio::io::{AsyncReadExt, AsyncWriteExt};
657 let mut stream = None;
659 for _ in 0..200 {
660 match tokio::net::UnixStream::connect(sock).await {
661 Ok(s) => {
662 stream = Some(s);
663 break;
664 }
665 Err(_) => tokio::time::sleep(std::time::Duration::from_millis(25)).await,
666 }
667 }
668 let mut stream = stream.ok_or_else(|| {
669 BoxError::PoolError(format!("snapshot socket {} never appeared", sock.display()))
670 })?;
671 let cmd = format!("snapshot {}\n", state_file.display());
672 stream
673 .write_all(cmd.as_bytes())
674 .await
675 .map_err(BoxError::IoError)?;
676 let mut buf = [0u8; 64];
677 let n = stream.read(&mut buf).await.map_err(BoxError::IoError)?;
678 let reply = String::from_utf8_lossy(&buf[..n]);
679 if reply.trim() == "ok" {
680 Ok(())
681 } else {
682 Err(BoxError::PoolError(format!(
683 "snapshot trigger failed: {}",
684 reply.trim()
685 )))
686 }
687 }
688
689 #[cfg(not(unix))]
693 async fn trigger_snapshot(
694 _sock: &std::path::Path,
695 _state_file: &std::path::Path,
696 ) -> Result<()> {
697 Err(BoxError::PoolError(
698 "snapshot-fork is only supported on Linux/KVM hosts".to_string(),
699 ))
700 }
701
702 async fn fill_to_min(&self) {
704 let current = self.idle.lock().await.len();
705 let needed = self.config.min_idle.saturating_sub(current);
706
707 if needed == 0 {
708 return;
709 }
710
711 tracing::debug!(
712 current,
713 needed,
714 min_idle = self.config.min_idle,
715 "Replenishing warm pool"
716 );
717
718 let mut added_ids: Vec<String> = Vec::new();
720
721 for _ in 0..needed {
722 match self.boot_new_vm().await {
723 Ok(vm) => {
724 let box_id = vm.box_id().to_string();
725 let mut idle = self.idle.lock().await;
726 idle.push(WarmVm {
727 vm,
728 created_at: Instant::now(),
729 });
730 let mut stats = self.stats.lock().await;
731 stats.idle_count = idle.len();
732 added_ids.push(box_id.clone());
733
734 tracing::debug!(box_id = %box_id, "Added VM to warm pool");
735 }
736 Err(e) => {
737 tracing::warn!(error = %e, "Failed to boot VM for warm pool");
738 if !added_ids.is_empty() {
740 tracing::info!(
741 count = added_ids.len(),
742 "Cleaning up VMs added before fill_to_min failed"
743 );
744 self.remove_idle_vms(&added_ids).await;
745 }
746 break;
747 }
748 }
749 }
750
751 self.event_emitter.emit(BoxEvent::empty("pool.replenish"));
752 }
753
754 fn spawn_maintenance_loop(&self) -> JoinHandle<()> {
761 let idle = Arc::clone(&self.idle);
762 let stats = Arc::clone(&self.stats);
763 let config = self.config.clone();
764 let box_config = self.box_config.clone();
765 let event_emitter = self.event_emitter.clone();
766 let mut shutdown_rx = self.shutdown_rx.clone();
767 let scaler = self.scaler.clone();
768 let template = Arc::clone(&self.template);
769
770 tokio::spawn(async move {
771 let check_interval = std::time::Duration::from_secs(
772 if config.idle_ttl_secs > 0 {
774 (config.idle_ttl_secs / 5).max(5)
775 } else {
776 30
777 },
778 );
779
780 let mut effective_min_idle = config.min_idle;
782
783 loop {
784 tokio::select! {
785 result = shutdown_rx.changed() => {
786 if result.is_ok() && *shutdown_rx.borrow() {
787 tracing::debug!("Pool maintenance loop shutting down");
788 break;
789 }
790 }
791 _ = tokio::time::sleep(check_interval) => {
792 if config.idle_ttl_secs > 0 {
794 Self::evict_expired_static(
795 &idle,
796 &stats,
797 &event_emitter,
798 config.idle_ttl_secs,
799 ).await;
800 }
801
802 if let Some(ref scaler) = scaler {
804 let mut s = scaler.lock().await;
805 let decision = s.evaluate();
806 let new_min = s.current_min_idle();
807 if new_min != effective_min_idle {
808 tracing::info!(
809 old_min_idle = effective_min_idle,
810 new_min_idle = new_min,
811 ?decision,
812 "Autoscaler adjusted min_idle"
813 );
814 event_emitter.emit(BoxEvent::with_string(
815 "pool.autoscale",
816 format!(
817 "min_idle adjusted {} → {} ({:?})",
818 effective_min_idle, new_min, decision
819 ),
820 ));
821 effective_min_idle = new_min;
822 }
823 }
824
825 let current = idle.lock().await.len();
827 if current < effective_min_idle {
828 let needed = effective_min_idle - current;
829 tracing::debug!(current, needed, min_idle = effective_min_idle, "Replenishing warm pool");
830
831 let mut set = tokio::task::JoinSet::new();
838 for _ in 0..needed {
839 let sf = config.snapshot_fork;
840 let bc = box_config.clone();
841 let ee = event_emitter.clone();
842 let tpl = Arc::clone(&template);
843 set.spawn(async move {
844 WarmPool::boot_or_restore(sf, &bc, &ee, &tpl).await
845 });
846 }
847 while let Some(joined) = set.join_next().await {
848 match joined {
849 Ok(Ok(mut vm)) => {
850 let box_id = vm.box_id().to_string();
851 let mut pool = idle.lock().await;
862 if *shutdown_rx.borrow() {
863 drop(pool);
864 tracing::debug!(
865 box_id = %box_id,
866 "Pool shutting down mid-replenish; destroying freshly-booted VM"
867 );
868 let _ = vm.destroy_with_timeout(2000).await;
869 continue;
870 }
871 pool.push(WarmVm {
872 vm,
873 created_at: Instant::now(),
874 });
875 let mut s = stats.lock().await;
876 s.total_created += 1;
877 s.idle_count = pool.len();
878 drop(s);
879 drop(pool);
880
881 event_emitter.emit(BoxEvent::with_string(
882 "pool.vm.created",
883 format!("Replenished VM {}", box_id),
884 ));
885 }
886 Ok(Err(e)) => {
887 tracing::warn!(error = %e, "Failed to replenish warm pool");
888 }
889 Err(e) => {
890 tracing::warn!(error = %e, "Replenish task join error");
891 }
892 }
893 }
894
895 event_emitter.emit(BoxEvent::empty("pool.replenish"));
896 }
897 }
898 }
899 }
900 })
901 }
902
903 async fn evict_expired_static(
905 idle: &Arc<Mutex<Vec<WarmVm>>>,
906 stats: &Arc<Mutex<PoolStats>>,
907 event_emitter: &EventEmitter,
908 idle_ttl_secs: u64,
909 ) {
910 let ttl = std::time::Duration::from_secs(idle_ttl_secs);
911
912 let mut pool = idle.lock().await;
913 let mut kept = Vec::new();
914 let mut expired = Vec::new();
915
916 for warm_vm in pool.drain(..) {
917 if warm_vm.created_at.elapsed() > ttl {
918 expired.push(warm_vm);
919 } else {
920 kept.push(warm_vm);
921 }
922 }
923 *pool = kept;
924 let after_count = pool.len();
925 drop(pool);
926
927 let evicted_count = expired.len();
928 for warm_vm in expired {
929 let mut vm = warm_vm.vm;
930 let _ = vm.destroy().await;
931 }
932
933 if evicted_count > 0 {
934 let mut s = stats.lock().await;
935 s.total_evicted += evicted_count as u64;
936 s.idle_count = after_count;
937
938 event_emitter.emit(BoxEvent::with_string(
939 "pool.vm.evicted",
940 format!("Evicted {} expired VMs", evicted_count),
941 ));
942 }
943 }
944}
945
946#[cfg(test)]
947mod tests {
948 use super::*;
949 use a3s_box_core::config::PoolConfig;
950
951 fn test_pool_config(min_idle: usize, max_size: usize) -> PoolConfig {
952 PoolConfig {
953 enabled: true,
954 min_idle,
955 max_size,
956 idle_ttl_secs: 300,
957 ..Default::default()
958 }
959 }
960
961 fn test_event_emitter() -> EventEmitter {
962 EventEmitter::new(100)
963 }
964
965 #[test]
966 fn boot_or_restore_future_stays_heap_indirected() {
967 let config = BoxConfig::default();
968 let emitter = test_event_emitter();
969 let template = Arc::new(Mutex::new(TemplateState::Unbuilt));
970 let future = WarmPool::boot_or_restore(false, &config, &emitter, &template);
971
972 assert!(
973 std::mem::size_of_val(&future) <= 2 * std::mem::size_of::<usize>(),
974 "boot_or_restore future must remain pointer-sized so pool misses fit on Tokio worker stacks; got {} bytes",
975 std::mem::size_of_val(&future)
976 );
977 }
978
979 #[tokio::test]
982 async fn test_pool_rejects_zero_max_size() {
983 let config = test_pool_config(0, 0);
984 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
985 match result {
986 Err(e) => assert!(e.to_string().contains("max_size must be greater than 0")),
987 Ok(_) => panic!("Expected error for zero max_size"),
988 }
989 }
990
991 #[tokio::test]
992 async fn test_pool_rejects_min_idle_exceeds_max() {
993 let config = test_pool_config(10, 5);
994 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
995 match result {
996 Err(e) => assert!(e.to_string().contains("cannot exceed max_size")),
997 Ok(_) => panic!("Expected error for min_idle > max_size"),
998 }
999 }
1000
1001 #[test]
1004 fn test_pool_stats_default() {
1005 let stats = PoolStats {
1006 idle_count: 0,
1007 total_created: 0,
1008 total_acquired: 0,
1009 total_released: 0,
1010 total_evicted: 0,
1011 };
1012 assert_eq!(stats.idle_count, 0);
1013 assert_eq!(stats.total_created, 0);
1014 }
1015
1016 #[test]
1017 fn test_pool_stats_clone() {
1018 let stats = PoolStats {
1019 idle_count: 3,
1020 total_created: 10,
1021 total_acquired: 7,
1022 total_released: 5,
1023 total_evicted: 2,
1024 };
1025 let cloned = stats.clone();
1026 assert_eq!(cloned.idle_count, 3);
1027 assert_eq!(cloned.total_created, 10);
1028 assert_eq!(cloned.total_acquired, 7);
1029 assert_eq!(cloned.total_released, 5);
1030 assert_eq!(cloned.total_evicted, 2);
1031 }
1032
1033 #[test]
1034 fn test_pool_stats_debug() {
1035 let stats = PoolStats {
1036 idle_count: 1,
1037 total_created: 2,
1038 total_acquired: 3,
1039 total_released: 4,
1040 total_evicted: 5,
1041 };
1042 let debug = format!("{:?}", stats);
1043 assert!(debug.contains("idle_count"));
1044 assert!(debug.contains("total_created"));
1045 }
1046
1047 #[test]
1050 fn test_pool_config_roundtrip() {
1051 let config = PoolConfig {
1052 enabled: true,
1053 min_idle: 3,
1054 max_size: 10,
1055 idle_ttl_secs: 600,
1056 ..Default::default()
1057 };
1058
1059 let json = serde_json::to_string(&config).unwrap();
1060 let parsed: PoolConfig = serde_json::from_str(&json).unwrap();
1061
1062 assert!(parsed.enabled);
1063 assert_eq!(parsed.min_idle, 3);
1064 assert_eq!(parsed.max_size, 10);
1065 assert_eq!(parsed.idle_ttl_secs, 600);
1066 }
1067
1068 #[test]
1069 fn test_pool_config_default_values() {
1070 let config = PoolConfig::default();
1071 assert!(!config.enabled);
1072 assert_eq!(config.min_idle, 1);
1073 assert_eq!(config.max_size, 5);
1074 assert_eq!(config.idle_ttl_secs, 300);
1075 }
1076
1077 #[test]
1078 fn test_pool_config_deserialization_with_defaults() {
1079 let json = r#"{"enabled": true}"#;
1080 let config: PoolConfig = serde_json::from_str(json).unwrap();
1081 assert!(config.enabled);
1082 assert_eq!(config.min_idle, 1);
1083 assert_eq!(config.max_size, 5);
1084 assert_eq!(config.idle_ttl_secs, 300);
1085 }
1086
1087 #[tokio::test]
1090 async fn test_pool_accepts_min_idle_equals_max() {
1091 let config = test_pool_config(3, 3);
1092 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
1095 match result {
1097 Err(e) => assert!(!e.to_string().contains("cannot exceed max_size")),
1098 Ok(mut pool) => {
1099 let _ = pool.drain().await;
1100 }
1101 }
1102 }
1103
1104 #[tokio::test]
1105 async fn test_pool_accepts_min_idle_zero() {
1106 let config = test_pool_config(0, 5);
1107 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
1109 match result {
1110 Ok(mut pool) => {
1111 assert_eq!(pool.idle_count().await, 0);
1113 let stats = pool.stats().await;
1114 assert_eq!(stats.idle_count, 0);
1115 assert_eq!(stats.total_created, 0);
1116 let _ = pool.drain().await;
1117 }
1118 Err(e) => {
1119 assert!(!e.to_string().contains("max_size"));
1121 assert!(!e.to_string().contains("min_idle"));
1122 }
1123 }
1124 }
1125
1126 #[tokio::test]
1129 async fn test_pool_stats_initial() {
1130 let config = test_pool_config(0, 5);
1131 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
1132 if let Ok(mut pool) = result {
1133 let stats = pool.stats().await;
1134 assert_eq!(stats.idle_count, 0);
1135 assert_eq!(stats.total_created, 0);
1136 assert_eq!(stats.total_acquired, 0);
1137 assert_eq!(stats.total_released, 0);
1138 assert_eq!(stats.total_evicted, 0);
1139 let _ = pool.drain().await;
1140 }
1141 }
1142
1143 #[tokio::test]
1144 async fn test_pool_idle_count_initial() {
1145 let config = test_pool_config(0, 5);
1146 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
1147 if let Ok(mut pool) = result {
1148 assert_eq!(pool.idle_count().await, 0);
1149 let _ = pool.drain().await;
1150 }
1151 }
1152
1153 #[tokio::test]
1154 async fn test_pool_drain_empty_pool() {
1155 let config = test_pool_config(0, 5);
1156 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
1157 if let Ok(mut pool) = result {
1158 let drain_result = pool.drain().await;
1160 assert!(drain_result.is_ok());
1161
1162 let stats = pool.stats().await;
1163 assert_eq!(stats.idle_count, 0);
1164 }
1165 }
1166
1167 #[tokio::test]
1168 async fn test_pool_drain_emits_event() {
1169 let emitter = test_event_emitter();
1170 let mut receiver = emitter.subscribe();
1171 let config = test_pool_config(0, 5);
1172
1173 let result = WarmPool::start(config, BoxConfig::default(), emitter).await;
1174 if let Ok(mut pool) = result {
1175 pool.drain().await.unwrap();
1176
1177 let mut found_drain_event = false;
1179 while let Ok(event) = receiver.try_recv() {
1181 if event.key == "pool.drained" {
1182 found_drain_event = true;
1183 }
1184 }
1185 assert!(found_drain_event, "Expected pool.drained event");
1186 }
1187 }
1188
1189 #[tokio::test]
1190 async fn test_pool_acquire_from_empty_pool_fails_without_shim() {
1191 let config = test_pool_config(0, 5);
1192 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
1193 if let Ok(pool) = result {
1194 let acquire_result = pool.acquire().await;
1197 assert!(acquire_result.is_err());
1198 }
1199 }
1200
1201 #[test]
1204 #[allow(clippy::unnecessary_min_or_max)]
1205 fn test_maintenance_check_interval_with_ttl() {
1206 let interval = if 300_u64 > 0 {
1208 (300_u64 / 5).max(5)
1209 } else {
1210 30
1211 };
1212 assert_eq!(interval, 60);
1213 }
1214
1215 #[test]
1216 #[allow(clippy::unnecessary_min_or_max)]
1217 fn test_maintenance_check_interval_short_ttl() {
1218 let interval = if 10_u64 > 0 { (10_u64 / 5).max(5) } else { 30 };
1220 assert_eq!(interval, 5);
1221 }
1222
1223 #[test]
1224 #[allow(clippy::unnecessary_min_or_max)]
1225 fn test_maintenance_check_interval_very_short_ttl() {
1226 let interval = if 1_u64 > 0 { (1_u64 / 5).max(5) } else { 30 };
1228 assert_eq!(interval, 5);
1229 }
1230
1231 #[test]
1232 #[allow(
1233 clippy::absurd_extreme_comparisons,
1234 clippy::erasing_op,
1235 clippy::unnecessary_min_or_max,
1236 unused_comparisons
1237 )]
1238 fn test_maintenance_check_interval_no_ttl() {
1239 let interval = if 0_u64 > 0 { (0_u64 / 5).max(5) } else { 30 };
1241 assert_eq!(interval, 30);
1242 }
1243
1244 #[test]
1247 fn test_warm_vm_created_at_is_recent() {
1248 let before = Instant::now();
1249 let created_at = Instant::now();
1250 let after = Instant::now();
1251
1252 assert!(created_at >= before);
1253 assert!(created_at <= after);
1254 }
1255
1256 #[test]
1259 fn test_pool_stats_all_fields() {
1260 let stats = PoolStats {
1261 idle_count: 10,
1262 total_created: 100,
1263 total_acquired: 80,
1264 total_released: 70,
1265 total_evicted: 15,
1266 };
1267
1268 assert_eq!(stats.idle_count, 10);
1269 assert_eq!(stats.total_created, 100);
1270 assert_eq!(stats.total_acquired, 80);
1271 assert_eq!(stats.total_released, 70);
1272 assert_eq!(stats.total_evicted, 15);
1273
1274 let debug = format!("{:?}", stats);
1276 assert!(debug.contains("10"));
1277 assert!(debug.contains("100"));
1278 assert!(debug.contains("80"));
1279 assert!(debug.contains("70"));
1280 assert!(debug.contains("15"));
1281 }
1282
1283 #[tokio::test]
1290 async fn test_pool_set_metrics_attaches() {
1291 let config = test_pool_config(0, 5);
1292 let result = WarmPool::start(config, BoxConfig::default(), test_event_emitter()).await;
1293 match result {
1294 Ok(mut pool) => {
1295 let metrics = crate::prom::RuntimeMetrics::new();
1296 pool.set_metrics(metrics.clone());
1297 assert!(pool.metrics.is_some());
1298 assert_eq!(metrics.warm_pool_hits.get(), 0);
1300 assert_eq!(metrics.warm_pool_misses.get(), 0);
1301 assert_eq!(metrics.warm_pool_size.get(), 0);
1302 let _ = pool.drain().await;
1303 }
1304 Err(_) => {
1305 }
1307 }
1308 }
1309}