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