77//! `on_destroy` stops the child while the instance stays warm for the next
88//! placement.
99
10- use std:: sync:: Arc ;
10+ use std:: sync:: { Arc , LazyLock } ;
1111
1212use anyhow:: { Context , Result } ;
1313use async_trait:: async_trait;
@@ -21,6 +21,12 @@ use crate::{
2121 runner_config,
2222} ;
2323
24+ /// Live actor contexts on this instance, keyed by actor id. Lets the process
25+ /// shutdown path report actors as crashed when the platform reclaims the
26+ /// container out from under them.
27+ static ACTOR_CTXS : LazyLock < scc:: HashMap < String , Ctx < GameServer > > > =
28+ LazyLock :: new ( scc:: HashMap :: new) ;
29+
2430pub struct GameServer {
2531 child : TokioMutex < Option < Arc < ChildProcess > > > ,
2632}
@@ -35,6 +41,7 @@ impl GameServer {
3541 // deliberate, then stop. `stop` is idempotent if the process shutdown
3642 // sweep already stopped this child.
3743 children ( ) . remove_async ( actor_id) . await ;
44+ ACTOR_CTXS . remove_async ( actor_id) . await ;
3845 let child = self . child . lock ( ) . await . take ( ) ;
3946 if let Some ( child) = child {
4047 child. stop ( effective_stop_grace ( ) ) . await ;
@@ -87,6 +94,7 @@ impl Actor for GameServer {
8794 "{} runner: actor already running, ignoring duplicate start" ,
8895 log_prefix( & actor_id, existing. key. as_deref( ) )
8996 ) ;
97+ register_ctx ( & actor_id, & ctx) . await ;
9098 * self . child . lock ( ) . await = Some ( existing) ;
9199 return Ok ( ( ) ) ;
92100 }
@@ -152,6 +160,10 @@ impl Actor for GameServer {
152160 release_child_port ( child_port) . await ;
153161 anyhow:: bail!( "a child for actor {actor_id} is already registered" ) ;
154162 }
163+ // Register only now that startup has succeeded. Registering earlier would
164+ // leak an entry for any generation whose start failed, since a failed
165+ // start never runs on_destroy/on_sleep to remove it.
166+ register_ctx ( & actor_id, & ctx) . await ;
155167 * self . child . lock ( ) . await = Some ( child) ;
156168 Ok ( ( ) )
157169 }
@@ -236,6 +248,38 @@ impl Actor for GameServer {
236248 }
237249}
238250
251+ /// Register an actor context for crash-on-shutdown reporting. Overwrites any
252+ /// stale entry left by a prior generation with the same id.
253+ async fn register_ctx ( actor_id : & str , ctx : & Ctx < GameServer > ) {
254+ ACTOR_CTXS . remove_async ( actor_id) . await ;
255+ let _ = ACTOR_CTXS
256+ . insert_async ( actor_id. to_string ( ) , ctx. clone ( ) )
257+ . await ;
258+ }
259+
260+ /// Report every live actor on this instance as crashed. Called when the
261+ /// platform reclaims the container (an unexpected SIGTERM) so the reclaim
262+ /// surfaces as a crash on the engine instead of a silent reallocation. Runs
263+ /// while the envoy is still connected so the crash reaches the engine.
264+ pub async fn crash_all_actors ( message : & str ) {
265+ let mut ctxs = Vec :: new ( ) ;
266+ ACTOR_CTXS
267+ . retain_async ( |_, ctx| {
268+ ctxs. push ( ctx. clone ( ) ) ;
269+ false
270+ } )
271+ . await ;
272+ for ctx in ctxs {
273+ if let Err ( err) = ctx. stop_with_error ( message) {
274+ tracing:: debug!(
275+ actor_id = %ctx. actor_id( ) ,
276+ error = ?err,
277+ "crash-on-shutdown stop_with_error failed"
278+ ) ;
279+ }
280+ }
281+ }
282+
239283fn actor_key_string ( ctx : & Ctx < GameServer > ) -> Option < String > {
240284 let key = ctx. key ( ) ;
241285 if key. is_empty ( ) {
0 commit comments