Skip to main content

rivet_envoy_client/
actor.rs

1use std::{
2	collections::{BTreeMap, HashMap},
3	sync::Arc,
4};
5
6use crate::async_counter::AsyncCounter;
7use rivet_envoy_protocol as protocol;
8use tokio::sync::oneshot::error::TryRecvError;
9use tokio::sync::{mpsc, oneshot};
10use tokio::task::JoinSet;
11use tracing::Instrument;
12
13use crate::connection::ws_send;
14use crate::context::SharedContext;
15use crate::handle::EnvoyHandle;
16use crate::http::HttpRequest;
17#[cfg(test)]
18use crate::http::{HTTP_BODY_STREAM_CHANNEL_CAPACITY, HttpResponse, ResponseChunk};
19use crate::stringify::stringify_to_rivet_tunnel_message_kind;
20use crate::utils::{
21	BufferMap, id_to_str, spawn_detached, wrapping_add_u16, wrapping_lte_u16, wrapping_sub_u16,
22};
23use crate::websocket::{WebSocketHandler, WebSocketMessage, WebSocketSender, WsOutgoing};
24
25mod http;
26
27pub enum ToActor {
28	Intent {
29		intent: protocol::ActorIntent,
30		error: Option<String>,
31	},
32	Stop {
33		command_idx: i64,
34		reason: protocol::StopActorReason,
35	},
36	Lost,
37	SetAlarm {
38		alarm_ts: Option<i64>,
39		ack_tx: Option<oneshot::Sender<()>>,
40	},
41	ReqStart {
42		message_id: protocol::MessageId,
43		req: protocol::ToEnvoyRequestStart,
44		connection_session: u64,
45	},
46	ConnectionClosed {
47		session: u64,
48	},
49	ReqChunk {
50		message_id: protocol::MessageId,
51		chunk: protocol::ToEnvoyRequestChunk,
52	},
53	ReqAbort {
54		message_id: protocol::MessageId,
55		reason: protocol::HttpStreamAbortReason,
56	},
57	ReqProtocolViolation {
58		message_id: protocol::MessageId,
59		detail: String,
60	},
61	ReqBodyCancelled {
62		message_id: protocol::MessageId,
63	},
64	ReqBodyCancel {
65		message_id: protocol::MessageId,
66	},
67	ResponseBodyWindowUpdate {
68		message_id: protocol::MessageId,
69		consumed_bytes: u64,
70	},
71	ReqComplete {
72		message_id: protocol::MessageId,
73	},
74	WsOpen {
75		message_id: protocol::MessageId,
76		path: String,
77		headers: BTreeMap<String, String>,
78	},
79	WsMsg {
80		message_id: protocol::MessageId,
81		msg: protocol::ToEnvoyWebSocketMessage,
82	},
83	WsClose {
84		message_id: protocol::MessageId,
85		close: protocol::ToEnvoyWebSocketClose,
86	},
87	HwsAck {
88		gateway_id: protocol::GatewayId,
89		request_id: protocol::RequestId,
90		envoy_message_index: u16,
91	},
92}
93
94struct WebSocketRequestState {
95	envoy_message_index: u16,
96}
97
98struct WsEntry {
99	is_hibernatable: bool,
100	rivet_message_index: u16,
101	ws_handler: Option<WebSocketHandler>,
102	outgoing_tx: mpsc::UnboundedSender<WsOutgoing>,
103}
104
105struct ActorContext {
106	shared: Arc<SharedContext>,
107	tx: mpsc::UnboundedSender<ToActor>,
108	actor_id: String,
109	generation: u32,
110	command_idx: i64,
111	event_index: i64,
112	error: Option<String>,
113	http_requests: http::HttpRequests,
114	websocket_requests: BufferMap<WebSocketRequestState>,
115	ws_entries: BufferMap<WsEntry>,
116	hibernating_requests: Vec<protocol::HibernatingRequest>,
117	active_http_request_count: Arc<AsyncCounter>,
118}
119
120struct PendingStop {
121	completion_rx: oneshot::Receiver<anyhow::Result<()>>,
122	stop_code: protocol::StopCode,
123	stop_message: Option<String>,
124}
125
126enum StopProgress {
127	Stopped,
128	Pending(PendingStop),
129}
130
131pub fn create_actor(
132	shared: Arc<SharedContext>,
133	actor_id: String,
134	generation: u32,
135	config: protocol::ActorConfig,
136	hibernating_requests: Vec<protocol::HibernatingRequest>,
137	preloaded_kv: Option<protocol::PreloadedKv>,
138) -> (mpsc::UnboundedSender<ToActor>, Arc<AsyncCounter>) {
139	let (tx, rx) = mpsc::unbounded_channel();
140	let active_http_request_count = Arc::new(AsyncCounter::new());
141	spawn_detached(actor_inner(
142		shared,
143		actor_id,
144		generation,
145		config,
146		hibernating_requests,
147		preloaded_kv,
148		tx.clone(),
149		rx,
150		active_http_request_count.clone(),
151	));
152	(tx, active_http_request_count)
153}
154
155#[tracing::instrument(
156	skip_all,
157	fields(
158		envoy_key = %shared.envoy_key,
159		actor_id = %actor_id,
160		generation = generation,
161		actor_key = %config.key.as_deref().unwrap_or(""),
162	),
163)]
164async fn actor_inner(
165	shared: Arc<SharedContext>,
166	actor_id: String,
167	generation: u32,
168	config: protocol::ActorConfig,
169	hibernating_requests: Vec<protocol::HibernatingRequest>,
170	preloaded_kv: Option<protocol::PreloadedKv>,
171	tx: mpsc::UnboundedSender<ToActor>,
172	mut rx: mpsc::UnboundedReceiver<ToActor>,
173	active_http_request_count: Arc<AsyncCounter>,
174) {
175	let handle = EnvoyHandle {
176		shared: shared.clone(),
177		// Fake channel, don't care
178		started_rx: tokio::sync::watch::channel(()).1,
179	};
180
181	let mut ctx = ActorContext {
182		shared: shared.clone(),
183		tx,
184		actor_id: actor_id.clone(),
185		generation,
186		command_idx: 0,
187		event_index: 0,
188		error: None,
189		http_requests: http::HttpRequests::new(),
190		websocket_requests: BufferMap::new(),
191		ws_entries: BufferMap::new(),
192		hibernating_requests,
193		active_http_request_count,
194	};
195	let mut http_request_tasks = JoinSet::new();
196	let mut pending_stop: Option<PendingStop> = None;
197	let mut rx_closed = false;
198
199	// Call on_actor_start
200	let start_result = shared
201		.config
202		.callbacks
203		.on_actor_start(
204			handle.clone(),
205			actor_id.clone(),
206			generation,
207			config,
208			preloaded_kv,
209		)
210		.await;
211
212	if let Err(error) = start_result {
213		let error_chain = error.chain().map(ToString::to_string).collect::<Vec<_>>();
214		tracing::error!(?error, error_chain = ?error_chain, "actor start failed");
215		send_event(
216			&mut ctx,
217			protocol::Event::EventActorStateUpdate(protocol::EventActorStateUpdate {
218				state: protocol::ActorState::ActorStateStopped(protocol::ActorStateStopped {
219					code: protocol::StopCode::Error,
220					message: Some(format!("{error:#}")),
221				}),
222			}),
223		);
224		return;
225	}
226
227	if let Some(meta_entries) = handle.take_pending_hibernation_restore(&actor_id) {
228		if let Err(error) = handle_hws_restore(&mut ctx, &handle, meta_entries).await {
229			tracing::error!(?error, "actor hibernation restore failed");
230			send_event(
231				&mut ctx,
232				protocol::Event::EventActorStateUpdate(protocol::EventActorStateUpdate {
233					state: protocol::ActorState::ActorStateStopped(protocol::ActorStateStopped {
234						code: protocol::StopCode::Error,
235						message: Some(format!("{error:#}")),
236					}),
237				}),
238			);
239			return;
240		}
241	}
242
243	// Send running state
244	send_event(
245		&mut ctx,
246		protocol::Event::EventActorStateUpdate(protocol::EventActorStateUpdate {
247			state: protocol::ActorState::ActorStateRunning,
248		}),
249	);
250
251	loop {
252		tokio::select! {
253			maybe_task = async {
254				if http_request_tasks.is_empty() {
255					std::future::pending().await
256				} else {
257					http_request_tasks.join_next().await
258				}
259			} => {
260				if let Some(result) = maybe_task {
261					http::handle_task_result(result);
262				}
263			}
264			msg = async {
265				if rx_closed {
266					std::future::pending::<Option<ToActor>>().await
267				} else {
268					rx.recv().await
269				}
270			} => {
271				let Some(msg) = msg else {
272					if pending_stop.is_some() {
273						rx_closed = true;
274						continue;
275					}
276					break;
277				};
278
279				match msg {
280					ToActor::Intent { intent, error } => {
281						send_event(
282							&mut ctx,
283							protocol::Event::EventActorIntent(protocol::EventActorIntent { intent }),
284						);
285						if error.is_some() {
286							ctx.error = error;
287						}
288					}
289					ToActor::Stop {
290						command_idx,
291						reason,
292					} => {
293						if pending_stop.is_some() {
294							tracing::warn!(
295								command_idx,
296								"ignoring duplicate stop while actor teardown is in progress"
297							);
298							continue;
299						}
300						if command_idx <= ctx.command_idx {
301							tracing::warn!(command_idx, "ignoring already seen command");
302							continue;
303						}
304						ctx.command_idx = command_idx;
305						match begin_stop(&mut ctx, &handle, &mut http_request_tasks, reason).await {
306							StopProgress::Stopped => break,
307							StopProgress::Pending(stop) => pending_stop = Some(stop),
308						}
309					}
310					ToActor::Lost => {
311						if pending_stop.is_some() {
312							tracing::warn!(
313								"ignoring lost signal while actor teardown is in progress"
314							);
315							continue;
316						}
317
318						ctx.error = Some("actor lost due to timeout".to_string());
319
320						match begin_stop(
321							&mut ctx,
322							&handle,
323							&mut http_request_tasks,
324							protocol::StopActorReason::SleepIntent,
325						)
326						.await
327						{
328							StopProgress::Stopped => break,
329							StopProgress::Pending(stop) => pending_stop = Some(stop),
330						}
331					}
332					ToActor::SetAlarm { alarm_ts, ack_tx } => {
333						send_event(
334							&mut ctx,
335							protocol::Event::EventActorSetAlarm(protocol::EventActorSetAlarm { alarm_ts }),
336						);
337						if let Some(ack_tx) = ack_tx {
338							let _ = ack_tx.send(());
339						}
340					}
341					ToActor::ReqStart { message_id, req, connection_session } => {
342						http::handle_req_start(
343							&mut ctx,
344							&handle,
345							&mut http_request_tasks,
346							message_id,
347							req,
348							connection_session,
349						);
350					}
351					ToActor::ConnectionClosed { session } => {
352						http::handle_connection_closed(&mut ctx, session).await;
353					}
354					ToActor::ReqChunk { message_id, chunk } => {
355						http::handle_req_chunk(&mut ctx, message_id, chunk);
356					}
357					ToActor::ReqAbort { message_id, reason } => {
358						http::handle_req_abort(&mut ctx, message_id, reason);
359					}
360					ToActor::ReqProtocolViolation { message_id, detail } => {
361						http::handle_protocol_violation(&mut ctx, message_id, detail).await;
362					}
363					ToActor::ReqBodyCancelled { message_id } => {
364						http::handle_req_body_cancelled(&mut ctx, message_id);
365					}
366					ToActor::ReqBodyCancel { message_id } => {
367						http::handle_req_body_cancel(&mut ctx, message_id);
368					}
369					ToActor::ResponseBodyWindowUpdate {
370						message_id,
371						consumed_bytes,
372					} => {
373						http::handle_response_body_window_update(
374							&mut ctx,
375							message_id,
376							consumed_bytes,
377						)
378						.await;
379					}
380					ToActor::ReqComplete { message_id } => {
381						http::handle_req_complete(&mut ctx, message_id);
382					}
383					ToActor::WsOpen {
384						message_id,
385						path,
386						headers,
387					} => {
388						handle_ws_open(&mut ctx, &handle, message_id, path, headers).await;
389					}
390					ToActor::WsMsg { message_id, msg } => {
391						handle_ws_message(&mut ctx, message_id, msg).await;
392					}
393					ToActor::WsClose { message_id, close } => {
394						handle_ws_close(&mut ctx, message_id, close).await;
395					}
396					ToActor::HwsAck {
397						gateway_id,
398						request_id,
399						envoy_message_index,
400					} => {
401						handle_hws_ack(&mut ctx, gateway_id, request_id, envoy_message_index).await;
402					}
403				}
404			}
405			stop_result = async {
406				let pending = pending_stop
407					.as_mut()
408					.expect("pending stop must exist when waiting for stop completion");
409				(&mut pending.completion_rx).await
410			}, if pending_stop.is_some() => {
411				let pending = pending_stop
412					.take()
413					.expect("pending stop must exist when stop completion resolves");
414				http::abort_and_join_tasks(&mut ctx, &mut http_request_tasks).await;
415				finalize_stop(&mut ctx, pending, stop_result);
416				break;
417			}
418		}
419	}
420
421	http::abort_and_join_tasks(&mut ctx, &mut http_request_tasks).await;
422	tracing::debug!("envoy actor stopped");
423}
424
425fn send_event(ctx: &mut ActorContext, inner: protocol::Event) {
426	let checkpoint = increment_checkpoint(ctx);
427	let _ = crate::envoy::send_to_envoy_tx(
428		&ctx.shared,
429		crate::envoy::ToEnvoyMessage::SendEvents {
430			events: vec![protocol::EventWrapper { checkpoint, inner }],
431		},
432	);
433}
434
435async fn begin_stop(
436	ctx: &mut ActorContext,
437	handle: &EnvoyHandle,
438	_http_request_tasks: &mut JoinSet<()>,
439	reason: protocol::StopActorReason,
440) -> StopProgress {
441	// A Lost stop must surface as Stopped(Error). The runner side detected its
442	// own WS to pegboard-envoy was unhealthy and gave up on the actor; that is
443	// not a graceful exit. If we emitted Stopped(Ok) here, pegboard's
444	// `handle_stopped` would see Stopped(Ok) from `Transition::Running` (no
445	// prior `ActorIntent` was sent) and take `Decision::Destroy`, wiping the
446	// actor and its KV after every transient WS flap that exceeds
447	// `envoy_lost_threshold`.
448	let (mut stop_code, mut stop_message) = if let Some(err) = ctx.error.clone() {
449		(protocol::StopCode::Error, Some(err))
450	} else if matches!(reason, protocol::StopActorReason::Lost) {
451		(
452			protocol::StopCode::Error,
453			Some("envoy connection lost".to_string()),
454		)
455	} else {
456		(protocol::StopCode::Ok, None)
457	};
458	let (stop_tx, mut stop_rx) = oneshot::channel();
459
460	let stop_result = ctx
461		.shared
462		.config
463		.callbacks
464		.on_actor_stop_with_completion(
465			handle.clone(),
466			ctx.actor_id.clone(),
467			ctx.generation,
468			reason.clone(),
469			crate::callbacks::ActorStopHandle::new(stop_tx),
470		)
471		.await;
472
473	if let Err(error) = stop_result {
474		tracing::error!(?error, "actor stop failed");
475		stop_code = protocol::StopCode::Error;
476		if stop_message.is_none() {
477			stop_message = Some(format!("{error:#}"));
478		}
479		send_stopped_event(ctx, stop_code, stop_message);
480		return StopProgress::Stopped;
481	}
482
483	match stop_rx.try_recv() {
484		Ok(stop_result) => {
485			send_stopped_event_for_result(ctx, stop_code, stop_message, stop_result);
486			StopProgress::Stopped
487		}
488		Err(TryRecvError::Empty) => StopProgress::Pending(PendingStop {
489			completion_rx: stop_rx,
490			stop_code,
491			stop_message,
492		}),
493		Err(TryRecvError::Closed) => {
494			send_stopped_event(ctx, stop_code, stop_message);
495			StopProgress::Stopped
496		}
497	}
498}
499
500fn finalize_stop(
501	ctx: &mut ActorContext,
502	pending: PendingStop,
503	stop_result: Result<anyhow::Result<()>, oneshot::error::RecvError>,
504) {
505	match stop_result {
506		Ok(stop_result) => {
507			send_stopped_event_for_result(
508				ctx,
509				pending.stop_code,
510				pending.stop_message,
511				stop_result,
512			);
513		}
514		Err(error) => {
515			tracing::warn!(
516				?error,
517				"actor stop completion handle dropped before signaling teardown result"
518			);
519			send_stopped_event(ctx, pending.stop_code, pending.stop_message);
520		}
521	}
522}
523
524fn send_stopped_event_for_result(
525	ctx: &mut ActorContext,
526	mut stop_code: protocol::StopCode,
527	mut stop_message: Option<String>,
528	stop_result: anyhow::Result<()>,
529) {
530	if let Err(error) = stop_result {
531		tracing::error!(?error, "actor stop completion failed");
532		stop_code = protocol::StopCode::Error;
533		if stop_message.is_none() {
534			stop_message = Some(format!("{error:#}"));
535		}
536	}
537
538	send_stopped_event(ctx, stop_code, stop_message);
539}
540
541fn send_stopped_event(
542	ctx: &mut ActorContext,
543	stop_code: protocol::StopCode,
544	stop_message: Option<String>,
545) {
546	send_event(
547		ctx,
548		protocol::Event::EventActorStateUpdate(protocol::EventActorStateUpdate {
549			state: protocol::ActorState::ActorStateStopped(protocol::ActorStateStopped {
550				code: stop_code,
551				message: stop_message,
552			}),
553		}),
554	);
555}
556
557fn spawn_ws_outgoing_task(
558	shared: Arc<SharedContext>,
559	gateway_id: protocol::GatewayId,
560	request_id: protocol::RequestId,
561	mut outgoing_rx: mpsc::UnboundedReceiver<WsOutgoing>,
562) {
563	let ws_task = async move {
564		let mut idx: u16 = 0;
565		while let Some(msg) = outgoing_rx.recv().await {
566			idx += 1;
567			match msg {
568				WsOutgoing::Message { data, binary } => {
569					ws_send(
570						&shared,
571						protocol::ToRivet::ToRivetTunnelMessage(protocol::ToRivetTunnelMessage {
572							message_id: protocol::MessageId {
573								gateway_id,
574								request_id,
575								message_index: idx,
576							},
577							message_kind:
578								protocol::ToRivetTunnelMessageKind::ToRivetWebSocketMessage(
579									protocol::ToRivetWebSocketMessage { data, binary },
580								),
581						}),
582					)
583					.await;
584				}
585				WsOutgoing::Flush { tx } => {
586					let _ = tx.send(());
587				}
588				WsOutgoing::Close { code, reason } => {
589					ws_send(
590						&shared,
591						protocol::ToRivet::ToRivetTunnelMessage(protocol::ToRivetTunnelMessage {
592							message_id: protocol::MessageId {
593								gateway_id,
594								request_id,
595								message_index: 0,
596							},
597							message_kind: protocol::ToRivetTunnelMessageKind::ToRivetWebSocketClose(
598								protocol::ToRivetWebSocketClose {
599									code,
600									reason,
601									hibernate: false,
602								},
603							),
604						}),
605					)
606					.await;
607					break;
608				}
609			}
610		}
611	};
612	spawn_detached(ws_task.in_current_span());
613}
614
615async fn handle_ws_open(
616	ctx: &mut ActorContext,
617	handle: &EnvoyHandle,
618	message_id: protocol::MessageId,
619	path: String,
620	headers: BTreeMap<String, String>,
621) {
622	let restored_ws = ctx
623		.ws_entries
624		.remove(&[&message_id.gateway_id, &message_id.request_id]);
625	let is_restoring_hibernatable = restored_ws
626		.as_ref()
627		.map(|ws| ws.is_hibernatable)
628		.unwrap_or(false);
629
630	if !is_restoring_hibernatable {
631		ctx.websocket_requests.insert(
632			&[&message_id.gateway_id, &message_id.request_id],
633			WebSocketRequestState {
634				envoy_message_index: 0,
635			},
636		);
637	}
638
639	let mut full_headers: HashMap<String, String> = headers.into_iter().collect();
640	full_headers.insert("Upgrade".to_string(), "websocket".to_string());
641	full_headers.insert("Connection".to_string(), "Upgrade".to_string());
642
643	let request = HttpRequest {
644		method: "GET".to_string(),
645		path: path.clone(),
646		headers: full_headers.clone(),
647		body: None,
648		body_stream: None,
649	};
650
651	let is_hibernatable = if is_restoring_hibernatable {
652		true
653	} else {
654		match ctx
655			.shared
656			.config
657			.callbacks
658			.can_hibernate(
659				&ctx.actor_id,
660				&message_id.gateway_id,
661				&message_id.request_id,
662				&request,
663			)
664			.await
665		{
666			Ok(is_hibernatable) => is_hibernatable,
667			Err(error) => {
668				tracing::error!(?error, "error checking websocket hibernation");
669
670				send_actor_message(
671					ctx,
672					message_id.gateway_id,
673					message_id.request_id,
674					protocol::ToRivetTunnelMessageKind::ToRivetWebSocketClose(
675						protocol::ToRivetWebSocketClose {
676							code: Some(1011),
677							reason: Some("Server Error".to_string()),
678							hibernate: false,
679						},
680					),
681				)
682				.await;
683
684				ctx.websocket_requests
685					.remove(&[&message_id.gateway_id, &message_id.request_id]);
686				return;
687			}
688		}
689	};
690
691	// Create outgoing channel BEFORE calling websocket() so the sender is available immediately
692	let (outgoing_tx, outgoing_rx) = mpsc::unbounded_channel::<WsOutgoing>();
693	let sender = WebSocketSender {
694		tx: outgoing_tx.clone(),
695	};
696
697	let ws_result = if is_restoring_hibernatable {
698		ctx.shared
699			.config
700			.callbacks
701			.websocket(
702				handle.clone(),
703				ctx.actor_id.clone(),
704				message_id.gateway_id,
705				message_id.request_id,
706				request,
707				path,
708				full_headers,
709				true,
710				true,
711				sender,
712			)
713			.await
714	} else {
715		ctx.shared
716			.config
717			.callbacks
718			.websocket(
719				handle.clone(),
720				ctx.actor_id.clone(),
721				message_id.gateway_id,
722				message_id.request_id,
723				request,
724				path,
725				full_headers,
726				is_hibernatable,
727				false,
728				sender,
729			)
730			.await
731	};
732
733	match ws_result {
734		Ok(ws_handler) => {
735			ctx.ws_entries.insert(
736				&[&message_id.gateway_id, &message_id.request_id],
737				WsEntry {
738					is_hibernatable,
739					rivet_message_index: message_id.message_index,
740					ws_handler: Some(ws_handler),
741					outgoing_tx,
742				},
743			);
744
745			spawn_ws_outgoing_task(
746				ctx.shared.clone(),
747				message_id.gateway_id,
748				message_id.request_id,
749				outgoing_rx,
750			);
751
752			// Gateway wake flows still wait for a websocket-open ack before they
753			// resume forwarding buffered client messages, even when the request is
754			// being restored after actor hibernation.
755			send_actor_message(
756				ctx,
757				message_id.gateway_id,
758				message_id.request_id,
759				protocol::ToRivetTunnelMessageKind::ToRivetWebSocketOpen(
760					protocol::ToRivetWebSocketOpen {
761						can_hibernate: is_hibernatable,
762					},
763				),
764			)
765			.await;
766
767			// Call on_open if provided
768			if let Some(ws) = ctx
769				.ws_entries
770				.get_mut(&[&message_id.gateway_id, &message_id.request_id])
771			{
772				if let Some(handler) = &mut ws.ws_handler {
773					if let Some(on_open) = handler.on_open.take() {
774						let sender = WebSocketSender {
775							tx: ws.outgoing_tx.clone(),
776						};
777
778						on_open(sender).await;
779					}
780				}
781			}
782		}
783		Err(error) => {
784			tracing::error!(?error, "error handling websocket open");
785
786			send_actor_message(
787				ctx,
788				message_id.gateway_id,
789				message_id.request_id,
790				protocol::ToRivetTunnelMessageKind::ToRivetWebSocketClose(
791					protocol::ToRivetWebSocketClose {
792						code: Some(1011),
793						reason: Some("Server Error".to_string()),
794						hibernate: false,
795					},
796				),
797			)
798			.await;
799
800			ctx.websocket_requests
801				.remove(&[&message_id.gateway_id, &message_id.request_id]);
802			ctx.ws_entries
803				.remove(&[&message_id.gateway_id, &message_id.request_id]);
804		}
805	}
806}
807
808async fn handle_ws_message(
809	ctx: &mut ActorContext,
810	message_id: protocol::MessageId,
811	msg: protocol::ToEnvoyWebSocketMessage,
812) {
813	let ws = ctx
814		.ws_entries
815		.get_mut(&[&message_id.gateway_id, &message_id.request_id]);
816
817	if let Some(ws) = ws {
818		// Validate message index for hibernatable websockets
819		if ws.is_hibernatable {
820			let previous_index = ws.rivet_message_index;
821			let received_index = message_id.message_index;
822
823			if wrapping_lte_u16(received_index, previous_index) {
824				tracing::info!(
825					request_id = id_to_str(&message_id.request_id),
826					previous_index,
827					received_index,
828					"received duplicate hibernating websocket message"
829				);
830				return;
831			}
832
833			let expected_index = wrapping_add_u16(previous_index, 1);
834			if received_index != expected_index {
835				tracing::warn!(
836					request_id = id_to_str(&message_id.request_id),
837					previous_index,
838					expected_index,
839					received_index,
840					gap = wrapping_sub_u16(wrapping_sub_u16(received_index, previous_index), 1),
841					"hibernatable websocket message index out of sequence, closing connection"
842				);
843
844				send_actor_message(
845					ctx,
846					message_id.gateway_id,
847					message_id.request_id,
848					protocol::ToRivetTunnelMessageKind::ToRivetWebSocketClose(
849						protocol::ToRivetWebSocketClose {
850							code: Some(1008),
851							reason: Some("ws.message_index_skip".to_string()),
852							hibernate: false,
853						},
854					),
855				)
856				.await;
857				return;
858			}
859
860			ws.rivet_message_index = received_index;
861		}
862
863		if let Some(handler) = &ws.ws_handler {
864			let sender = WebSocketSender {
865				tx: ws.outgoing_tx.clone(),
866			};
867			let ws_msg = WebSocketMessage {
868				data: msg.data,
869				binary: msg.binary,
870				gateway_id: message_id.gateway_id,
871				request_id: message_id.request_id,
872				message_index: message_id.message_index,
873				sender,
874			};
875			(handler.on_message)(ws_msg).await;
876		}
877	} else {
878		tracing::warn!("received message for unknown ws");
879	}
880}
881
882async fn handle_ws_close(
883	ctx: &mut ActorContext,
884	message_id: protocol::MessageId,
885	close: protocol::ToEnvoyWebSocketClose,
886) {
887	let ws = ctx
888		.ws_entries
889		.remove(&[&message_id.gateway_id, &message_id.request_id]);
890
891	if let Some(ws) = ws {
892		if let Some(handler) = &ws.ws_handler {
893			let code = close.code.unwrap_or(1000);
894			let reason = close.reason.unwrap_or_default();
895			(handler.on_close)(code, reason).await;
896		}
897		ctx.websocket_requests
898			.remove(&[&message_id.gateway_id, &message_id.request_id]);
899	} else {
900		tracing::warn!("received close for unknown ws");
901	}
902}
903
904async fn handle_hws_restore(
905	ctx: &mut ActorContext,
906	handle: &EnvoyHandle,
907	meta_entries: Vec<crate::tunnel::HibernatingWebSocketMetadata>,
908) -> anyhow::Result<()> {
909	tracing::debug!(
910		requests = ctx.hibernating_requests.len(),
911		"restoring hibernating requests"
912	);
913
914	let hibernating_requests = std::mem::take(&mut ctx.hibernating_requests);
915
916	for hib_req in &hibernating_requests {
917		let meta = meta_entries.iter().find(|entry| {
918			entry.gateway_id == hib_req.gateway_id && entry.request_id == hib_req.request_id
919		});
920
921		if let Some(meta) = meta {
922			ctx.websocket_requests.insert(
923				&[&hib_req.gateway_id, &hib_req.request_id],
924				WebSocketRequestState {
925					envoy_message_index: meta.envoy_message_index,
926				},
927			);
928
929			let mut full_headers = meta.headers.clone();
930			full_headers.insert("Upgrade".to_string(), "websocket".to_string());
931			full_headers.insert("Connection".to_string(), "Upgrade".to_string());
932
933			let request = HttpRequest {
934				method: "GET".to_string(),
935				path: meta.path.clone(),
936				headers: full_headers.clone(),
937				body: None,
938				body_stream: None,
939			};
940
941			let (hws_outgoing_tx, hws_outgoing_rx) = mpsc::unbounded_channel();
942			let hws_sender = WebSocketSender {
943				tx: hws_outgoing_tx.clone(),
944			};
945
946			let ws_result = ctx
947				.shared
948				.config
949				.callbacks
950				.websocket(
951					handle.clone(),
952					ctx.actor_id.clone(),
953					hib_req.gateway_id,
954					hib_req.request_id,
955					request,
956					meta.path.clone(),
957					full_headers,
958					true,
959					true,
960					hws_sender,
961				)
962				.await;
963
964			match ws_result {
965				Ok(ws_handler) => {
966					ctx.ws_entries.insert(
967						&[&hib_req.gateway_id, &hib_req.request_id],
968						WsEntry {
969							is_hibernatable: true,
970							rivet_message_index: meta.rivet_message_index,
971							ws_handler: Some(ws_handler),
972							outgoing_tx: hws_outgoing_tx,
973						},
974					);
975					if !handle
976						.rebind_hibernating_websocket(
977							ctx.actor_id.clone(),
978							ctx.generation,
979							hib_req.gateway_id,
980							hib_req.request_id,
981						)
982						.await
983					{
984						tracing::warn!(
985							request_id = id_to_str(&hib_req.request_id),
986							"hibernating websocket route disappeared before restore"
987						);
988						ctx.websocket_requests
989							.remove(&[&hib_req.gateway_id, &hib_req.request_id]);
990						ctx.ws_entries
991							.remove(&[&hib_req.gateway_id, &hib_req.request_id]);
992						continue;
993					}
994					// Gateway wake flows wait for the websocket-open ack before
995					// they resume forwarding buffered client messages.
996					send_actor_message(
997						ctx,
998						hib_req.gateway_id,
999						hib_req.request_id,
1000						protocol::ToRivetTunnelMessageKind::ToRivetWebSocketOpen(
1001							protocol::ToRivetWebSocketOpen {
1002								can_hibernate: true,
1003							},
1004						),
1005					)
1006					.await;
1007					// Writes made by the restored callback are already queued in
1008					// `hws_outgoing_rx`. Start forwarding them only after the exact route
1009					// is rebound and the restore Open frame has entered the ordered tunnel.
1010					spawn_ws_outgoing_task(
1011						ctx.shared.clone(),
1012						hib_req.gateway_id,
1013						hib_req.request_id,
1014						hws_outgoing_rx,
1015					);
1016					if let Some(ws) = ctx
1017						.ws_entries
1018						.get_mut(&[&hib_req.gateway_id, &hib_req.request_id])
1019					{
1020						if let Some(handler) = &mut ws.ws_handler {
1021							if let Some(on_open) = handler.on_open.take() {
1022								let sender = WebSocketSender {
1023									tx: ws.outgoing_tx.clone(),
1024								};
1025
1026								on_open(sender).await;
1027							}
1028						}
1029					}
1030					tracing::info!(
1031						request_id = id_to_str(&hib_req.request_id),
1032						"connection successfully restored"
1033					);
1034				}
1035				Err(error) => {
1036					tracing::error!(
1037						request_id = id_to_str(&hib_req.request_id),
1038						?error,
1039						"error creating websocket during restore"
1040					);
1041
1042					send_actor_message(
1043						ctx,
1044						hib_req.gateway_id,
1045						hib_req.request_id,
1046						protocol::ToRivetTunnelMessageKind::ToRivetWebSocketClose(
1047							protocol::ToRivetWebSocketClose {
1048								code: Some(1011),
1049								reason: Some("ws.restore_error".to_string()),
1050								hibernate: false,
1051							},
1052						),
1053					)
1054					.await;
1055
1056					ctx.websocket_requests
1057						.remove(&[&hib_req.gateway_id, &hib_req.request_id]);
1058				}
1059			}
1060		} else {
1061			tracing::warn!(
1062				request_id = id_to_str(&hib_req.request_id),
1063				"closing websocket that is not persisted"
1064			);
1065
1066			send_actor_message(
1067				ctx,
1068				hib_req.gateway_id,
1069				hib_req.request_id,
1070				protocol::ToRivetTunnelMessageKind::ToRivetWebSocketClose(
1071					protocol::ToRivetWebSocketClose {
1072						code: Some(1000),
1073						reason: Some("ws.meta_not_found_during_restore".to_string()),
1074						hibernate: false,
1075					},
1076				),
1077			)
1078			.await;
1079		}
1080	}
1081
1082	// Process loaded but not connected (stale)
1083	for meta in &meta_entries {
1084		let is_connected = hibernating_requests
1085			.iter()
1086			.any(|req| req.gateway_id == meta.gateway_id && req.request_id == meta.request_id);
1087
1088		if !is_connected {
1089			tracing::warn!(
1090				request_id = id_to_str(&meta.request_id),
1091				"removing stale persisted websocket"
1092			);
1093
1094			let full_headers = meta.headers.clone();
1095			let request = HttpRequest {
1096				method: "GET".to_string(),
1097				path: meta.path.clone(),
1098				headers: full_headers.clone(),
1099				body: None,
1100				body_stream: None,
1101			};
1102
1103			let (stale_tx, _) = mpsc::unbounded_channel();
1104			let stale_sender = WebSocketSender { tx: stale_tx };
1105
1106			let ws_result = ctx
1107				.shared
1108				.config
1109				.callbacks
1110				.websocket(
1111					handle.clone(),
1112					ctx.actor_id.clone(),
1113					meta.gateway_id,
1114					meta.request_id,
1115					request,
1116					meta.path.clone(),
1117					full_headers,
1118					true,
1119					true,
1120					stale_sender,
1121				)
1122				.await;
1123
1124			if let Ok(handler) = ws_result {
1125				(handler.on_close)(1000, "ws.stale_metadata".to_string()).await;
1126			}
1127		}
1128	}
1129
1130	ctx.hibernating_requests = hibernating_requests;
1131	tracing::info!("restored hibernatable websockets");
1132	Ok(())
1133}
1134
1135async fn handle_hws_ack(
1136	ctx: &mut ActorContext,
1137	gateway_id: protocol::GatewayId,
1138	request_id: protocol::RequestId,
1139	envoy_message_index: u16,
1140) {
1141	tracing::debug!(
1142		request_id = id_to_str(&request_id),
1143		index = envoy_message_index,
1144		"ack ws msg"
1145	);
1146
1147	send_actor_message(
1148		ctx,
1149		gateway_id,
1150		request_id,
1151		protocol::ToRivetTunnelMessageKind::ToRivetWebSocketMessageAck(
1152			protocol::ToRivetWebSocketMessageAck {
1153				index: envoy_message_index,
1154			},
1155		),
1156	)
1157	.await;
1158}
1159
1160fn increment_checkpoint(ctx: &mut ActorContext) -> protocol::ActorCheckpoint {
1161	let index = ctx.event_index;
1162	ctx.event_index += 1;
1163	protocol::ActorCheckpoint {
1164		actor_id: ctx.actor_id.clone(),
1165		generation: ctx.generation,
1166		index,
1167	}
1168}
1169
1170async fn send_actor_message(
1171	ctx: &mut ActorContext,
1172	gateway_id: protocol::GatewayId,
1173	request_id: protocol::RequestId,
1174	message_kind: protocol::ToRivetTunnelMessageKind,
1175) {
1176	let req = ctx.websocket_requests.get_mut(&[&gateway_id, &request_id]);
1177	let envoy_message_index = if let Some(req) = req {
1178		let idx = req.envoy_message_index;
1179		req.envoy_message_index += 1;
1180		idx
1181	} else {
1182		tracing::warn!(
1183			gateway_id = id_to_str(&gateway_id),
1184			request_id = id_to_str(&request_id),
1185			"missing pending request for send message"
1186		);
1187		return;
1188	};
1189
1190	let msg = protocol::ToRivetTunnelMessage {
1191		message_id: protocol::MessageId {
1192			gateway_id,
1193			request_id,
1194			message_index: envoy_message_index,
1195		},
1196		message_kind: message_kind.clone(),
1197	};
1198
1199	let buffer_msg = msg.clone();
1200	let failed = ws_send(&ctx.shared, protocol::ToRivet::ToRivetTunnelMessage(msg)).await;
1201
1202	if failed {
1203		if tracing::enabled!(tracing::Level::DEBUG) {
1204			tracing::debug!(
1205				request_id = id_to_str(&request_id),
1206				message = stringify_to_rivet_tunnel_message_kind(&message_kind),
1207				"buffering tunnel message, socket not connected to engine"
1208			);
1209		}
1210		let _ = crate::envoy::send_to_envoy_tx(
1211			&ctx.shared,
1212			crate::envoy::ToEnvoyMessage::SendOrBufferTunnelMsg { msg: buffer_msg },
1213		);
1214	}
1215}
1216
1217#[cfg(test)]
1218mod tests {
1219	use std::collections::HashMap;
1220	use std::future::pending;
1221	use std::sync::Mutex;
1222	use std::sync::atomic::{AtomicBool, Ordering};
1223	use std::time::{Duration, Instant};
1224
1225	use tokio::sync::Notify;
1226	use tokio::sync::oneshot;
1227	use vbare::OwnedVersionedData;
1228
1229	use super::*;
1230	use crate::config::{BoxFuture, EnvoyCallbacks, WebSocketHandler, WebSocketSender};
1231	use crate::context::WsTxMessage;
1232	use crate::envoy::ToEnvoyMessage;
1233
1234	struct DropSignal(Option<oneshot::Sender<()>>);
1235
1236	impl Drop for DropSignal {
1237		fn drop(&mut self) {
1238			if let Some(tx) = self.0.take() {
1239				let _ = tx.send(());
1240			}
1241		}
1242	}
1243
1244	pub(super) struct TestCallbacks {
1245		fetch_started_tx: Mutex<Option<oneshot::Sender<()>>>,
1246		fetch_dropped_tx: Mutex<Option<oneshot::Sender<()>>>,
1247		release_fetch: Arc<Notify>,
1248		complete_fetch: AtomicBool,
1249	}
1250
1251	impl TestCallbacks {
1252		pub(super) fn idle() -> Self {
1253			Self {
1254				fetch_started_tx: Mutex::new(None),
1255				fetch_dropped_tx: Mutex::new(None),
1256				release_fetch: Arc::new(Notify::new()),
1257				complete_fetch: AtomicBool::new(true),
1258			}
1259		}
1260
1261		fn completing(fetch_started_tx: oneshot::Sender<()>, release_fetch: Arc<Notify>) -> Self {
1262			Self {
1263				fetch_started_tx: Mutex::new(Some(fetch_started_tx)),
1264				fetch_dropped_tx: Mutex::new(None),
1265				release_fetch,
1266				complete_fetch: AtomicBool::new(true),
1267			}
1268		}
1269
1270		pub(super) fn hanging(
1271			fetch_started_tx: oneshot::Sender<()>,
1272			fetch_dropped_tx: oneshot::Sender<()>,
1273		) -> Self {
1274			Self {
1275				fetch_started_tx: Mutex::new(Some(fetch_started_tx)),
1276				fetch_dropped_tx: Mutex::new(Some(fetch_dropped_tx)),
1277				release_fetch: Arc::new(Notify::new()),
1278				complete_fetch: AtomicBool::new(false),
1279			}
1280		}
1281	}
1282
1283	struct DeferredStopCallbacks {
1284		stop_handle_tx: Mutex<Option<oneshot::Sender<crate::config::ActorStopHandle>>>,
1285	}
1286
1287	pub(super) struct StreamingCallbacks {
1288		pub(super) body_tx: Mutex<Option<oneshot::Sender<mpsc::Sender<ResponseChunk>>>>,
1289	}
1290
1291	pub(super) struct StreamingRequestCallbacks {
1292		pub(super) request_tx: Mutex<Option<oneshot::Sender<HttpRequest>>>,
1293	}
1294
1295	impl EnvoyCallbacks for TestCallbacks {
1296		fn on_actor_start(
1297			&self,
1298			_handle: EnvoyHandle,
1299			_actor_id: String,
1300			_generation: u32,
1301			_config: protocol::ActorConfig,
1302			_preloaded_kv: Option<protocol::PreloadedKv>,
1303		) -> BoxFuture<anyhow::Result<()>> {
1304			Box::pin(async { Ok(()) })
1305		}
1306
1307		fn on_actor_stop(
1308			&self,
1309			_handle: EnvoyHandle,
1310			_actor_id: String,
1311			_generation: u32,
1312			_reason: protocol::StopActorReason,
1313		) -> BoxFuture<anyhow::Result<()>> {
1314			Box::pin(async { Ok(()) })
1315		}
1316
1317		fn on_shutdown(&self) {}
1318
1319		fn fetch(
1320			&self,
1321			_handle: EnvoyHandle,
1322			_actor_id: String,
1323			_gateway_id: protocol::GatewayId,
1324			_request_id: protocol::RequestId,
1325			request: HttpRequest,
1326		) -> BoxFuture<anyhow::Result<HttpResponse>> {
1327			let fetch_started_tx = self
1328				.fetch_started_tx
1329				.lock()
1330				.expect("fetch_started mutex poisoned")
1331				.take();
1332			let fetch_dropped_tx = self
1333				.fetch_dropped_tx
1334				.lock()
1335				.expect("fetch_dropped mutex poisoned")
1336				.take();
1337			let release_fetch = self.release_fetch.clone();
1338			let complete_fetch = self.complete_fetch.load(Ordering::Acquire);
1339
1340			Box::pin(async move {
1341				let _request = request;
1342				if let Some(tx) = fetch_started_tx {
1343					let _ = tx.send(());
1344				}
1345
1346				let _drop_signal = DropSignal(fetch_dropped_tx);
1347
1348				if complete_fetch {
1349					release_fetch.notified().await;
1350					Ok(HttpResponse {
1351						status: 200,
1352						headers: HashMap::new(),
1353						body: Some(Vec::new()),
1354						body_stream: None,
1355					})
1356				} else {
1357					pending::<()>().await;
1358					unreachable!("pending future should never resolve");
1359				}
1360			})
1361		}
1362
1363		fn websocket(
1364			&self,
1365			_handle: EnvoyHandle,
1366			_actor_id: String,
1367			_gateway_id: protocol::GatewayId,
1368			_request_id: protocol::RequestId,
1369			_request: HttpRequest,
1370			_path: String,
1371			_headers: HashMap<String, String>,
1372			_is_hibernatable: bool,
1373			_is_restoring_hibernatable: bool,
1374			_sender: WebSocketSender,
1375		) -> BoxFuture<anyhow::Result<WebSocketHandler>> {
1376			Box::pin(async {
1377				Ok(WebSocketHandler {
1378					on_message: Box::new(|_| Box::pin(async {})),
1379					on_close: Box::new(|_, _| Box::pin(async {})),
1380					on_open: None,
1381				})
1382			})
1383		}
1384
1385		fn can_hibernate(
1386			&self,
1387			_actor_id: &str,
1388			_gateway_id: &protocol::GatewayId,
1389			_request_id: &protocol::RequestId,
1390			_request: &HttpRequest,
1391		) -> BoxFuture<anyhow::Result<bool>> {
1392			Box::pin(async { Ok(false) })
1393		}
1394	}
1395
1396	impl EnvoyCallbacks for StreamingRequestCallbacks {
1397		fn on_actor_start(
1398			&self,
1399			_handle: EnvoyHandle,
1400			_actor_id: String,
1401			_generation: u32,
1402			_config: protocol::ActorConfig,
1403			_preloaded_kv: Option<protocol::PreloadedKv>,
1404		) -> BoxFuture<anyhow::Result<()>> {
1405			Box::pin(async { Ok(()) })
1406		}
1407
1408		fn on_shutdown(&self) {}
1409
1410		fn fetch(
1411			&self,
1412			_handle: EnvoyHandle,
1413			_actor_id: String,
1414			_gateway_id: protocol::GatewayId,
1415			_request_id: protocol::RequestId,
1416			request: HttpRequest,
1417		) -> BoxFuture<anyhow::Result<HttpResponse>> {
1418			let request_tx = self
1419				.request_tx
1420				.lock()
1421				.expect("streaming request mutex poisoned")
1422				.take();
1423			Box::pin(async move {
1424				let Some(request_tx) = request_tx else {
1425					anyhow::bail!("streaming request sender missing");
1426				};
1427				request_tx
1428					.send(request)
1429					.map_err(|_| anyhow::anyhow!("streaming request receiver dropped"))?;
1430				pending::<()>().await;
1431				unreachable!("pending request callback should not resolve")
1432			})
1433		}
1434
1435		fn websocket(
1436			&self,
1437			_handle: EnvoyHandle,
1438			_actor_id: String,
1439			_gateway_id: protocol::GatewayId,
1440			_request_id: protocol::RequestId,
1441			_request: HttpRequest,
1442			_path: String,
1443			_headers: HashMap<String, String>,
1444			_is_hibernatable: bool,
1445			_is_restoring_hibernatable: bool,
1446			_sender: WebSocketSender,
1447		) -> BoxFuture<anyhow::Result<WebSocketHandler>> {
1448			Box::pin(async { anyhow::bail!("websocket should not be called in HTTP stream test") })
1449		}
1450
1451		fn can_hibernate(
1452			&self,
1453			_actor_id: &str,
1454			_gateway_id: &protocol::GatewayId,
1455			_request_id: &protocol::RequestId,
1456			_request: &HttpRequest,
1457		) -> BoxFuture<anyhow::Result<bool>> {
1458			Box::pin(async { Ok(false) })
1459		}
1460	}
1461
1462	impl EnvoyCallbacks for DeferredStopCallbacks {
1463		fn on_actor_start(
1464			&self,
1465			_handle: EnvoyHandle,
1466			_actor_id: String,
1467			_generation: u32,
1468			_config: protocol::ActorConfig,
1469			_preloaded_kv: Option<protocol::PreloadedKv>,
1470		) -> BoxFuture<anyhow::Result<()>> {
1471			Box::pin(async { Ok(()) })
1472		}
1473
1474		fn on_actor_stop_with_completion(
1475			&self,
1476			_handle: EnvoyHandle,
1477			_actor_id: String,
1478			_generation: u32,
1479			_reason: protocol::StopActorReason,
1480			stop_handle: crate::config::ActorStopHandle,
1481		) -> BoxFuture<anyhow::Result<()>> {
1482			let stop_handle_tx = self
1483				.stop_handle_tx
1484				.lock()
1485				.expect("stop handle mutex poisoned")
1486				.take();
1487
1488			Box::pin(async move {
1489				let Some(tx) = stop_handle_tx else {
1490					anyhow::bail!("stop handle sender missing");
1491				};
1492
1493				tx.send(stop_handle)
1494					.map_err(|_| anyhow::anyhow!("failed to publish stop handle"))?;
1495				Ok(())
1496			})
1497		}
1498
1499		fn on_shutdown(&self) {}
1500
1501		fn fetch(
1502			&self,
1503			_handle: EnvoyHandle,
1504			_actor_id: String,
1505			_gateway_id: protocol::GatewayId,
1506			_request_id: protocol::RequestId,
1507			_request: HttpRequest,
1508		) -> BoxFuture<anyhow::Result<HttpResponse>> {
1509			Box::pin(async { anyhow::bail!("fetch should not be called in deferred stop test") })
1510		}
1511
1512		fn websocket(
1513			&self,
1514			_handle: EnvoyHandle,
1515			_actor_id: String,
1516			_gateway_id: protocol::GatewayId,
1517			_request_id: protocol::RequestId,
1518			_request: HttpRequest,
1519			_path: String,
1520			_headers: HashMap<String, String>,
1521			_is_hibernatable: bool,
1522			_is_restoring_hibernatable: bool,
1523			_sender: WebSocketSender,
1524		) -> BoxFuture<anyhow::Result<WebSocketHandler>> {
1525			Box::pin(async {
1526				anyhow::bail!("websocket should not be called in deferred stop test")
1527			})
1528		}
1529
1530		fn can_hibernate(
1531			&self,
1532			_actor_id: &str,
1533			_gateway_id: &protocol::GatewayId,
1534			_request_id: &protocol::RequestId,
1535			_request: &HttpRequest,
1536		) -> BoxFuture<anyhow::Result<bool>> {
1537			Box::pin(async { Ok(false) })
1538		}
1539	}
1540
1541	impl EnvoyCallbacks for StreamingCallbacks {
1542		fn on_actor_start(
1543			&self,
1544			_handle: EnvoyHandle,
1545			_actor_id: String,
1546			_generation: u32,
1547			_config: protocol::ActorConfig,
1548			_preloaded_kv: Option<protocol::PreloadedKv>,
1549		) -> BoxFuture<anyhow::Result<()>> {
1550			Box::pin(async { Ok(()) })
1551		}
1552
1553		fn on_actor_stop(
1554			&self,
1555			_handle: EnvoyHandle,
1556			_actor_id: String,
1557			_generation: u32,
1558			_reason: protocol::StopActorReason,
1559		) -> BoxFuture<anyhow::Result<()>> {
1560			Box::pin(async { Ok(()) })
1561		}
1562
1563		fn on_shutdown(&self) {}
1564
1565		fn fetch(
1566			&self,
1567			_handle: EnvoyHandle,
1568			_actor_id: String,
1569			_gateway_id: protocol::GatewayId,
1570			_request_id: protocol::RequestId,
1571			_request: HttpRequest,
1572		) -> BoxFuture<anyhow::Result<HttpResponse>> {
1573			let body_tx = self
1574				.body_tx
1575				.lock()
1576				.expect("streaming body mutex poisoned")
1577				.take();
1578
1579			Box::pin(async move {
1580				let (tx, rx) = mpsc::channel(HTTP_BODY_STREAM_CHANNEL_CAPACITY);
1581				if let Some(body_tx) = body_tx {
1582					let _ = body_tx.send(tx);
1583				}
1584				Ok(HttpResponse {
1585					status: 200,
1586					headers: HashMap::new(),
1587					body: None,
1588					body_stream: Some(rx.into()),
1589				})
1590			})
1591		}
1592
1593		fn websocket(
1594			&self,
1595			_handle: EnvoyHandle,
1596			_actor_id: String,
1597			_gateway_id: protocol::GatewayId,
1598			_request_id: protocol::RequestId,
1599			_request: HttpRequest,
1600			_path: String,
1601			_headers: HashMap<String, String>,
1602			_is_hibernatable: bool,
1603			_is_restoring_hibernatable: bool,
1604			_sender: WebSocketSender,
1605		) -> BoxFuture<anyhow::Result<WebSocketHandler>> {
1606			Box::pin(async { anyhow::bail!("websocket should not be called in streaming test") })
1607		}
1608
1609		fn can_hibernate(
1610			&self,
1611			_actor_id: &str,
1612			_gateway_id: &protocol::GatewayId,
1613			_request_id: &protocol::RequestId,
1614			_request: &HttpRequest,
1615		) -> BoxFuture<anyhow::Result<bool>> {
1616			Box::pin(async { Ok(false) })
1617		}
1618	}
1619
1620	pub(super) fn build_shared_context(
1621		callbacks: Arc<dyn EnvoyCallbacks>,
1622	) -> (Arc<SharedContext>, mpsc::UnboundedReceiver<ToEnvoyMessage>) {
1623		let (envoy_tx, envoy_rx) = mpsc::unbounded_channel();
1624		let shared = Arc::new(SharedContext {
1625			config: crate::config::EnvoyConfig {
1626				version: 1,
1627				endpoint: "http://127.0.0.1:1".to_string(),
1628				token: None,
1629				namespace: "test".to_string(),
1630				pool_name: "test".to_string(),
1631				prepopulate_actor_names: HashMap::new(),
1632				metadata: None,
1633				not_global: true,
1634				debug_latency_ms: None,
1635				callbacks,
1636			},
1637			envoy_key: "test-envoy".to_string(),
1638			envoy_tx,
1639			actors: Arc::new(std::sync::Mutex::new(HashMap::new())),
1640			actors_notify: Arc::new(tokio::sync::Notify::new()),
1641			live_tunnel_requests: Arc::new(std::sync::Mutex::new(HashMap::new())),
1642			pending_hibernation_restores: Arc::new(std::sync::Mutex::new(HashMap::new())),
1643			ws_tx: Arc::new(tokio::sync::Mutex::new(
1644				None::<mpsc::UnboundedSender<WsTxMessage>>,
1645			)),
1646			http_ws_tx: Arc::new(tokio::sync::Mutex::new(None)),
1647			connection_session: std::sync::atomic::AtomicU64::new(0),
1648			next_connection_session: std::sync::atomic::AtomicU64::new(0),
1649			connection_session_tx: tokio::sync::watch::channel(0).0,
1650			protocol_metadata: Arc::new(tokio::sync::Mutex::new(None)),
1651			shutting_down: std::sync::atomic::AtomicBool::new(false),
1652			last_ping_ts: std::sync::atomic::AtomicI64::new(0),
1653			stopped_tx: tokio::sync::watch::channel(true).0,
1654		});
1655		(shared, envoy_rx)
1656	}
1657
1658	pub(super) fn actor_config() -> protocol::ActorConfig {
1659		protocol::ActorConfig {
1660			name: "test".to_string(),
1661			key: Some("test-key".to_string()),
1662			create_ts: 0,
1663			input: None,
1664		}
1665	}
1666
1667	pub(super) fn request_start() -> protocol::ToEnvoyRequestStart {
1668		protocol::ToEnvoyRequestStart {
1669			actor_id: "test-actor".to_string(),
1670			actor_generation: Some(1),
1671			method: "GET".to_string(),
1672			path: "/test".to_string(),
1673			headers: HashMap::new(),
1674			body: None,
1675			stream: false,
1676			response_stream: true,
1677		}
1678	}
1679
1680	pub(super) fn message_id() -> protocol::MessageId {
1681		protocol::MessageId {
1682			gateway_id: [1, 2, 3, 4],
1683			request_id: [5, 6, 7, 8],
1684			message_index: 0,
1685		}
1686	}
1687
1688	pub(super) async fn wait_for_zero(active_http_request_count: &Arc<AsyncCounter>) {
1689		assert!(
1690			active_http_request_count
1691				.wait_zero(Instant::now() + Duration::from_secs(2))
1692				.await,
1693			"timed out waiting for active HTTP request count to reach zero"
1694		);
1695	}
1696
1697	pub(super) async fn recv_ws_tunnel_msg(
1698		ws_rx: &mut mpsc::UnboundedReceiver<WsTxMessage>,
1699	) -> protocol::ToRivetTunnelMessage {
1700		tokio::time::timeout(Duration::from_secs(2), async {
1701			loop {
1702				let Some(msg) = ws_rx.recv().await else {
1703					panic!("websocket channel closed before tunnel message");
1704				};
1705				let WsTxMessage::Send(bytes) = msg else {
1706					continue;
1707				};
1708				let message =
1709					protocol::versioned::ToRivet::deserialize(&bytes, protocol::PROTOCOL_VERSION)
1710						.expect("failed to decode ToRivet message");
1711				if let protocol::ToRivet::ToRivetTunnelMessage(msg) = message {
1712					return msg;
1713				}
1714			}
1715		})
1716		.await
1717		.expect("timed out waiting for tunnel message")
1718	}
1719
1720	pub(super) async fn wait_for_stopped_event(
1721		envoy_rx: &mut mpsc::UnboundedReceiver<ToEnvoyMessage>,
1722	) {
1723		tokio::time::timeout(Duration::from_secs(2), async {
1724			loop {
1725				let Some(msg) = envoy_rx.recv().await else {
1726					panic!("envoy channel closed before stopped event");
1727				};
1728
1729				if let ToEnvoyMessage::SendEvents { events } = msg {
1730					if events.iter().any(|event| {
1731						matches!(
1732							event.inner,
1733							protocol::Event::EventActorStateUpdate(
1734								protocol::EventActorStateUpdate {
1735									state: protocol::ActorState::ActorStateStopped(_),
1736								}
1737							)
1738						)
1739					}) {
1740						return;
1741					}
1742				}
1743			}
1744		})
1745		.await
1746		.expect("timed out waiting for stopped event");
1747	}
1748
1749	async fn assert_alarm_before_stopped_event(
1750		envoy_rx: &mut mpsc::UnboundedReceiver<ToEnvoyMessage>,
1751		expected_alarm_ts: Option<i64>,
1752	) {
1753		tokio::time::timeout(Duration::from_secs(2), async {
1754			let mut saw_alarm = false;
1755			loop {
1756				let Some(msg) = envoy_rx.recv().await else {
1757					panic!("envoy channel closed before stopped event");
1758				};
1759
1760				if let ToEnvoyMessage::SendEvents { events } = msg {
1761					for event in events {
1762						match event.inner {
1763							protocol::Event::EventActorSetAlarm(alarm) => {
1764								if alarm.alarm_ts == expected_alarm_ts {
1765									saw_alarm = true;
1766								}
1767							}
1768							protocol::Event::EventActorStateUpdate(
1769								protocol::EventActorStateUpdate {
1770									state: protocol::ActorState::ActorStateStopped(_),
1771								},
1772							) => {
1773								assert!(saw_alarm, "stopped event arrived before alarm update");
1774								return;
1775							}
1776							_ => {}
1777						}
1778					}
1779				}
1780			}
1781		})
1782		.await
1783		.expect("timed out waiting for stopped event");
1784	}
1785
1786	async fn assert_no_stopped_event(envoy_rx: &mut mpsc::UnboundedReceiver<ToEnvoyMessage>) {
1787		let result = tokio::time::timeout(Duration::from_millis(100), async {
1788			loop {
1789				let Some(msg) = envoy_rx.recv().await else {
1790					panic!("envoy channel closed while waiting for non-stopped event");
1791				};
1792
1793				if let ToEnvoyMessage::SendEvents { events } = msg {
1794					if events.iter().any(|event| {
1795						matches!(
1796							event.inner,
1797							protocol::Event::EventActorStateUpdate(
1798								protocol::EventActorStateUpdate {
1799									state: protocol::ActorState::ActorStateStopped(_),
1800								}
1801							)
1802						)
1803					}) {
1804						panic!("received stopped event before teardown completion");
1805					}
1806				}
1807			}
1808		})
1809		.await;
1810
1811		assert!(
1812			result.is_err(),
1813			"stopped event arrived before teardown completion"
1814		);
1815	}
1816
1817	#[tokio::test]
1818	async fn active_http_request_count_tracks_in_flight_fetches() {
1819		let (fetch_started_tx, fetch_started_rx) = oneshot::channel();
1820		let release_fetch = Arc::new(Notify::new());
1821		let callbacks = Arc::new(TestCallbacks::completing(
1822			fetch_started_tx,
1823			release_fetch.clone(),
1824		));
1825		let (shared, mut envoy_rx) = build_shared_context(callbacks);
1826		let (actor_tx, active_http_request_count) = create_actor(
1827			shared,
1828			"actor-1".to_string(),
1829			1,
1830			actor_config(),
1831			Vec::new(),
1832			None,
1833		);
1834
1835		actor_tx
1836			.send(ToActor::ReqStart {
1837				message_id: message_id(),
1838				req: request_start(),
1839				connection_session: 1,
1840			})
1841			.expect("failed to send request start");
1842
1843		tokio::time::timeout(Duration::from_secs(2), fetch_started_rx)
1844			.await
1845			.expect("timed out waiting for fetch start")
1846			.expect("fetch start sender dropped");
1847		assert_eq!(active_http_request_count.load(), 1);
1848
1849		release_fetch.notify_waiters();
1850		wait_for_zero(&active_http_request_count).await;
1851
1852		actor_tx
1853			.send(ToActor::Stop {
1854				command_idx: 1,
1855				reason: protocol::StopActorReason::StopIntent,
1856			})
1857			.expect("failed to send stop");
1858		wait_for_stopped_event(&mut envoy_rx).await;
1859	}
1860
1861	#[tokio::test]
1862	async fn actor_stop_aborts_in_flight_http_requests_before_stopped_event() {
1863		let (fetch_started_tx, fetch_started_rx) = oneshot::channel();
1864		let (fetch_dropped_tx, fetch_dropped_rx) = oneshot::channel();
1865		let callbacks = Arc::new(TestCallbacks::hanging(fetch_started_tx, fetch_dropped_tx));
1866		let (shared, mut envoy_rx) = build_shared_context(callbacks);
1867		let (actor_tx, active_http_request_count) = create_actor(
1868			shared,
1869			"actor-2".to_string(),
1870			1,
1871			actor_config(),
1872			Vec::new(),
1873			None,
1874		);
1875
1876		actor_tx
1877			.send(ToActor::ReqStart {
1878				message_id: message_id(),
1879				req: request_start(),
1880				connection_session: 1,
1881			})
1882			.expect("failed to send request start");
1883
1884		tokio::time::timeout(Duration::from_secs(2), fetch_started_rx)
1885			.await
1886			.expect("timed out waiting for fetch start")
1887			.expect("fetch start sender dropped");
1888		assert_eq!(active_http_request_count.load(), 1);
1889
1890		actor_tx
1891			.send(ToActor::Stop {
1892				command_idx: 1,
1893				reason: protocol::StopActorReason::StopIntent,
1894			})
1895			.expect("failed to send stop");
1896
1897		tokio::time::timeout(Duration::from_secs(2), fetch_dropped_rx)
1898			.await
1899			.expect("timed out waiting for fetch abort")
1900			.expect("fetch drop sender dropped");
1901		wait_for_stopped_event(&mut envoy_rx).await;
1902		assert_eq!(active_http_request_count.load(), 0);
1903	}
1904
1905	#[tokio::test]
1906	async fn actor_stop_waits_for_completion_handle_before_stopped_event() {
1907		let (stop_handle_tx, stop_handle_rx) = oneshot::channel();
1908		let callbacks = Arc::new(DeferredStopCallbacks {
1909			stop_handle_tx: Mutex::new(Some(stop_handle_tx)),
1910		});
1911		let (shared, mut envoy_rx) = build_shared_context(callbacks);
1912		let (actor_tx, _active_http_request_count) = create_actor(
1913			shared,
1914			"actor-3".to_string(),
1915			1,
1916			actor_config(),
1917			Vec::new(),
1918			None,
1919		);
1920
1921		actor_tx
1922			.send(ToActor::Stop {
1923				command_idx: 1,
1924				reason: protocol::StopActorReason::StopIntent,
1925			})
1926			.expect("failed to send stop");
1927
1928		let stop_handle = tokio::time::timeout(Duration::from_secs(2), stop_handle_rx)
1929			.await
1930			.expect("timed out waiting for stop handle")
1931			.expect("stop handle sender dropped");
1932		assert_no_stopped_event(&mut envoy_rx).await;
1933
1934		assert!(stop_handle.complete(), "stop handle should complete once");
1935		wait_for_stopped_event(&mut envoy_rx).await;
1936	}
1937
1938	#[tokio::test]
1939	async fn actor_stop_flushes_acknowledged_alarm_before_completion() {
1940		let (stop_handle_tx, stop_handle_rx) = oneshot::channel();
1941		let callbacks = Arc::new(DeferredStopCallbacks {
1942			stop_handle_tx: Mutex::new(Some(stop_handle_tx)),
1943		});
1944		let (shared, mut envoy_rx) = build_shared_context(callbacks);
1945		let (actor_tx, _active_http_request_count) = create_actor(
1946			shared,
1947			"actor-4".to_string(),
1948			1,
1949			actor_config(),
1950			Vec::new(),
1951			None,
1952		);
1953
1954		actor_tx
1955			.send(ToActor::Stop {
1956				command_idx: 1,
1957				reason: protocol::StopActorReason::StopIntent,
1958			})
1959			.expect("failed to send stop");
1960
1961		let stop_handle = tokio::time::timeout(Duration::from_secs(2), stop_handle_rx)
1962			.await
1963			.expect("timed out waiting for stop handle")
1964			.expect("stop handle sender dropped");
1965
1966		let (alarm_ack_tx, alarm_ack_rx) = oneshot::channel();
1967		actor_tx
1968			.send(ToActor::SetAlarm {
1969				alarm_ts: Some(123),
1970				ack_tx: Some(alarm_ack_tx),
1971			})
1972			.expect("failed to send alarm");
1973
1974		tokio::time::timeout(Duration::from_secs(2), alarm_ack_rx)
1975			.await
1976			.expect("timed out waiting for alarm ack")
1977			.expect("alarm ack sender dropped");
1978
1979		assert!(stop_handle.complete(), "stop handle should complete once");
1980		assert_alarm_before_stopped_event(&mut envoy_rx, Some(123)).await;
1981	}
1982}
1983
1984#[cfg(test)]
1985#[path = "../tests/support/actor_http_stream.rs"]
1986mod http_stream_tests;