Skip to main content

rivet_envoy_client/
handle.rs

1use std::sync::Arc;
2use std::sync::atomic::Ordering;
3
4use crate::async_counter::AsyncCounter;
5use rivet_envoy_protocol as protocol;
6use tokio::sync::oneshot;
7
8use crate::context::SharedContext;
9use crate::envoy::{ActorInfo, ToEnvoyMessage};
10use crate::metrics::METRICS;
11use crate::sqlite::{RemoteSqliteRequest, RemoteSqliteResponse, SqliteRequest, SqliteResponse};
12use crate::tunnel::HibernatingWebSocketMetadata;
13
14/// Handle for interacting with the envoy from callbacks.
15#[derive(Clone)]
16pub struct EnvoyHandle {
17	pub(crate) shared: Arc<SharedContext>,
18	pub(crate) started_rx: tokio::sync::watch::Receiver<()>,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct ServerlessActorStart {
23	pub actor_id: String,
24	pub generation: u32,
25}
26
27impl EnvoyHandle {
28	#[doc(hidden)]
29	pub fn from_shared(shared: Arc<SharedContext>) -> Self {
30		Self {
31			shared,
32			started_rx: tokio::sync::watch::channel(()).1,
33		}
34	}
35
36	pub fn shutdown(&self, immediate: bool) {
37		self.shared.shutting_down.store(true, Ordering::Release);
38
39		if immediate {
40			let _ = crate::envoy::send_to_envoy_tx(&self.shared, ToEnvoyMessage::Stop);
41		} else {
42			let _ = crate::envoy::send_to_envoy_tx(&self.shared, ToEnvoyMessage::Shutdown);
43		}
44	}
45
46	/// True once the envoy loop has finished its cleanup block. Latched: stays
47	/// true forever after the loop exits.
48	pub fn is_stopped(&self) -> bool {
49		*self.shared.stopped_tx.borrow()
50	}
51
52	/// Resolves when the envoy loop has finished its cleanup block.
53	///
54	/// Returning does NOT imply successful delivery of pending KV/SQLite/tunnel
55	/// requests. The cleanup block errors out every outstanding request with
56	/// `EnvoyShutdownError`. Callers needing durability must wait on individual
57	/// request acks before invoking shutdown.
58	///
59	/// Latched: safe to call before, during, or after the envoy loop exits.
60	/// A waiter arriving after the loop already exited resolves immediately.
61	pub async fn wait_stopped(&self) {
62		let mut rx = self.shared.stopped_tx.subscribe();
63		if *rx.borrow_and_update() {
64			return;
65		}
66		let _ = rx.changed().await;
67	}
68
69	/// Convenience: signal shutdown then await `wait_stopped`.
70	pub async fn shutdown_and_wait(&self, immediate: bool) {
71		self.shutdown(immediate);
72		self.wait_stopped().await;
73	}
74
75	pub async fn get_protocol_metadata(&self) -> Option<protocol::ProtocolMetadata> {
76		self.shared.protocol_metadata.lock().await.clone()
77	}
78
79	/// Threshold for `is_ping_healthy`.
80	pub const PING_HEALTHY_THRESHOLD_MS: i64 = 20_000;
81
82	/// True after the engine has sent at least one ping and the most recent ping is within
83	/// `PING_HEALTHY_THRESHOLD_MS`. Returns false when the engine link has never completed
84	/// the ping handshake or has gone silently dead long enough that an upstream health check
85	/// should treat this envoy as unhealthy and recycle it.
86	pub fn is_ping_healthy(&self) -> bool {
87		let last = self.shared.last_ping_ts.load(Ordering::Acquire);
88		if last == 0 {
89			return false;
90		}
91		crate::time::now_millis() - last < Self::PING_HEALTHY_THRESHOLD_MS
92	}
93
94	pub fn get_envoy_key(&self) -> &str {
95		&self.shared.envoy_key
96	}
97
98	pub fn endpoint(&self) -> &str {
99		&self.shared.config.endpoint
100	}
101
102	pub fn token(&self) -> Option<&str> {
103		self.shared.config.token.as_deref()
104	}
105
106	/// Returns the current WebSocket session ID, or `None` while disconnected.
107	/// This is an internal client-side affinity token; it is never sent over the
108	/// Envoy protocol.
109	#[doc(hidden)]
110	pub fn connection_session(&self) -> Option<u64> {
111		let session = self.shared.connection_session.load(Ordering::Acquire);
112		(session != 0).then_some(session)
113	}
114
115	#[doc(hidden)]
116	pub fn subscribe_connection_session(&self) -> tokio::sync::watch::Receiver<u64> {
117		self.shared.connection_session_tx.subscribe()
118	}
119
120	pub fn namespace(&self) -> &str {
121		&self.shared.config.namespace
122	}
123
124	pub fn active_actor_count(&self) -> usize {
125		let guard = self
126			.shared
127			.actors
128			.lock()
129			.expect("shared actor registry poisoned");
130		guard
131			.values()
132			.map(|generations| {
133				generations
134					.values()
135					.filter(|actor| !actor.handle.is_closed())
136					.count()
137			})
138			.sum()
139	}
140
141	pub fn pool_name(&self) -> &str {
142		&self.shared.config.pool_name
143	}
144
145	pub async fn started(&self) -> anyhow::Result<()> {
146		self.started_rx
147			.clone()
148			.changed()
149			.await
150			.map_err(|_| anyhow::anyhow!("envoy stopped before startup completed"))?;
151		Ok(())
152	}
153
154	/// Reports a sleep intent for an actor. An `error` marks the sleep as a
155	/// crash: it surfaces as `StopCode::Error` on the eventual `Stopped` event,
156	/// which is what the engine records the crash from. The engine answers a
157	/// crashed stop by putting the actor back to sleep rather than destroying
158	/// it, so a crash belongs here rather than on [`Self::stop_actor`].
159	pub fn sleep_actor(&self, actor_id: String, generation: Option<u32>, error: Option<String>) {
160		let _ = crate::envoy::send_to_envoy_tx(
161			&self.shared,
162			ToEnvoyMessage::ActorIntent {
163				actor_id,
164				generation,
165				intent: protocol::ActorIntent::ActorIntentSleep,
166				error,
167			},
168		);
169	}
170
171	/// Reports a stop intent for an actor. This is the deliberate-destruction
172	/// signal: the engine answers it by destroying the actor and its durable
173	/// state. It takes no error by construction, because a crash must not be
174	/// reported as an intent to destroy.
175	pub fn stop_actor(&self, actor_id: String, generation: Option<u32>) {
176		let _ = crate::envoy::send_to_envoy_tx(
177			&self.shared,
178			ToEnvoyMessage::ActorIntent {
179				actor_id,
180				generation,
181				intent: protocol::ActorIntent::ActorIntentStop,
182				error: None,
183			},
184		);
185	}
186
187	pub async fn get_actor(&self, actor_id: &str, generation: Option<u32>) -> Option<ActorInfo> {
188		let (tx, rx) = tokio::sync::oneshot::channel();
189		crate::envoy::send_to_envoy_tx(
190			&self.shared,
191			ToEnvoyMessage::GetActor {
192				actor_id: actor_id.to_string(),
193				generation,
194				response_tx: tx,
195			},
196		)
197		.ok()?;
198		rx.await.ok().flatten()
199	}
200
201	pub async fn wait_actor_registered_then_stopped(&self, actor_id: &str, generation: u32) {
202		let mut registered = false;
203		loop {
204			let notified = self.shared.actors_notify.notified();
205			if self.is_stopped() {
206				return;
207			}
208
209			let actor_is_registered = {
210				let guard = self
211					.shared
212					.actors
213					.lock()
214					.expect("shared actor registry poisoned");
215				guard
216					.get(actor_id)
217					.and_then(|generations| generations.get(&generation))
218					.is_some()
219			};
220
221			if registered && !actor_is_registered {
222				return;
223			}
224			if actor_is_registered {
225				registered = true;
226			}
227
228			tokio::select! {
229				_ = notified => {}
230				_ = self.wait_stopped() => return,
231			}
232		}
233	}
234
235	/// Resolve once the envoy has no active actors (or has stopped). Event-driven via
236	/// `actors_notify`, which `remove_actor` pings on deregister; armed with `enable`
237	/// before the count check so a drain-to-zero cannot race past the waiter.
238	pub async fn wait_actors_drained(&self) {
239		loop {
240			let notified = self.shared.actors_notify.notified();
241			tokio::pin!(notified);
242			// Register interest before reading the count so a `notify_waiters` that
243			// fires between the check and the await is not lost.
244			notified.as_mut().enable();
245
246			if self.is_stopped() || self.active_actor_count() == 0 {
247				return;
248			}
249
250			tokio::select! {
251				_ = notified => {}
252				_ = self.wait_stopped() => return,
253			}
254		}
255	}
256
257	pub fn http_request_counter(
258		&self,
259		actor_id: &str,
260		generation: Option<u32>,
261	) -> Option<Arc<AsyncCounter>> {
262		let guard = self
263			.shared
264			.actors
265			.lock()
266			.expect("shared actor registry poisoned");
267		let generations = guard.get(actor_id)?;
268
269		if let Some(generation) = generation {
270			return generations
271				.get(&generation)
272				.map(|actor| actor.active_http_request_count.clone());
273		}
274
275		generations
276			.iter()
277			.filter(|(_, actor)| !actor.handle.is_closed())
278			.max_by_key(|(generation, _)| *generation)
279			.map(|(_, actor)| actor.active_http_request_count.clone())
280	}
281
282	pub async fn get_active_http_request_count(
283		&self,
284		actor_id: &str,
285		generation: Option<u32>,
286	) -> Option<usize> {
287		self.http_request_counter(actor_id, generation)
288			.map(|counter| counter.load())
289	}
290
291	pub fn hibernatable_connection_is_live(
292		&self,
293		actor_id: &str,
294		_generation: Option<u32>,
295		gateway_id: protocol::GatewayId,
296		request_id: protocol::RequestId,
297	) -> bool {
298		let key = make_ws_key(&gateway_id, &request_id);
299		if self
300			.shared
301			.live_tunnel_requests
302			.lock()
303			.expect("shared live tunnel request registry poisoned")
304			.get(&key)
305			.is_some_and(|live_actor_id| live_actor_id == actor_id)
306		{
307			return true;
308		}
309
310		self.shared
311			.pending_hibernation_restores
312			.lock()
313			.expect("shared pending hibernation restore registry poisoned")
314			.get(actor_id)
315			.is_some_and(|entries| {
316				entries
317					.iter()
318					.any(|entry| entry.gateway_id == gateway_id && entry.request_id == request_id)
319			})
320	}
321
322	pub fn set_alarm(&self, actor_id: String, alarm_ts: Option<i64>, generation: Option<u32>) {
323		self.set_alarm_with_ack(actor_id, alarm_ts, generation, None);
324	}
325
326	pub fn set_alarm_with_ack(
327		&self,
328		actor_id: String,
329		alarm_ts: Option<i64>,
330		generation: Option<u32>,
331		ack_tx: Option<oneshot::Sender<()>>,
332	) {
333		let _ = crate::envoy::send_to_envoy_tx(
334			&self.shared,
335			ToEnvoyMessage::SetAlarm {
336				actor_id,
337				generation,
338				alarm_ts,
339				ack_tx,
340			},
341		);
342	}
343
344	pub async fn kv_get(
345		&self,
346		actor_id: String,
347		keys: Vec<Vec<u8>>,
348	) -> anyhow::Result<Vec<Option<Vec<u8>>>> {
349		let request_keys = keys.clone();
350		let response = self
351			.send_kv_request(
352				actor_id,
353				protocol::KvRequestData::KvGetRequest(protocol::KvGetRequest { keys }),
354			)
355			.await?;
356
357		match response {
358			protocol::KvResponseData::KvGetResponse(resp) => {
359				let mut result = Vec::with_capacity(request_keys.len());
360				for requested_key in &request_keys {
361					let mut found = false;
362					for (i, resp_key) in resp.keys.iter().enumerate() {
363						if requested_key == resp_key {
364							result.push(Some(resp.values[i].clone()));
365							found = true;
366							break;
367						}
368					}
369					if !found {
370						result.push(None);
371					}
372				}
373				Ok(result)
374			}
375			protocol::KvResponseData::KvErrorResponse(e) => {
376				anyhow::bail!("{}", e.message)
377			}
378			_ => anyhow::bail!("unexpected KV response type"),
379		}
380	}
381
382	pub async fn kv_list_all(
383		&self,
384		actor_id: String,
385		reverse: Option<bool>,
386		limit: Option<u64>,
387	) -> anyhow::Result<Vec<(Vec<u8>, Vec<u8>)>> {
388		let response = self
389			.send_kv_request(
390				actor_id,
391				protocol::KvRequestData::KvListRequest(protocol::KvListRequest {
392					query: protocol::KvListQuery::KvListAllQuery,
393					reverse,
394					limit,
395				}),
396			)
397			.await?;
398		parse_list_response(response)
399	}
400
401	pub async fn kv_list_range(
402		&self,
403		actor_id: String,
404		start: Vec<u8>,
405		end: Vec<u8>,
406		exclusive: bool,
407		reverse: Option<bool>,
408		limit: Option<u64>,
409	) -> anyhow::Result<Vec<(Vec<u8>, Vec<u8>)>> {
410		let response = self
411			.send_kv_request(
412				actor_id,
413				protocol::KvRequestData::KvListRequest(protocol::KvListRequest {
414					query: protocol::KvListQuery::KvListRangeQuery(protocol::KvListRangeQuery {
415						start,
416						end,
417						exclusive,
418					}),
419					reverse,
420					limit,
421				}),
422			)
423			.await?;
424		parse_list_response(response)
425	}
426
427	pub async fn kv_list_prefix(
428		&self,
429		actor_id: String,
430		prefix: Vec<u8>,
431		reverse: Option<bool>,
432		limit: Option<u64>,
433	) -> anyhow::Result<Vec<(Vec<u8>, Vec<u8>)>> {
434		let response = self
435			.send_kv_request(
436				actor_id,
437				protocol::KvRequestData::KvListRequest(protocol::KvListRequest {
438					query: protocol::KvListQuery::KvListPrefixQuery(protocol::KvListPrefixQuery {
439						key: prefix,
440					}),
441					reverse,
442					limit,
443				}),
444			)
445			.await?;
446		parse_list_response(response)
447	}
448
449	pub async fn kv_put(
450		&self,
451		actor_id: String,
452		entries: Vec<(Vec<u8>, Vec<u8>)>,
453	) -> anyhow::Result<()> {
454		let (keys, values): (Vec<_>, Vec<_>) = entries.into_iter().unzip();
455		let response = self
456			.send_kv_request(
457				actor_id,
458				protocol::KvRequestData::KvPutRequest(protocol::KvPutRequest { keys, values }),
459			)
460			.await?;
461		match response {
462			protocol::KvResponseData::KvPutResponse => Ok(()),
463			protocol::KvResponseData::KvErrorResponse(e) => anyhow::bail!("{}", e.message),
464			_ => anyhow::bail!("unexpected KV response type"),
465		}
466	}
467
468	pub async fn kv_delete(&self, actor_id: String, keys: Vec<Vec<u8>>) -> anyhow::Result<()> {
469		let response = self
470			.send_kv_request(
471				actor_id,
472				protocol::KvRequestData::KvDeleteRequest(protocol::KvDeleteRequest { keys }),
473			)
474			.await?;
475		match response {
476			protocol::KvResponseData::KvDeleteResponse => Ok(()),
477			protocol::KvResponseData::KvErrorResponse(e) => anyhow::bail!("{}", e.message),
478			_ => anyhow::bail!("unexpected KV response type"),
479		}
480	}
481
482	pub async fn kv_delete_range(
483		&self,
484		actor_id: String,
485		start: Vec<u8>,
486		end: Vec<u8>,
487	) -> anyhow::Result<()> {
488		let response = self
489			.send_kv_request(
490				actor_id,
491				protocol::KvRequestData::KvDeleteRangeRequest(protocol::KvDeleteRangeRequest {
492					start,
493					end,
494				}),
495			)
496			.await?;
497		match response {
498			protocol::KvResponseData::KvDeleteResponse => Ok(()),
499			protocol::KvResponseData::KvErrorResponse(e) => anyhow::bail!("{}", e.message),
500			_ => anyhow::bail!("unexpected KV response type"),
501		}
502	}
503
504	pub async fn kv_drop(&self, actor_id: String) -> anyhow::Result<()> {
505		let response = self
506			.send_kv_request(actor_id, protocol::KvRequestData::KvDropRequest)
507			.await?;
508		match response {
509			protocol::KvResponseData::KvDropResponse => Ok(()),
510			protocol::KvResponseData::KvErrorResponse(e) => anyhow::bail!("{}", e.message),
511			_ => anyhow::bail!("unexpected KV response type"),
512		}
513	}
514
515	pub async fn sqlite_get_pages(
516		&self,
517		request: protocol::SqliteGetPagesRequest,
518	) -> anyhow::Result<protocol::SqliteGetPagesResponse> {
519		match self
520			.send_sqlite_request(SqliteRequest::GetPages(request))
521			.await?
522		{
523			SqliteResponse::GetPages(response) => Ok(response),
524			_ => anyhow::bail!("unexpected sqlite get_pages response type"),
525		}
526	}
527
528	pub async fn sqlite_commit(
529		&self,
530		request: protocol::SqliteCommitRequest,
531	) -> anyhow::Result<protocol::SqliteCommitResponse> {
532		match self
533			.send_sqlite_request(SqliteRequest::Commit(request))
534			.await?
535		{
536			SqliteResponse::Commit(response) => Ok(response),
537			_ => anyhow::bail!("unexpected sqlite commit response type"),
538		}
539	}
540
541	/// Opens a staged commit for a commit too large to cross the socket in one message.
542	pub async fn sqlite_commit_stage_begin(
543		&self,
544		request: protocol::SqliteCommitStageBeginRequest,
545	) -> anyhow::Result<protocol::SqliteCommitStageBeginResponse> {
546		match self
547			.send_sqlite_request(SqliteRequest::CommitStageBegin(request))
548			.await?
549		{
550			SqliteResponse::CommitStageBegin(response) => Ok(response),
551			_ => anyhow::bail!("unexpected sqlite commit_stage_begin response type"),
552		}
553	}
554
555	pub async fn sqlite_commit_stage_segment(
556		&self,
557		request: protocol::SqliteCommitStageSegmentRequest,
558	) -> anyhow::Result<protocol::SqliteCommitStageSegmentResponse> {
559		match self
560			.send_sqlite_request(SqliteRequest::CommitStageSegment(request))
561			.await?
562		{
563			SqliteResponse::CommitStageSegment(response) => Ok(response),
564			_ => anyhow::bail!("unexpected sqlite commit_stage_segment response type"),
565		}
566	}
567
568	/// Publishes a staged commit. This is the point the commit becomes visible.
569	pub async fn sqlite_commit_finalize(
570		&self,
571		request: protocol::SqliteCommitFinalizeRequest,
572	) -> anyhow::Result<protocol::SqliteCommitFinalizeResponse> {
573		match self
574			.send_sqlite_request(SqliteRequest::CommitFinalize(request))
575			.await?
576		{
577			SqliteResponse::CommitFinalize(response) => Ok(response),
578			_ => anyhow::bail!("unexpected sqlite commit_finalize response type"),
579		}
580	}
581
582	pub async fn remote_sqlite_exec(
583		&self,
584		request: protocol::SqliteExecRequest,
585	) -> anyhow::Result<protocol::SqliteExecResponse> {
586		match self
587			.send_remote_sqlite_request(RemoteSqliteRequest::Exec(request), None)
588			.await?
589			.response
590		{
591			RemoteSqliteResponse::Exec(response) => Ok(response),
592			_ => anyhow::bail!("unexpected remote sqlite exec response type"),
593		}
594	}
595
596	pub async fn remote_sqlite_execute(
597		&self,
598		request: protocol::SqliteExecuteRequest,
599	) -> anyhow::Result<protocol::SqliteExecuteResponse> {
600		match self
601			.send_remote_sqlite_request(RemoteSqliteRequest::Execute(request), None)
602			.await?
603			.response
604		{
605			RemoteSqliteResponse::Execute(response) => Ok(response),
606			_ => anyhow::bail!("unexpected remote sqlite execute response type"),
607		}
608	}
609
610	pub async fn remote_sqlite_execute_batch(
611		&self,
612		request: protocol::SqliteExecuteBatchRequest,
613	) -> anyhow::Result<protocol::SqliteExecuteBatchResponse> {
614		match self
615			.send_remote_sqlite_request(RemoteSqliteRequest::ExecuteBatch(request), None)
616			.await?
617			.response
618		{
619			RemoteSqliteResponse::ExecuteBatch(response) => Ok(response),
620			_ => anyhow::bail!("unexpected remote sqlite execute batch response type"),
621		}
622	}
623
624	/// Executes remote SQLite on one exact WebSocket session and returns the
625	/// session that carried the response. Passing `None` allows an unsent request
626	/// to wait for the next connection; passing `Some` fails before sending if
627	/// that session has disconnected.
628	#[doc(hidden)]
629	pub async fn remote_sqlite_exec_with_session(
630		&self,
631		request: protocol::SqliteExecRequest,
632		expected_session: Option<u64>,
633	) -> anyhow::Result<(protocol::SqliteExecResponse, u64)> {
634		let envelope = self
635			.send_remote_sqlite_request(RemoteSqliteRequest::Exec(request), expected_session)
636			.await?;
637		match envelope.response {
638			RemoteSqliteResponse::Exec(response) => Ok((response, envelope.session)),
639			_ => anyhow::bail!("unexpected remote sqlite exec response type"),
640		}
641	}
642
643	#[doc(hidden)]
644	pub async fn remote_sqlite_execute_with_session(
645		&self,
646		request: protocol::SqliteExecuteRequest,
647		expected_session: Option<u64>,
648	) -> anyhow::Result<(protocol::SqliteExecuteResponse, u64)> {
649		let envelope = self
650			.send_remote_sqlite_request(RemoteSqliteRequest::Execute(request), expected_session)
651			.await?;
652		match envelope.response {
653			RemoteSqliteResponse::Execute(response) => Ok((response, envelope.session)),
654			_ => anyhow::bail!("unexpected remote sqlite execute response type"),
655		}
656	}
657
658	pub fn restore_hibernating_requests(
659		&self,
660		actor_id: String,
661		meta_entries: Vec<HibernatingWebSocketMetadata>,
662	) {
663		self.shared
664			.pending_hibernation_restores
665			.lock()
666			.expect("shared pending hibernation restore registry poisoned")
667			.insert(actor_id, meta_entries);
668	}
669
670	pub(crate) fn take_pending_hibernation_restore(
671		&self,
672		actor_id: &str,
673	) -> Option<Vec<HibernatingWebSocketMetadata>> {
674		self.shared
675			.pending_hibernation_restores
676			.lock()
677			.expect("shared pending hibernation restore registry poisoned")
678			.remove(actor_id)
679	}
680
681	pub fn send_hibernatable_ws_message_ack(
682		&self,
683		gateway_id: protocol::GatewayId,
684		request_id: protocol::RequestId,
685		client_message_index: u16,
686	) {
687		let _ = crate::envoy::send_to_envoy_tx(
688			&self.shared,
689			ToEnvoyMessage::HwsAck {
690				gateway_id,
691				request_id,
692				envoy_message_index: client_message_index,
693			},
694		);
695	}
696
697	pub(crate) async fn rebind_hibernating_websocket(
698		&self,
699		actor_id: String,
700		generation: u32,
701		gateway_id: protocol::GatewayId,
702		request_id: protocol::RequestId,
703	) -> bool {
704		let (response_tx, response_rx) = oneshot::channel();
705		if crate::envoy::send_to_envoy_tx(
706			&self.shared,
707			ToEnvoyMessage::RebindWebSocket {
708				actor_id,
709				generation,
710				gateway_id,
711				request_id,
712				response_tx,
713			},
714		)
715		.is_err()
716		{
717			return false;
718		}
719		response_rx.await.unwrap_or(false)
720	}
721
722	/// Inject a serverless start payload into the envoy.
723	/// The payload is a u16 LE protocol version followed by a serialized ToEnvoy message.
724	pub async fn start_serverless_actor(&self, payload: &[u8]) -> anyhow::Result<()> {
725		tracing::debug!(
726			envoy_key = %self.shared.envoy_key,
727			payload_len = payload.len(),
728			"received serverless start request"
729		);
730		let (message, _) = decode_serverless_actor_start_payload(payload)?;
731
732		// Wait for envoy to be started before injecting
733		self.started().await?;
734
735		tracing::debug!(
736			envoy_key = %self.shared.envoy_key,
737			data = crate::stringify::stringify_to_envoy(&message),
738			"received serverless start"
739		);
740		crate::envoy::send_to_envoy_tx(
741			&self.shared,
742			ToEnvoyMessage::ConnMessage {
743				message,
744				session: self.shared.connection_session.load(Ordering::Acquire),
745			},
746		)
747		.map_err(|_| anyhow::anyhow!("envoy channel closed"))?;
748
749		Ok(())
750	}
751
752	pub fn decode_serverless_actor_start(
753		&self,
754		payload: &[u8],
755	) -> anyhow::Result<ServerlessActorStart> {
756		let (_, actor_start) = decode_serverless_actor_start_payload(payload)?;
757		Ok(actor_start)
758	}
759}
760
761fn decode_serverless_actor_start_payload(
762	payload: &[u8],
763) -> anyhow::Result<(protocol::ToEnvoy, ServerlessActorStart)> {
764	use vbare::OwnedVersionedData;
765
766	if payload.len() < 2 {
767		anyhow::bail!("serverless start payload too short");
768	}
769
770	let version = u16::from_le_bytes([payload[0], payload[1]]);
771	if version != protocol::PROTOCOL_VERSION {
772		anyhow::bail!(
773			"serverless start payload does not match protocol version: {version} vs {}",
774			protocol::PROTOCOL_VERSION
775		);
776	}
777
778	let message = match crate::protocol::versioned::ToEnvoy::deserialize(&payload[2..], version) {
779		Ok(message) => message,
780		Err(err) if version == protocol::PROTOCOL_VERSION => {
781			tracing::debug!(
782				?err,
783				"serverless start payload failed current-version decode, retrying as v1-compatible body"
784			);
785			crate::protocol::versioned::ToEnvoy::deserialize(
786				&payload[2..],
787				protocol::PROTOCOL_VERSION - 1,
788			)?
789		}
790		Err(err) => return Err(err),
791	};
792
793	let protocol::ToEnvoy::ToEnvoyCommands(ref commands) = message else {
794		anyhow::bail!("invalid serverless payload: expected ToEnvoyCommands");
795	};
796	if commands.len() != 1 {
797		anyhow::bail!("invalid serverless payload: expected exactly 1 command");
798	}
799	if !matches!(commands[0].inner, protocol::Command::CommandStartActor(_)) {
800		anyhow::bail!("invalid serverless payload: expected CommandStartActor");
801	}
802
803	let actor_start = ServerlessActorStart {
804		actor_id: commands[0].checkpoint.actor_id.clone(),
805		generation: commands[0].checkpoint.generation,
806	};
807
808	Ok((message, actor_start))
809}
810
811impl EnvoyHandle {
812	async fn send_kv_request(
813		&self,
814		actor_id: String,
815		data: protocol::KvRequestData,
816	) -> anyhow::Result<protocol::KvResponseData> {
817		let (tx, rx) = tokio::sync::oneshot::channel();
818		crate::envoy::send_to_envoy_tx(
819			&self.shared,
820			ToEnvoyMessage::KvRequest {
821				actor_id,
822				data,
823				response_tx: tx,
824			},
825		)
826		.map_err(|_| anyhow::anyhow!("envoy channel closed"))?;
827		rx.await
828			.map_err(|_| anyhow::anyhow!("kv response channel closed"))?
829	}
830
831	async fn send_sqlite_request(&self, request: SqliteRequest) -> anyhow::Result<SqliteResponse> {
832		let kind = request.kind();
833		let total_start = crate::time::Instant::now();
834		let submit_start = crate::time::Instant::now();
835		let (tx, rx) = tokio::sync::oneshot::channel();
836		crate::envoy::send_to_envoy_tx(
837			&self.shared,
838			ToEnvoyMessage::SqliteRequest {
839				request,
840				response_tx: tx,
841			},
842		)
843		.map_err(|_| anyhow::anyhow!("envoy channel closed"))?;
844		let submit_elapsed = submit_start.elapsed();
845		METRICS
846			.sqlite_request_submit_duration_seconds
847			.with_label_values(&[kind])
848			.observe(submit_elapsed.as_secs_f64());
849
850		let wait_start = crate::time::Instant::now();
851		let result = rx
852			.await
853			.map_err(|_| anyhow::anyhow!("sqlite response channel closed"))?;
854		let wait_elapsed = wait_start.elapsed();
855		METRICS
856			.sqlite_request_wait_duration_seconds
857			.with_label_values(&[kind])
858			.observe(wait_elapsed.as_secs_f64());
859		METRICS
860			.sqlite_request_total_duration_seconds
861			.with_label_values(&[kind])
862			.observe(total_start.elapsed().as_secs_f64());
863		result
864	}
865
866	async fn send_remote_sqlite_request(
867		&self,
868		request: RemoteSqliteRequest,
869		expected_session: Option<u64>,
870	) -> anyhow::Result<crate::sqlite::RemoteSqliteResponseEnvelope> {
871		let kind = request.kind();
872		let total_start = crate::time::Instant::now();
873		let submit_start = crate::time::Instant::now();
874		let (tx, rx) = tokio::sync::oneshot::channel();
875		crate::envoy::send_to_envoy_tx(
876			&self.shared,
877			ToEnvoyMessage::RemoteSqliteRequest {
878				request,
879				expected_session,
880				response_tx: tx,
881			},
882		)
883		.map_err(|_| anyhow::anyhow!("envoy channel closed"))?;
884		let submit_elapsed = submit_start.elapsed();
885		METRICS
886			.sqlite_request_submit_duration_seconds
887			.with_label_values(&[kind])
888			.observe(submit_elapsed.as_secs_f64());
889
890		let wait_start = crate::time::Instant::now();
891		let result = rx
892			.await
893			.map_err(|_| anyhow::anyhow!("remote sqlite response channel closed"))?;
894		let wait_elapsed = wait_start.elapsed();
895		METRICS
896			.sqlite_request_wait_duration_seconds
897			.with_label_values(&[kind])
898			.observe(wait_elapsed.as_secs_f64());
899		METRICS
900			.sqlite_request_total_duration_seconds
901			.with_label_values(&[kind])
902			.observe(total_start.elapsed().as_secs_f64());
903		result
904	}
905}
906
907fn make_ws_key(gateway_id: &protocol::GatewayId, request_id: &protocol::RequestId) -> [u8; 8] {
908	let mut key = [0u8; 8];
909	key[..4].copy_from_slice(gateway_id);
910	key[4..].copy_from_slice(request_id);
911	key
912}
913
914fn parse_list_response(
915	response: protocol::KvResponseData,
916) -> anyhow::Result<Vec<(Vec<u8>, Vec<u8>)>> {
917	match response {
918		protocol::KvResponseData::KvListResponse(resp) => {
919			Ok(resp.keys.into_iter().zip(resp.values).collect())
920		}
921		protocol::KvResponseData::KvErrorResponse(e) => anyhow::bail!("{}", e.message),
922		_ => anyhow::bail!("unexpected KV response type"),
923	}
924}