@@ -75,10 +75,12 @@ pub struct WarmPool {
7575 scaler : Option < Arc < Mutex < PoolScaler > > > ,
7676 /// Prometheus metrics (optional).
7777 metrics : Option < crate :: prom:: RuntimeMetrics > ,
78- /// Snapshot-fork template (built lazily on first fill when
78+ /// Snapshot-fork template state (built lazily on first fill when
7979 /// `config.snapshot_fork`): the file-backed RAM image + state file every other
80- /// pool VM restores from. `None` until the first template is built.
81- template : Arc < Mutex < Option < PoolTemplate > > > ,
80+ /// pool VM restores from. Caches an `Unavailable` verdict so a build failure
81+ /// (native VM snapshot unsupported on this build) is not re-attempted on every
82+ /// fill — the pool cold-boots instead.
83+ template : Arc < Mutex < TemplateState > > ,
8284}
8385
8486/// A built snapshot-fork template: the shared RAM image + state file that pool VMs
@@ -89,6 +91,17 @@ struct PoolTemplate {
8991 state_file : String ,
9092}
9193
94+ /// Cached state of the snapshot-fork template.
95+ enum TemplateState {
96+ /// Not built yet — the first snapshot-fork fill attempts the build.
97+ Unbuilt ,
98+ /// Built and ready; pool VMs restore from it.
99+ Ready ( PoolTemplate ) ,
100+ /// The build failed (native VM snapshot is unavailable on this build/platform).
101+ /// Cached so it is not retried — `boot_or_restore` cold-boots instead.
102+ Unavailable ,
103+ }
104+
92105impl WarmPool {
93106 /// Create and start the warm pool.
94107 ///
@@ -142,7 +155,7 @@ impl WarmPool {
142155 shutdown_rx,
143156 scaler,
144157 metrics : None ,
145- template : Arc :: new ( Mutex :: new ( None ) ) ,
158+ template : Arc :: new ( Mutex :: new ( TemplateState :: Unbuilt ) ) ,
146159 } ;
147160
148161 // Initial fill
@@ -431,37 +444,87 @@ impl WarmPool {
431444 snapshot_fork : bool ,
432445 box_config : & BoxConfig ,
433446 event_emitter : & EventEmitter ,
434- template : & Arc < Mutex < Option < PoolTemplate > > > ,
447+ template : & Arc < Mutex < TemplateState > > ,
435448 ) -> Result < VmManager > {
436449 if snapshot_fork {
437- let tpl = Self :: ensure_template ( box_config, event_emitter, template) . await ?;
438- let mut cfg = box_config. clone ( ) ;
439- cfg. snapshot_mem_file = Some ( tpl. mem_file . clone ( ) ) ;
440- cfg. restore_from = Some ( tpl. state_file . clone ( ) ) ;
441- cfg. snapshot_sock = None ;
442- let mut vm = VmManager :: new ( cfg, event_emitter. clone ( ) ) ;
443- vm. boot ( ) . await ?;
444- Ok ( vm)
445- } else {
446- let mut vm = VmManager :: new ( box_config. clone ( ) , event_emitter. clone ( ) ) ;
447- vm. boot ( ) . await ?;
448- Ok ( vm)
450+ // Try the snapshot-fork template. If it can't be built (native VM
451+ // snapshot unavailable — the verdict is cached so this is attempted at
452+ // most once), fall back to a normal cold boot so the warm pool still
453+ // fills rather than failing outright.
454+ match Self :: ensure_template ( box_config, event_emitter, template) . await {
455+ Ok ( tpl) => {
456+ let mut cfg = box_config. clone ( ) ;
457+ cfg. snapshot_mem_file = Some ( tpl. mem_file . clone ( ) ) ;
458+ cfg. restore_from = Some ( tpl. state_file . clone ( ) ) ;
459+ cfg. snapshot_sock = None ;
460+ let mut vm = VmManager :: new ( cfg, event_emitter. clone ( ) ) ;
461+ vm. boot ( ) . await ?;
462+ return Ok ( vm) ;
463+ }
464+ Err ( error) => {
465+ tracing:: debug!( %error, "snapshot-fork unavailable; cold-booting this pool VM" ) ;
466+ }
467+ }
449468 }
469+ let mut vm = VmManager :: new ( box_config. clone ( ) , event_emitter. clone ( ) ) ;
470+ vm. boot ( ) . await ?;
471+ Ok ( vm)
450472 }
451473
452- /// Build the snapshot-fork template once (lazily): cold-boot one VM with
453- /// file-backed RAM, snapshot it, tear down the source. Concurrent callers wait on
454- /// the lock and reuse the first-built template.
474+ /// Get the snapshot-fork template, building it once lazily. Concurrent callers
475+ /// wait on the lock and reuse the first result — a built template OR a cached
476+ /// `Unavailable` verdict, so a failed build (native VM snapshot unsupported on
477+ /// this build) is attempted at most once rather than re-tried (and re-timed-out)
478+ /// on every pool fill. Returns `Err` when unavailable so `boot_or_restore` cold
479+ /// boots instead.
455480 async fn ensure_template (
456481 box_config : & BoxConfig ,
457482 event_emitter : & EventEmitter ,
458- template : & Arc < Mutex < Option < PoolTemplate > > > ,
483+ template : & Arc < Mutex < TemplateState > > ,
459484 ) -> Result < PoolTemplate > {
460485 let mut guard = template. lock ( ) . await ;
461- if let Some ( t) = guard. as_ref ( ) {
462- return Ok ( t. clone ( ) ) ;
486+ match & * guard {
487+ TemplateState :: Ready ( t) => return Ok ( t. clone ( ) ) ,
488+ TemplateState :: Unavailable => {
489+ return Err ( BoxError :: PoolError (
490+ "snapshot-fork template unavailable (native VM snapshot unsupported)"
491+ . to_string ( ) ,
492+ ) ) ;
493+ }
494+ TemplateState :: Unbuilt => { }
463495 }
464496
497+ match Self :: build_template ( box_config, event_emitter) . await {
498+ Ok ( tpl) => {
499+ * guard = TemplateState :: Ready ( tpl. clone ( ) ) ;
500+ event_emitter. emit ( BoxEvent :: with_string (
501+ "pool.template.built" ,
502+ format ! (
503+ "Snapshot-fork template built for image {}" ,
504+ box_config. image
505+ ) ,
506+ ) ) ;
507+ Ok ( tpl)
508+ }
509+ Err ( error) => {
510+ // Cache the verdict and warn ONCE; the pool falls back to cold boot.
511+ tracing:: warn!(
512+ %error,
513+ "snapshot-fork template build failed; marking unavailable — \
514+ the warm pool will cold-boot instead"
515+ ) ;
516+ * guard = TemplateState :: Unavailable ;
517+ Err ( error)
518+ }
519+ }
520+ }
521+
522+ /// Cold-boot one source VM with file-backed RAM + a trigger socket, snapshot it,
523+ /// and tear it down — leaving the RAM image + state file as the template.
524+ async fn build_template (
525+ box_config : & BoxConfig ,
526+ event_emitter : & EventEmitter ,
527+ ) -> Result < PoolTemplate > {
465528 let dir = a3s_box_core:: dirs_home ( ) . join ( "pool" ) . join ( format ! (
466529 "tpl-{:016x}" ,
467530 crate :: vm:: fnv1a_hash( & box_config. image)
@@ -485,18 +548,10 @@ impl WarmPool {
485548 Self :: trigger_snapshot ( & sock, & state_file) . await ?;
486549 let _ = src. destroy_with_timeout ( 2000 ) . await ;
487550
488- let tpl = PoolTemplate {
551+ Ok ( PoolTemplate {
489552 mem_file : mem_file. to_string_lossy ( ) . into_owned ( ) ,
490553 state_file : state_file. to_string_lossy ( ) . into_owned ( ) ,
491- } ;
492- * guard = Some ( tpl. clone ( ) ) ;
493-
494- event_emitter. emit ( BoxEvent :: with_string (
495- "pool.template.built" ,
496- format ! ( "Snapshot-fork template built at {}" , dir. display( ) ) ,
497- ) ) ;
498-
499- Ok ( tpl)
554+ } )
500555 }
501556
502557 /// Send a `snapshot <state>` request to libkrun's per-template trigger socket and
0 commit comments