Skip to main content

rivet_envoy_client/
envoy.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3#[cfg(not(target_arch = "wasm32"))]
4use std::sync::OnceLock;
5use std::sync::atomic::Ordering;
6
7#[cfg(not(target_arch = "wasm32"))]
8use parking_lot::Mutex;
9
10use crate::async_counter::AsyncCounter;
11use rivet_envoy_protocol as protocol;
12use tokio::sync::mpsc;
13use tokio::sync::oneshot;
14use tracing::Instrument;
15
16use crate::actor::ToActor;
17use crate::commands::{ACK_COMMANDS_INTERVAL_MS, handle_commands, send_command_ack};
18use crate::config::EnvoyConfig;
19use crate::connection::{start_connection, ws_send};
20use crate::context::{SharedContext, WsTxMessage};
21use crate::events::{handle_ack_events, handle_send_events, resend_unacknowledged_events};
22use crate::handle::EnvoyHandle;
23use crate::kv::{
24	KV_CLEANUP_INTERVAL_MS, KvRequestEntry, cleanup_old_kv_requests, handle_kv_request,
25	handle_kv_response, process_unsent_kv_requests,
26};
27use crate::metrics::METRICS;
28use crate::sqlite::{
29	RemoteSqliteRequest, RemoteSqliteRequestEntry, RemoteSqliteResponseEnvelope, SqliteRequest,
30	SqliteRequestEntry, SqliteResponse, cleanup_old_remote_sqlite_requests,
31	cleanup_old_sqlite_requests, fail_remote_sqlite_requests_with_shutdown,
32	fail_sent_remote_sqlite_requests_with_indeterminate_result,
33	fail_sent_sqlite_requests_with_indeterminate_result, fail_sqlite_requests_with_shutdown,
34	handle_remote_sqlite_exec_response, handle_remote_sqlite_execute_batch_response,
35	handle_remote_sqlite_execute_response, handle_remote_sqlite_request,
36	handle_sqlite_commit_finalize_response, handle_sqlite_commit_response,
37	handle_sqlite_commit_stage_begin_response, handle_sqlite_commit_stage_segment_response,
38	handle_sqlite_get_pages_response, handle_sqlite_request, process_unsent_remote_sqlite_requests,
39	process_unsent_sqlite_requests,
40};
41use crate::tunnel::{
42	HttpRequestCancellationKey, handle_tunnel_message, make_ws_key,
43	resend_buffered_tunnel_messages, send_hibernatable_ws_message_ack,
44};
45use crate::utils::{BufferMap, EnvoyShutdownError, SleepFuture, boxed_sleep, spawn_detached};
46
47/// Process-wide envoy slot. Holds the handle inside a mutex so a stopped
48/// handle (e.g. from a shutdown-during-build race in serverless mode) can be
49/// replaced on the next `start_envoy_sync` call.
50#[cfg(not(target_arch = "wasm32"))]
51static GLOBAL_ENVOY: OnceLock<Mutex<Option<EnvoyHandle>>> = OnceLock::new();
52
53pub struct EnvoyContext {
54	pub shared: Arc<SharedContext>,
55	pub shutting_down: bool,
56	pub actors: HashMap<String, HashMap<u32, ActorEntry>>,
57	pub pending_events: HashMap<(String, u32), PendingActorEvents>,
58	pub buffered_actor_messages: HashMap<String, Vec<BufferedActorMessage>>,
59	pub kv_requests: HashMap<u32, KvRequestEntry>,
60	pub next_kv_request_id: u32,
61	pub sqlite_requests: HashMap<u32, SqliteRequestEntry>,
62	pub next_sqlite_request_id: u32,
63	pub remote_sqlite_requests: HashMap<u32, RemoteSqliteRequestEntry>,
64	pub next_remote_sqlite_request_id: u32,
65	pub request_to_actor: BufferMap<WebSocketRoute>,
66	pub http_request_routes: BufferMap<HttpRequestRoute>,
67	pub http_message_indices: BufferMap<protocol::MessageIndex>,
68	/// Recently cancelled exact-generation requests. This prevents a delayed
69	/// `RequestStart` from reaching the actor after its cancellation arrived.
70	pub http_request_cancellations: HashMap<HttpRequestCancellationKey, crate::time::Instant>,
71	pub buffered_messages: Vec<protocol::ToRivetTunnelMessage>,
72	/// Highest command index processed per `(actor_id, generation)`, used to
73	/// drop replayed commands from `pegboard-envoy` after a reconnect. Persists
74	/// across `remove_actor` so a replayed `CommandStartActor` for an
75	/// already-stopped actor cannot resurrect it.
76	pub processed_command_idx: HashMap<(String, u32), i64>,
77}
78
79pub struct HttpRequestRoute {
80	pub actor_id: String,
81	pub actor_generation: Option<u32>,
82	pub actor_admitted: bool,
83	pub session: u64,
84	pub gateway_id: protocol::GatewayId,
85	pub request_id: protocol::RequestId,
86}
87
88#[derive(Clone)]
89pub struct WebSocketRoute {
90	pub actor_id: String,
91	pub actor_generation: Option<u32>,
92}
93
94pub struct ActorEntry {
95	pub handle: mpsc::UnboundedSender<ToActor>,
96	pub active_http_request_count: Arc<AsyncCounter>,
97	pub name: String,
98	pub last_command_idx: i64,
99	pub received_stop: bool,
100}
101
102#[derive(Default)]
103pub struct PendingActorEvents {
104	pub events: Vec<protocol::EventWrapper>,
105	pub retired_at: Option<crate::time::Instant>,
106}
107
108pub enum BufferedActorMessage {
109	WsMsg {
110		message_id: protocol::MessageId,
111		msg: protocol::ToEnvoyWebSocketMessage,
112	},
113	WsClose {
114		message_id: protocol::MessageId,
115		close: protocol::ToEnvoyWebSocketClose,
116	},
117}
118
119pub enum ToEnvoyMessage {
120	ConnMessage {
121		message: protocol::ToEnvoy,
122		session: u64,
123	},
124	ConnClose {
125		evict: bool,
126		session: u64,
127	},
128	SendEvents {
129		events: Vec<protocol::EventWrapper>,
130	},
131	KvRequest {
132		actor_id: String,
133		data: protocol::KvRequestData,
134		response_tx: oneshot::Sender<anyhow::Result<protocol::KvResponseData>>,
135	},
136	SqliteRequest {
137		request: SqliteRequest,
138		response_tx: oneshot::Sender<anyhow::Result<SqliteResponse>>,
139	},
140	RemoteSqliteRequest {
141		request: RemoteSqliteRequest,
142		expected_session: Option<u64>,
143		response_tx: oneshot::Sender<anyhow::Result<RemoteSqliteResponseEnvelope>>,
144	},
145	SendOrBufferTunnelMsg {
146		msg: protocol::ToRivetTunnelMessage,
147	},
148	ActorIntent {
149		actor_id: String,
150		generation: Option<u32>,
151		intent: protocol::ActorIntent,
152		error: Option<String>,
153	},
154	SetAlarm {
155		actor_id: String,
156		generation: Option<u32>,
157		alarm_ts: Option<i64>,
158		ack_tx: Option<oneshot::Sender<()>>,
159	},
160	HwsAck {
161		gateway_id: protocol::GatewayId,
162		request_id: protocol::RequestId,
163		envoy_message_index: u16,
164	},
165	RebindWebSocket {
166		actor_id: String,
167		generation: u32,
168		gateway_id: protocol::GatewayId,
169		request_id: protocol::RequestId,
170		response_tx: oneshot::Sender<bool>,
171	},
172	HttpRequestComplete {
173		gateway_id: protocol::GatewayId,
174		request_id: protocol::RequestId,
175	},
176	GetActor {
177		actor_id: String,
178		generation: Option<u32>,
179		response_tx: oneshot::Sender<Option<ActorInfo>>,
180	},
181	Shutdown,
182	Stop,
183}
184
185/// Information about an actor, returned by `EnvoyHandle::get_actor`.
186#[derive(Clone)]
187pub struct ActorInfo {
188	pub name: String,
189	pub generation: u32,
190	pub active_http_request_count: Arc<AsyncCounter>,
191}
192
193impl EnvoyContext {
194	pub(crate) fn rebind_websocket(
195		&mut self,
196		actor_id: &str,
197		generation: u32,
198		gateway_id: &protocol::GatewayId,
199		request_id: &protocol::RequestId,
200	) -> bool {
201		if let Some(route) = self.request_to_actor.get_mut(&[gateway_id, request_id]) {
202			if route.actor_id != actor_id {
203				return false;
204			}
205			if route.actor_generation.is_some() {
206				route.actor_generation = Some(generation);
207			}
208		} else {
209			// A hibernating actor may be restored on a different Envoy process. Its
210			// authoritative start command carries the hibernating request ids, while
211			// the original process owns the old ephemeral route map.
212			self.request_to_actor.insert(
213				&[gateway_id, request_id],
214				WebSocketRoute {
215					actor_id: actor_id.to_owned(),
216					actor_generation: Some(generation),
217				},
218			);
219		}
220
221		self.shared
222			.live_tunnel_requests
223			.lock()
224			.expect("shared live tunnel request registry poisoned")
225			.insert(make_ws_key(gateway_id, request_id), actor_id.to_owned());
226		true
227	}
228
229	pub fn insert_actor(
230		&mut self,
231		actor_id: String,
232		generation: u32,
233		handle: mpsc::UnboundedSender<ToActor>,
234		active_http_request_count: Arc<AsyncCounter>,
235		name: String,
236		last_command_idx: i64,
237	) {
238		let buffered_actor_id = actor_id.clone();
239		let buffered_handle = handle.clone();
240		self.actors
241			.entry(actor_id.clone())
242			.or_insert_with(HashMap::new)
243			.insert(
244				generation,
245				ActorEntry {
246					handle: handle.clone(),
247					active_http_request_count: active_http_request_count.clone(),
248					name,
249					last_command_idx,
250					received_stop: false,
251				},
252			);
253		self.shared
254			.actors
255			.lock()
256			.expect("shared actor registry poisoned")
257			.entry(actor_id)
258			.or_insert_with(HashMap::new)
259			.insert(
260				generation,
261				crate::context::SharedActorEntry {
262					handle,
263					active_http_request_count,
264				},
265			);
266
267		self.shared.actors_notify.notify_waiters();
268
269		if let Some(messages) = self.buffered_actor_messages.remove(&buffered_actor_id) {
270			for message in messages {
271				match message {
272					BufferedActorMessage::WsMsg { message_id, msg } => {
273						let _ = buffered_handle.send(ToActor::WsMsg { message_id, msg });
274					}
275					BufferedActorMessage::WsClose { message_id, close } => {
276						let _ = buffered_handle.send(ToActor::WsClose { message_id, close });
277					}
278				}
279			}
280		}
281	}
282
283	pub fn remove_actor(&mut self, actor_id: &str, generation: u32) {
284		if let Some(generations) = self.actors.get_mut(actor_id) {
285			generations.remove(&generation);
286			if generations.is_empty() {
287				self.actors.remove(actor_id);
288			}
289		}
290
291		let mut shared = self
292			.shared
293			.actors
294			.lock()
295			.expect("shared actor registry poisoned");
296		if let Some(generations) = shared.get_mut(actor_id) {
297			generations.remove(&generation);
298			if generations.is_empty() {
299				shared.remove(actor_id);
300			}
301		}
302		self.shared.actors_notify.notify_waiters();
303	}
304
305	pub fn get_actor(&self, actor_id: &str, generation: Option<u32>) -> Option<&ActorEntry> {
306		let gens = self.actors.get(actor_id)?;
307		if gens.is_empty() {
308			return None;
309		}
310
311		if let Some(g) = generation {
312			return gens.get(&g).filter(|entry| !entry.handle.is_closed());
313		}
314
315		// Return highest generation non-closed entry
316		// HashMap doesn't guarantee order, so find max key
317		let mut best: Option<&ActorEntry> = None;
318		let mut best_gen: u32 = 0;
319		for (&g, entry) in gens {
320			if !entry.handle.is_closed() && (best.is_none() || g > best_gen) {
321				best = Some(entry);
322				best_gen = g;
323			}
324		}
325		best
326	}
327
328	/// Selects an actor for a new request. Exact-generation continuations use
329	/// `get_actor` directly so an already admitted stream can finish during stop.
330	pub fn get_actor_for_admission(
331		&self,
332		actor_id: &str,
333		generation: Option<u32>,
334	) -> Option<&ActorEntry> {
335		let actor = self.get_actor(actor_id, generation)?;
336		if generation.is_some() && actor.received_stop {
337			None
338		} else {
339			Some(actor)
340		}
341	}
342
343	pub fn get_actor_entry_mut(
344		&mut self,
345		actor_id: &str,
346		generation: u32,
347	) -> Option<&mut ActorEntry> {
348		self.actors
349			.get_mut(actor_id)
350			.and_then(|gens| gens.get_mut(&generation))
351	}
352}
353
354pub async fn start_envoy(config: EnvoyConfig) -> EnvoyHandle {
355	let handle = start_envoy_sync(config);
356	handle
357		.started()
358		.await
359		.expect("envoy failed to start before returning handle");
360	handle
361}
362
363pub fn start_envoy_sync(config: EnvoyConfig) -> EnvoyHandle {
364	#[cfg(target_arch = "wasm32")]
365	{
366		start_envoy_sync_inner(config)
367	}
368
369	#[cfg(not(target_arch = "wasm32"))]
370	{
371		if config.not_global {
372			return start_envoy_sync_inner(config);
373		}
374
375		let slot = GLOBAL_ENVOY.get_or_init(|| Mutex::new(None));
376		let mut guard = slot.lock();
377		if let Some(handle) = guard.as_ref() {
378			if !handle.is_stopped() {
379				return handle.clone();
380			}
381		}
382		let handle = start_envoy_sync_inner(config);
383		*guard = Some(handle.clone());
384		handle
385	}
386}
387
388fn start_envoy_sync_inner(config: EnvoyConfig) -> EnvoyHandle {
389	let (envoy_tx, envoy_rx) = mpsc::unbounded_channel::<ToEnvoyMessage>();
390	let (start_tx, start_rx) = tokio::sync::watch::channel(());
391	let (stopped_tx, _stopped_rx) = tokio::sync::watch::channel(false);
392	let (connection_session_tx, _connection_session_rx) = tokio::sync::watch::channel(0);
393
394	let envoy_key = uuid::Uuid::new_v4().to_string();
395	let shared = Arc::new(SharedContext {
396		config,
397		envoy_key,
398		envoy_tx: envoy_tx.clone(),
399		actors: Arc::new(std::sync::Mutex::new(HashMap::new())),
400		actors_notify: Arc::new(tokio::sync::Notify::new()),
401		live_tunnel_requests: Arc::new(std::sync::Mutex::new(HashMap::new())),
402		pending_hibernation_restores: Arc::new(std::sync::Mutex::new(HashMap::new())),
403		ws_tx: Arc::new(tokio::sync::Mutex::new(None)),
404		http_ws_tx: Arc::new(tokio::sync::Mutex::new(None)),
405		connection_session: std::sync::atomic::AtomicU64::new(0),
406		next_connection_session: std::sync::atomic::AtomicU64::new(0),
407		connection_session_tx,
408		protocol_metadata: Arc::new(tokio::sync::Mutex::new(None)),
409		shutting_down: std::sync::atomic::AtomicBool::new(false),
410		last_ping_ts: std::sync::atomic::AtomicI64::new(0),
411		stopped_tx,
412	});
413
414	let handle = EnvoyHandle {
415		shared: shared.clone(),
416		started_rx: start_rx,
417	};
418
419	start_connection(shared.clone());
420
421	let ctx = EnvoyContext {
422		shared: shared.clone(),
423		shutting_down: false,
424		actors: HashMap::new(),
425		pending_events: HashMap::new(),
426		buffered_actor_messages: HashMap::new(),
427		kv_requests: HashMap::new(),
428		next_kv_request_id: 0,
429		sqlite_requests: HashMap::new(),
430		next_sqlite_request_id: 0,
431		remote_sqlite_requests: HashMap::new(),
432		next_remote_sqlite_request_id: 0,
433		request_to_actor: BufferMap::new(),
434		http_request_routes: BufferMap::new(),
435		http_message_indices: BufferMap::new(),
436		http_request_cancellations: HashMap::new(),
437		buffered_messages: Vec::new(),
438		processed_command_idx: HashMap::new(),
439	};
440
441	tracing::info!(envoy_key = %shared.envoy_key, "starting envoy");
442	let span = tracing::info_span!("envoy_client", envoy_key = %shared.envoy_key);
443	spawn_detached(envoy_loop(ctx, envoy_rx, start_tx).instrument(span));
444
445	handle
446}
447
448async fn envoy_loop(
449	mut ctx: EnvoyContext,
450	mut rx: mpsc::UnboundedReceiver<ToEnvoyMessage>,
451	start_tx: tokio::sync::watch::Sender<()>,
452) {
453	let mut ack_tick = boxed_sleep(std::time::Duration::from_millis(ACK_COMMANDS_INTERVAL_MS));
454	let mut kv_cleanup_tick = boxed_sleep(std::time::Duration::from_millis(KV_CLEANUP_INTERVAL_MS));
455
456	let mut lost_timeout: Option<SleepFuture> = None;
457
458	loop {
459		let iter_start = crate::time::Instant::now();
460		#[allow(unused_assignments)]
461		let mut branch: &'static str = "unknown";
462		tokio::select! {
463			msg = rx.recv() => {
464				branch = "envoy_msg";
465				let Some(msg) = msg else {
466					observe_envoy_loop_iteration(branch, iter_start);
467					break;
468				};
469				METRICS.envoy_tx_depth.dec();
470
471				match msg {
472					ToEnvoyMessage::ConnMessage { message, session } => {
473						lost_timeout = handle_conn_message(&mut ctx, &start_tx, lost_timeout, message, session).await;
474					}
475					ToEnvoyMessage::ConnClose { evict, session } => {
476						remove_http_routes_for_session(&mut ctx, session);
477						for generations in ctx.actors.values() {
478							for actor in generations.values() {
479								let _ = actor.handle.send(ToActor::ConnectionClosed { session });
480							}
481						}
482						fail_sent_remote_sqlite_requests_with_indeterminate_result(&mut ctx);
483						fail_sent_sqlite_requests_with_indeterminate_result(&mut ctx);
484						lost_timeout = handle_conn_close(&ctx, lost_timeout);
485						if evict {
486							observe_envoy_loop_iteration(branch, iter_start);
487							break;
488						}
489					}
490					ToEnvoyMessage::SendEvents { events } => {
491						handle_send_events(&mut ctx, events).await;
492					}
493					ToEnvoyMessage::KvRequest { actor_id, data, response_tx } => {
494						handle_kv_request(&mut ctx, actor_id, data, response_tx).await;
495					}
496					ToEnvoyMessage::SqliteRequest { request, response_tx } => {
497						handle_sqlite_request(&mut ctx, request, response_tx).await;
498					}
499					ToEnvoyMessage::RemoteSqliteRequest { request, expected_session, response_tx } => {
500						handle_remote_sqlite_request(&mut ctx, request, expected_session, response_tx).await;
501					}
502					ToEnvoyMessage::SendOrBufferTunnelMsg { msg } => {
503						crate::tunnel::send_or_buffer_tunnel_message(&mut ctx, msg).await;
504					}
505					ToEnvoyMessage::ActorIntent { actor_id, generation, intent, error } => {
506						if let Some(entry) = ctx.get_actor(&actor_id, generation) {
507							let _ = entry.handle.send(ToActor::Intent { intent, error });
508						}
509					}
510					ToEnvoyMessage::SetAlarm { actor_id, generation, alarm_ts, ack_tx } => {
511						if let Some(entry) = ctx.get_actor(&actor_id, generation) {
512							if let Err(error) = entry.handle.send(ToActor::SetAlarm { alarm_ts, ack_tx }) {
513								if let ToActor::SetAlarm { ack_tx: Some(ack_tx), .. } = error.0 {
514									let _ = ack_tx.send(());
515								}
516							}
517						} else if let Some(ack_tx) = ack_tx {
518							let _ = ack_tx.send(());
519						}
520					}
521					ToEnvoyMessage::HwsAck { gateway_id, request_id, envoy_message_index } => {
522						send_hibernatable_ws_message_ack(&mut ctx, gateway_id, request_id, envoy_message_index);
523					}
524					ToEnvoyMessage::RebindWebSocket { actor_id, generation, gateway_id, request_id, response_tx } => {
525						let rebound = ctx.rebind_websocket(
526							&actor_id,
527							generation,
528							&gateway_id,
529							&request_id,
530						);
531						let _ = response_tx.send(rebound);
532					}
533					ToEnvoyMessage::HttpRequestComplete { gateway_id, request_id } => {
534						ctx.http_request_routes.remove(&[&gateway_id, &request_id]);
535						ctx.http_message_indices.remove(&[&gateway_id, &request_id]);
536					}
537					ToEnvoyMessage::GetActor { actor_id, generation, response_tx } => {
538						let info = ctx.get_actor(&actor_id, generation).map(|entry| {
539							let actor_gen = generation.unwrap_or_else(|| {
540								ctx.actors
541									.get(&actor_id)
542									.and_then(|gens| {
543										gens.iter()
544											.filter(|(_, e)| !e.handle.is_closed())
545											.map(|(&g, _)| g)
546											.max()
547									})
548									.unwrap_or(0)
549							});
550							ActorInfo {
551								name: entry.name.clone(),
552								generation: actor_gen,
553								active_http_request_count: entry
554									.active_http_request_count
555									.clone(),
556							}
557						});
558						let _ = response_tx.send(info);
559					}
560					ToEnvoyMessage::Shutdown => {
561						handle_shutdown(&mut ctx).await;
562					}
563					ToEnvoyMessage::Stop => {
564						observe_envoy_loop_iteration(branch, iter_start);
565						break;
566					}
567				}
568			}
569			_ = ack_tick.as_mut() => {
570				branch = "ack_tick";
571				send_command_ack(&mut ctx).await;
572				ack_tick = boxed_sleep(std::time::Duration::from_millis(ACK_COMMANDS_INTERVAL_MS));
573			}
574			_ = kv_cleanup_tick.as_mut() => {
575				branch = "cleanup_tick";
576				cleanup_old_kv_requests(&mut ctx);
577				cleanup_old_sqlite_requests(&mut ctx);
578				cleanup_old_remote_sqlite_requests(&mut ctx);
579				crate::events::cleanup_expired_terminal_events(&mut ctx);
580				kv_cleanup_tick = boxed_sleep(std::time::Duration::from_millis(KV_CLEANUP_INTERVAL_MS));
581			}
582			_ = async {
583				match lost_timeout.as_mut() {
584					Some(timeout) => timeout.as_mut().await,
585					None => std::future::pending::<()>().await,
586				}
587			} => {
588				branch = "lost_timeout";
589				// Lost timeout fired
590				for (_id, request) in ctx.kv_requests.drain() {
591					METRICS.kv_requests_inflight.dec();
592					let _ = request.response_tx.send(Err(anyhow::anyhow!(EnvoyShutdownError)));
593				}
594				fail_sqlite_requests_with_shutdown(&mut ctx);
595				fail_remote_sqlite_requests_with_shutdown(&mut ctx);
596
597				if !ctx.actors.is_empty() {
598					tracing::warn!("stopping all actors due to envoy lost threshold");
599					for (_actor_id, gens) in &ctx.actors {
600						for (_g, entry) in gens {
601							if !entry.handle.is_closed() {
602								let _ = entry.handle.send(ToActor::Lost);
603							}
604						}
605					}
606					ctx.actors.clear();
607					ctx.shared
608						.actors
609						.lock()
610						.expect("shared actor registry poisoned")
611						.clear();
612				}
613
614				lost_timeout = None;
615			}
616		}
617		observe_envoy_loop_iteration(branch, iter_start);
618	}
619
620	// Cleanup
621	{
622		let guard = ctx.shared.ws_tx.lock().await;
623		if let Some(tx) = guard.as_ref() {
624			let _ = tx.send(WsTxMessage::Close);
625		}
626	}
627
628	for (_id, request) in ctx.kv_requests.drain() {
629		METRICS.kv_requests_inflight.dec();
630		let _ = request
631			.response_tx
632			.send(Err(anyhow::anyhow!("envoy shutting down")));
633	}
634	fail_sqlite_requests_with_shutdown(&mut ctx);
635	fail_remote_sqlite_requests_with_shutdown(&mut ctx);
636
637	ctx.actors.clear();
638	ctx.shared
639		.actors
640		.lock()
641		.expect("shared actor registry poisoned")
642		.clear();
643
644	tracing::info!("envoy stopped");
645
646	ctx.shared.config.callbacks.on_shutdown();
647
648	// Latched signal: waiters on `EnvoyHandle::wait_stopped` observe this and
649	// any future callers of `wait_stopped` resolve immediately because watch
650	// retains the last value.
651	let _ = ctx.shared.stopped_tx.send(true);
652}
653
654pub(crate) fn remove_http_routes_for_session(ctx: &mut EnvoyContext, session: u64) {
655	let closed_routes = ctx
656		.http_request_routes
657		.remove_where(|route| route.session == session);
658	for route in closed_routes {
659		ctx.http_message_indices
660			.remove(&[&route.gateway_id, &route.request_id]);
661	}
662}
663
664fn observe_envoy_loop_iteration(branch: &'static str, start: crate::time::Instant) {
665	let elapsed = start.elapsed();
666	METRICS
667		.envoy_loop_iteration_duration_seconds
668		.with_label_values(&[branch])
669		.observe(elapsed.as_secs_f64());
670}
671
672/// Send a message into the envoy_loop's mpsc and bump the depth gauge.
673/// Producers should prefer this over calling `shared.envoy_tx.send` directly
674/// so the `envoy_tx_depth` gauge stays in sync.
675pub fn send_to_envoy_tx(
676	shared: &crate::context::SharedContext,
677	msg: ToEnvoyMessage,
678) -> Result<(), tokio::sync::mpsc::error::SendError<ToEnvoyMessage>> {
679	match shared.envoy_tx.send(msg) {
680		Ok(()) => {
681			METRICS.envoy_tx_depth.inc();
682			Ok(())
683		}
684		Err(e) => Err(e),
685	}
686}
687
688async fn handle_conn_message(
689	ctx: &mut EnvoyContext,
690	start_tx: &tokio::sync::watch::Sender<()>,
691	mut lost_timeout: Option<SleepFuture>,
692	message: protocol::ToEnvoy,
693	session: u64,
694) -> Option<SleepFuture> {
695	match message {
696		protocol::ToEnvoy::ToEnvoyInit(init) => {
697			{
698				let mut guard = ctx.shared.protocol_metadata.lock().await;
699				*guard = Some(init.metadata.clone());
700			}
701			tracing::info!(?init.metadata, "received init");
702
703			lost_timeout = None;
704			resend_unacknowledged_events(ctx).await;
705			process_unsent_kv_requests(ctx).await;
706			process_unsent_sqlite_requests(ctx).await;
707			process_unsent_remote_sqlite_requests(ctx).await;
708			resend_buffered_tunnel_messages(ctx).await;
709
710			let _ = start_tx.send(());
711		}
712		protocol::ToEnvoy::ToEnvoyCommands(commands) => {
713			handle_commands(ctx, commands).await;
714		}
715		protocol::ToEnvoy::ToEnvoyAckEvents(ack) => {
716			handle_ack_events(ctx, ack);
717		}
718		protocol::ToEnvoy::ToEnvoyKvResponse(response) => {
719			handle_kv_response(ctx, response).await;
720		}
721		protocol::ToEnvoy::ToEnvoySqliteGetPagesResponse(response) => {
722			handle_sqlite_get_pages_response(ctx, response).await;
723		}
724		protocol::ToEnvoy::ToEnvoySqliteCommitResponse(response) => {
725			handle_sqlite_commit_response(ctx, response).await;
726		}
727		protocol::ToEnvoy::ToEnvoySqliteCommitStageBeginResponse(response) => {
728			handle_sqlite_commit_stage_begin_response(ctx, response).await;
729		}
730		protocol::ToEnvoy::ToEnvoySqliteCommitStageSegmentResponse(response) => {
731			handle_sqlite_commit_stage_segment_response(ctx, response).await;
732		}
733		protocol::ToEnvoy::ToEnvoySqliteCommitFinalizeResponse(response) => {
734			handle_sqlite_commit_finalize_response(ctx, response).await;
735		}
736		protocol::ToEnvoy::ToEnvoySqliteExecResponse(response) => {
737			handle_remote_sqlite_exec_response(ctx, response).await;
738		}
739		protocol::ToEnvoy::ToEnvoySqliteExecuteResponse(response) => {
740			handle_remote_sqlite_execute_response(ctx, response).await;
741		}
742		protocol::ToEnvoy::ToEnvoySqliteExecuteBatchResponse(response) => {
743			handle_remote_sqlite_execute_batch_response(ctx, response).await;
744		}
745		protocol::ToEnvoy::ToEnvoyTunnelMessage(tunnel_msg) => {
746			handle_tunnel_message(ctx, session, tunnel_msg).await;
747		}
748		protocol::ToEnvoy::ToEnvoyPing(_) => {
749			// Should be handled by connection task
750		}
751	}
752
753	lost_timeout
754}
755
756fn handle_conn_close(ctx: &EnvoyContext, lost_timeout: Option<SleepFuture>) -> Option<SleepFuture> {
757	if lost_timeout.is_some() {
758		return lost_timeout;
759	}
760
761	// Read threshold from protocol metadata, fall back to 10 seconds
762	let lost_threshold = {
763		let metadata = ctx.shared.protocol_metadata.try_lock().ok();
764		metadata
765			.and_then(|guard| guard.as_ref().map(|m| m.envoy_lost_threshold as u64))
766			.unwrap_or(10_000)
767	};
768
769	tracing::debug!(ms = lost_threshold, "starting envoy lost timeout");
770
771	Some(boxed_sleep(std::time::Duration::from_millis(
772		lost_threshold,
773	)))
774}
775
776async fn handle_shutdown(ctx: &mut EnvoyContext) {
777	if ctx.shutting_down {
778		return;
779	}
780	ctx.shutting_down = true;
781	ctx.shared.shutting_down.store(true, Ordering::Release);
782
783	tracing::debug!("envoy received shutdown");
784
785	ws_send(&ctx.shared, protocol::ToRivet::ToRivetStopping).await;
786
787	// Wait for all actors to finish. The process manager (Docker,
788	// k8s, etc.) provides the ultimate shutdown deadline.
789	let actor_handles: Vec<mpsc::UnboundedSender<ToActor>> = ctx
790		.actors
791		.values()
792		.flat_map(|gens| gens.values())
793		.filter(|entry| !entry.handle.is_closed())
794		.map(|entry| entry.handle.clone())
795		.collect();
796
797	let shared = ctx.shared.clone();
798	let shutdown_span = tracing::debug_span!(
799		parent: tracing::Span::current(),
800		"envoy_graceful_shutdown",
801		envoy_key = %ctx.shared.envoy_key,
802	);
803	spawn_detached(
804		async move {
805			futures_util::future::join_all(actor_handles.iter().map(|h| h.closed())).await;
806			tracing::debug!("all actors stopped during graceful shutdown");
807			let _ = send_to_envoy_tx(&shared, ToEnvoyMessage::Stop);
808		}
809		.instrument(shutdown_span),
810	);
811}