@@ -8,6 +8,7 @@ use rivet_envoy_protocol as protocol;
88use tokio:: sync:: oneshot:: error:: TryRecvError ;
99use tokio:: sync:: { mpsc, oneshot} ;
1010use tokio:: task:: JoinSet ;
11+ use tokio_util:: sync:: CancellationToken ;
1112use tracing:: Instrument ;
1213
1314use crate :: connection:: ws_send;
@@ -115,6 +116,8 @@ struct ActorContext {
115116 ws_entries : BufferMap < WsEntry > ,
116117 hibernating_requests : Vec < protocol:: HibernatingRequest > ,
117118 active_http_request_count : Arc < AsyncCounter > ,
119+ /// Cancelled when this generation is declared lost, by the engine or by the envoy itself.
120+ lost : CancellationToken ,
118121}
119122
120123struct PendingStop {
@@ -123,6 +126,19 @@ struct PendingStop {
123126 stop_message : Option < String > ,
124127}
125128
129+ const LOST_STOP_MESSAGE : & str = "envoy connection lost" ;
130+
131+ impl PendingStop {
132+ /// A stop that escalates to lost must still report `Stopped(Error)`, even if it started as a
133+ /// graceful stop. See `begin_stop` for why `Stopped(Ok)` would destroy the actor.
134+ fn mark_lost ( & mut self ) {
135+ self . stop_code = protocol:: StopCode :: Error ;
136+ if self . stop_message . is_none ( ) {
137+ self . stop_message = Some ( LOST_STOP_MESSAGE . to_string ( ) ) ;
138+ }
139+ }
140+ }
141+
126142enum StopProgress {
127143 Stopped ,
128144 Pending ( PendingStop ) ,
@@ -135,8 +151,13 @@ pub fn create_actor(
135151 config : protocol:: ActorConfig ,
136152 hibernating_requests : Vec < protocol:: HibernatingRequest > ,
137153 preloaded_kv : Option < protocol:: PreloadedKv > ,
138- ) -> ( mpsc:: UnboundedSender < ToActor > , Arc < AsyncCounter > ) {
154+ ) -> (
155+ mpsc:: UnboundedSender < ToActor > ,
156+ CancellationToken ,
157+ Arc < AsyncCounter > ,
158+ ) {
139159 let ( tx, rx) = mpsc:: unbounded_channel ( ) ;
160+ let lost = CancellationToken :: new ( ) ;
140161 let active_http_request_count = Arc :: new ( AsyncCounter :: new ( ) ) ;
141162 spawn_detached ( actor_inner (
142163 shared,
@@ -148,8 +169,9 @@ pub fn create_actor(
148169 tx. clone ( ) ,
149170 rx,
150171 active_http_request_count. clone ( ) ,
172+ lost. clone ( ) ,
151173 ) ) ;
152- ( tx, active_http_request_count)
174+ ( tx, lost , active_http_request_count)
153175}
154176
155177#[ tracing:: instrument(
@@ -171,6 +193,7 @@ async fn actor_inner(
171193 tx : mpsc:: UnboundedSender < ToActor > ,
172194 mut rx : mpsc:: UnboundedReceiver < ToActor > ,
173195 active_http_request_count : Arc < AsyncCounter > ,
196+ lost : CancellationToken ,
174197) {
175198 let handle = EnvoyHandle {
176199 shared : shared. clone ( ) ,
@@ -191,6 +214,7 @@ async fn actor_inner(
191214 ws_entries : BufferMap :: new ( ) ,
192215 hibernating_requests,
193216 active_http_request_count,
217+ lost,
194218 } ;
195219 let mut http_request_tasks = JoinSet :: new ( ) ;
196220 let mut pending_stop: Option < PendingStop > = None ;
@@ -200,12 +224,13 @@ async fn actor_inner(
200224 let start_result = shared
201225 . config
202226 . callbacks
203- . on_actor_start (
227+ . on_actor_start_with_lost_signal (
204228 handle. clone ( ) ,
205229 actor_id. clone ( ) ,
206230 generation,
207231 config,
208232 preloaded_kv,
233+ ctx. lost . clone ( ) ,
209234 )
210235 . await ;
211236
@@ -290,11 +315,20 @@ async fn actor_inner(
290315 command_idx,
291316 reason,
292317 } => {
293- if pending_stop. is_some( ) {
294- tracing:: warn!(
295- command_idx,
296- "ignoring duplicate stop while actor teardown is in progress"
297- ) ;
318+ if let Some ( pending) = pending_stop. as_mut( ) {
319+ if matches!( reason, protocol:: StopActorReason :: Lost ) {
320+ tracing:: warn!(
321+ command_idx,
322+ "lost stop escalates actor teardown already in progress"
323+ ) ;
324+ ctx. lost. cancel( ) ;
325+ pending. mark_lost( ) ;
326+ } else {
327+ tracing:: warn!(
328+ command_idx,
329+ "ignoring duplicate stop while actor teardown is in progress"
330+ ) ;
331+ }
298332 continue ;
299333 }
300334 if command_idx <= ctx. command_idx {
@@ -308,20 +342,23 @@ async fn actor_inner(
308342 }
309343 }
310344 ToActor :: Lost => {
311- if pending_stop . is_some ( ) {
312- tracing :: warn! (
313- "ignoring lost signal while actor teardown is in progress"
314- ) ;
345+ ctx . lost . cancel ( ) ;
346+ if let Some ( pending ) = pending_stop . as_mut ( ) {
347+ tracing :: warn! ( " lost signal escalates actor teardown already in progress") ;
348+ pending . mark_lost ( ) ;
315349 continue ;
316350 }
317351
318352 ctx. error = Some ( "actor lost due to timeout" . to_string( ) ) ;
319353
354+ // The engine treats this generation as lost once the envoy has been
355+ // disconnected past the lost threshold, so stop it the same way as an
356+ // engine-sent Lost.
320357 match begin_stop(
321358 & mut ctx,
322359 & handle,
323360 & mut http_request_tasks,
324- protocol:: StopActorReason :: SleepIntent ,
361+ protocol:: StopActorReason :: Lost ,
325362 )
326363 . await
327364 {
@@ -447,10 +484,10 @@ async fn begin_stop(
447484 // `envoy_lost_threshold`.
448485 let ( mut stop_code, mut stop_message) = if let Some ( err) = ctx. error . clone ( ) {
449486 ( protocol:: StopCode :: Error , Some ( err) )
450- } else if matches ! ( reason, protocol:: StopActorReason :: Lost ) {
487+ } else if matches ! ( reason, protocol:: StopActorReason :: Lost ) || ctx . lost . is_cancelled ( ) {
451488 (
452489 protocol:: StopCode :: Error ,
453- Some ( "envoy connection lost" . to_string ( ) ) ,
490+ Some ( LOST_STOP_MESSAGE . to_string ( ) ) ,
454491 )
455492 } else {
456493 ( protocol:: StopCode :: Ok , None )
@@ -540,9 +577,17 @@ fn send_stopped_event_for_result(
540577
541578fn send_stopped_event (
542579 ctx : & mut ActorContext ,
543- stop_code : protocol:: StopCode ,
544- stop_message : Option < String > ,
580+ mut stop_code : protocol:: StopCode ,
581+ mut stop_message : Option < String > ,
545582) {
583+ // The lost token is cancelled synchronously before the Lost command is queued, so a graceful
584+ // stop can complete after the generation was lost but before this task consumed the command.
585+ // Recheck here so a lost generation never reports Stopped(Ok), which the engine treats as a
586+ // destroy.
587+ if ctx. lost . is_cancelled ( ) && matches ! ( stop_code, protocol:: StopCode :: Ok ) {
588+ stop_code = protocol:: StopCode :: Error ;
589+ stop_message = Some ( LOST_STOP_MESSAGE . to_string ( ) ) ;
590+ }
546591 send_event (
547592 ctx,
548593 protocol:: Event :: EventActorStateUpdate ( protocol:: EventActorStateUpdate {
@@ -1717,6 +1762,33 @@ mod tests {
17171762 . expect ( "timed out waiting for tunnel message" )
17181763 }
17191764
1765+ async fn wait_for_stopped_code (
1766+ envoy_rx : & mut mpsc:: UnboundedReceiver < ToEnvoyMessage > ,
1767+ ) -> protocol:: StopCode {
1768+ tokio:: time:: timeout ( Duration :: from_secs ( 2 ) , async {
1769+ loop {
1770+ let Some ( msg) = envoy_rx. recv ( ) . await else {
1771+ panic ! ( "envoy channel closed before stopped event" ) ;
1772+ } ;
1773+ let ToEnvoyMessage :: SendEvents { events } = msg else {
1774+ continue ;
1775+ } ;
1776+ for event in events {
1777+ if let protocol:: Event :: EventActorStateUpdate (
1778+ protocol:: EventActorStateUpdate {
1779+ state : protocol:: ActorState :: ActorStateStopped ( stopped) ,
1780+ } ,
1781+ ) = event. inner
1782+ {
1783+ return stopped. code ;
1784+ }
1785+ }
1786+ }
1787+ } )
1788+ . await
1789+ . expect ( "timed out waiting for stopped event" )
1790+ }
1791+
17201792 pub ( super ) async fn wait_for_stopped_event (
17211793 envoy_rx : & mut mpsc:: UnboundedReceiver < ToEnvoyMessage > ,
17221794 ) {
@@ -1823,7 +1895,7 @@ mod tests {
18231895 release_fetch. clone ( ) ,
18241896 ) ) ;
18251897 let ( shared, mut envoy_rx) = build_shared_context ( callbacks) ;
1826- let ( actor_tx, active_http_request_count) = create_actor (
1898+ let ( actor_tx, _lost , active_http_request_count) = create_actor (
18271899 shared,
18281900 "actor-1" . to_string ( ) ,
18291901 1 ,
@@ -1864,7 +1936,7 @@ mod tests {
18641936 let ( fetch_dropped_tx, fetch_dropped_rx) = oneshot:: channel ( ) ;
18651937 let callbacks = Arc :: new ( TestCallbacks :: hanging ( fetch_started_tx, fetch_dropped_tx) ) ;
18661938 let ( shared, mut envoy_rx) = build_shared_context ( callbacks) ;
1867- let ( actor_tx, active_http_request_count) = create_actor (
1939+ let ( actor_tx, _lost , active_http_request_count) = create_actor (
18681940 shared,
18691941 "actor-2" . to_string ( ) ,
18701942 1 ,
@@ -1909,7 +1981,7 @@ mod tests {
19091981 stop_handle_tx : Mutex :: new ( Some ( stop_handle_tx) ) ,
19101982 } ) ;
19111983 let ( shared, mut envoy_rx) = build_shared_context ( callbacks) ;
1912- let ( actor_tx, _active_http_request_count) = create_actor (
1984+ let ( actor_tx, _lost , _active_http_request_count) = create_actor (
19131985 shared,
19141986 "actor-3" . to_string ( ) ,
19151987 1 ,
@@ -1942,7 +2014,7 @@ mod tests {
19422014 stop_handle_tx : Mutex :: new ( Some ( stop_handle_tx) ) ,
19432015 } ) ;
19442016 let ( shared, mut envoy_rx) = build_shared_context ( callbacks) ;
1945- let ( actor_tx, _active_http_request_count) = create_actor (
2017+ let ( actor_tx, _lost , _active_http_request_count) = create_actor (
19462018 shared,
19472019 "actor-4" . to_string ( ) ,
19482020 1 ,
@@ -1979,6 +2051,112 @@ mod tests {
19792051 assert ! ( stop_handle. complete( ) , "stop handle should complete once" ) ;
19802052 assert_alarm_before_stopped_event ( & mut envoy_rx, Some ( 123 ) ) . await ;
19812053 }
2054+ #[ tokio:: test]
2055+ async fn lost_signal_cancels_the_generation_token ( ) {
2056+ let ( stop_handle_tx, _stop_handle_rx) = oneshot:: channel ( ) ;
2057+ let callbacks = Arc :: new ( DeferredStopCallbacks {
2058+ stop_handle_tx : Mutex :: new ( Some ( stop_handle_tx) ) ,
2059+ } ) ;
2060+ let ( shared, _envoy_rx) = build_shared_context ( callbacks) ;
2061+ let ( actor_tx, lost, _active_http_request_count) = create_actor (
2062+ shared,
2063+ "actor-lost-token" . to_string ( ) ,
2064+ 1 ,
2065+ actor_config ( ) ,
2066+ Vec :: new ( ) ,
2067+ None ,
2068+ ) ;
2069+ assert ! ( !lost. is_cancelled( ) ) ;
2070+
2071+ actor_tx. send ( ToActor :: Lost ) . expect ( "failed to send lost" ) ;
2072+ tokio:: time:: timeout ( Duration :: from_secs ( 2 ) , lost. cancelled ( ) )
2073+ . await
2074+ . expect ( "lost signal should cancel the generation token" ) ;
2075+ }
2076+
2077+ #[ tokio:: test]
2078+ async fn lost_during_graceful_stop_reports_stopped_error ( ) {
2079+ let ( stop_handle_tx, stop_handle_rx) = oneshot:: channel ( ) ;
2080+ let callbacks = Arc :: new ( DeferredStopCallbacks {
2081+ stop_handle_tx : Mutex :: new ( Some ( stop_handle_tx) ) ,
2082+ } ) ;
2083+ let ( shared, mut envoy_rx) = build_shared_context ( callbacks) ;
2084+ let ( actor_tx, lost, _active_http_request_count) = create_actor (
2085+ shared,
2086+ "actor-lost-escalation" . to_string ( ) ,
2087+ 1 ,
2088+ actor_config ( ) ,
2089+ Vec :: new ( ) ,
2090+ None ,
2091+ ) ;
2092+
2093+ actor_tx
2094+ . send ( ToActor :: Stop {
2095+ command_idx : 1 ,
2096+ reason : protocol:: StopActorReason :: SleepIntent ,
2097+ } )
2098+ . expect ( "failed to send sleep stop" ) ;
2099+ let stop_handle = tokio:: time:: timeout ( Duration :: from_secs ( 2 ) , stop_handle_rx)
2100+ . await
2101+ . expect ( "timed out waiting for stop handle" )
2102+ . expect ( "stop handle sender dropped" ) ;
2103+
2104+ // The engine gives up on the generation while its graceful sleep is still running.
2105+ actor_tx
2106+ . send ( ToActor :: Stop {
2107+ command_idx : 2 ,
2108+ reason : protocol:: StopActorReason :: Lost ,
2109+ } )
2110+ . expect ( "failed to send lost stop" ) ;
2111+ tokio:: time:: timeout ( Duration :: from_secs ( 2 ) , lost. cancelled ( ) )
2112+ . await
2113+ . expect ( "lost stop should cancel the generation token" ) ;
2114+
2115+ assert ! ( stop_handle. complete( ) , "stop handle should complete once" ) ;
2116+ assert_eq ! (
2117+ wait_for_stopped_code( & mut envoy_rx) . await ,
2118+ protocol:: StopCode :: Error ,
2119+ "an escalated stop must not report Stopped(Ok), which the engine treats as a destroy"
2120+ ) ;
2121+ }
2122+
2123+ #[ tokio:: test]
2124+ async fn graceful_stop_completing_before_lost_command_is_consumed_reports_error ( ) {
2125+ let ( stop_handle_tx, stop_handle_rx) = oneshot:: channel ( ) ;
2126+ let callbacks = Arc :: new ( DeferredStopCallbacks {
2127+ stop_handle_tx : Mutex :: new ( Some ( stop_handle_tx) ) ,
2128+ } ) ;
2129+ let ( shared, mut envoy_rx) = build_shared_context ( callbacks) ;
2130+ let ( actor_tx, lost, _active_http_request_count) = create_actor (
2131+ shared,
2132+ "actor-lost-race" . to_string ( ) ,
2133+ 1 ,
2134+ actor_config ( ) ,
2135+ Vec :: new ( ) ,
2136+ None ,
2137+ ) ;
2138+
2139+ actor_tx
2140+ . send ( ToActor :: Stop {
2141+ command_idx : 1 ,
2142+ reason : protocol:: StopActorReason :: SleepIntent ,
2143+ } )
2144+ . expect ( "failed to send sleep stop" ) ;
2145+ let stop_handle = tokio:: time:: timeout ( Duration :: from_secs ( 2 ) , stop_handle_rx)
2146+ . await
2147+ . expect ( "timed out waiting for stop handle" )
2148+ . expect ( "stop handle sender dropped" ) ;
2149+
2150+ // The command path cancels the token before queueing Lost. Complete the graceful stop in
2151+ // that window, before the actor task sees the Lost command.
2152+ lost. cancel ( ) ;
2153+ assert ! ( stop_handle. complete( ) , "stop handle should complete once" ) ;
2154+ assert_eq ! (
2155+ wait_for_stopped_code( & mut envoy_rx) . await ,
2156+ protocol:: StopCode :: Error ,
2157+ "a lost generation must not report Stopped(Ok) even if the Lost command was not consumed"
2158+ ) ;
2159+ }
19822160}
19832161
19842162#[ cfg( test) ]
0 commit comments