Skip to main content

rivet_envoy_client/
sqlite.rs

1use rivet_envoy_protocol as protocol;
2use tokio::sync::oneshot;
3
4use crate::connection::{WsSendResult, ws_send, ws_send_for_session};
5use crate::envoy::EnvoyContext;
6use crate::kv::KV_EXPIRE_MS;
7use crate::metrics::METRICS;
8use crate::utils::{
9	EnvoyShutdownError, RemoteSqliteConnectionSessionLostError,
10	RemoteSqliteIndeterminateResultError,
11};
12
13#[derive(Clone)]
14pub enum SqliteRequest {
15	GetPages(protocol::SqliteGetPagesRequest),
16	Commit(protocol::SqliteCommitRequest),
17	CommitStageBegin(protocol::SqliteCommitStageBeginRequest),
18	CommitStageSegment(protocol::SqliteCommitStageSegmentRequest),
19	CommitFinalize(protocol::SqliteCommitFinalizeRequest),
20}
21
22impl SqliteRequest {
23	pub fn kind(&self) -> &'static str {
24		match self {
25			SqliteRequest::GetPages(_) => "get_pages",
26			SqliteRequest::Commit(_) => "commit",
27			SqliteRequest::CommitStageBegin(_) => "commit_stage_begin",
28			SqliteRequest::CommitStageSegment(_) => "commit_stage_segment",
29			SqliteRequest::CommitFinalize(_) => "commit_finalize",
30		}
31	}
32}
33
34pub enum SqliteResponse {
35	GetPages(protocol::SqliteGetPagesResponse),
36	Commit(protocol::SqliteCommitResponse),
37	CommitStageBegin(protocol::SqliteCommitStageBeginResponse),
38	CommitStageSegment(protocol::SqliteCommitStageSegmentResponse),
39	CommitFinalize(protocol::SqliteCommitFinalizeResponse),
40}
41
42#[derive(Clone, Debug)]
43pub enum RemoteSqliteRequest {
44	Exec(protocol::SqliteExecRequest),
45	Execute(protocol::SqliteExecuteRequest),
46	ExecuteBatch(protocol::SqliteExecuteBatchRequest),
47}
48
49#[derive(Debug)]
50pub enum RemoteSqliteResponse {
51	Exec(protocol::SqliteExecResponse),
52	Execute(protocol::SqliteExecuteResponse),
53	ExecuteBatch(protocol::SqliteExecuteBatchResponse),
54}
55
56#[derive(Debug)]
57pub struct RemoteSqliteResponseEnvelope {
58	pub response: RemoteSqliteResponse,
59	pub session: u64,
60}
61
62impl RemoteSqliteRequest {
63	fn operation(&self) -> &'static str {
64		match self {
65			RemoteSqliteRequest::Exec(_) => "exec",
66			RemoteSqliteRequest::Execute(_) => "execute",
67			RemoteSqliteRequest::ExecuteBatch(_) => "execute_batch",
68		}
69	}
70
71	pub fn kind(&self) -> &'static str {
72		match self {
73			RemoteSqliteRequest::Exec(_) => "remote_exec",
74			RemoteSqliteRequest::Execute(_) => "remote_execute",
75			RemoteSqliteRequest::ExecuteBatch(_) => "remote_execute_batch",
76		}
77	}
78}
79
80pub struct SqliteRequestEntry {
81	pub request: SqliteRequest,
82	pub response_tx: oneshot::Sender<anyhow::Result<SqliteResponse>>,
83	pub sent: bool,
84	pub timestamp: crate::time::Instant,
85}
86
87pub struct RemoteSqliteRequestEntry {
88	pub request: RemoteSqliteRequest,
89	pub response_tx: oneshot::Sender<anyhow::Result<RemoteSqliteResponseEnvelope>>,
90	pub expected_session: Option<u64>,
91	pub sent_session: Option<u64>,
92	pub sent: bool,
93	pub timestamp: crate::time::Instant,
94}
95
96pub async fn handle_sqlite_request(
97	ctx: &mut EnvoyContext,
98	request: SqliteRequest,
99	response_tx: oneshot::Sender<anyhow::Result<SqliteResponse>>,
100) {
101	let request_id = ctx.next_sqlite_request_id;
102	ctx.next_sqlite_request_id += 1;
103
104	let entry = SqliteRequestEntry {
105		request,
106		response_tx,
107		sent: false,
108		timestamp: crate::time::Instant::now(),
109	};
110
111	ctx.sqlite_requests.insert(request_id, entry);
112	METRICS.sqlite_requests_inflight.inc();
113
114	let ws_available = {
115		let guard = ctx.shared.ws_tx.lock().await;
116		guard.is_some()
117	};
118
119	if ws_available {
120		send_single_sqlite_request(ctx, request_id).await;
121	}
122}
123
124pub async fn handle_remote_sqlite_request(
125	ctx: &mut EnvoyContext,
126	request: RemoteSqliteRequest,
127	expected_session: Option<u64>,
128	response_tx: oneshot::Sender<anyhow::Result<RemoteSqliteResponseEnvelope>>,
129) {
130	let request_id = ctx.next_remote_sqlite_request_id;
131	ctx.next_remote_sqlite_request_id += 1;
132
133	let entry = RemoteSqliteRequestEntry {
134		request,
135		response_tx,
136		expected_session,
137		sent_session: None,
138		sent: false,
139		timestamp: crate::time::Instant::now(),
140	};
141
142	ctx.remote_sqlite_requests.insert(request_id, entry);
143	METRICS.remote_sqlite_requests_inflight.inc();
144
145	send_single_remote_sqlite_request(ctx, request_id).await;
146}
147
148pub async fn handle_sqlite_get_pages_response(
149	ctx: &mut EnvoyContext,
150	response: protocol::ToEnvoySqliteGetPagesResponse,
151) {
152	handle_sqlite_response(
153		ctx,
154		response.request_id,
155		SqliteResponse::GetPages(response.data),
156		"sqlite_get_pages",
157	);
158}
159
160pub async fn handle_sqlite_commit_response(
161	ctx: &mut EnvoyContext,
162	response: protocol::ToEnvoySqliteCommitResponse,
163) {
164	handle_sqlite_response(
165		ctx,
166		response.request_id,
167		SqliteResponse::Commit(response.data),
168		"sqlite_commit",
169	);
170}
171
172pub async fn handle_sqlite_commit_stage_begin_response(
173	ctx: &mut EnvoyContext,
174	response: protocol::ToEnvoySqliteCommitStageBeginResponse,
175) {
176	handle_sqlite_response(
177		ctx,
178		response.request_id,
179		SqliteResponse::CommitStageBegin(response.data),
180		"sqlite_commit_stage_begin",
181	);
182}
183
184pub async fn handle_sqlite_commit_stage_segment_response(
185	ctx: &mut EnvoyContext,
186	response: protocol::ToEnvoySqliteCommitStageSegmentResponse,
187) {
188	handle_sqlite_response(
189		ctx,
190		response.request_id,
191		SqliteResponse::CommitStageSegment(response.data),
192		"sqlite_commit_stage_segment",
193	);
194}
195
196pub async fn handle_sqlite_commit_finalize_response(
197	ctx: &mut EnvoyContext,
198	response: protocol::ToEnvoySqliteCommitFinalizeResponse,
199) {
200	handle_sqlite_response(
201		ctx,
202		response.request_id,
203		SqliteResponse::CommitFinalize(response.data),
204		"sqlite_commit_finalize",
205	);
206}
207
208pub async fn handle_remote_sqlite_exec_response(
209	ctx: &mut EnvoyContext,
210	response: protocol::ToEnvoySqliteExecResponse,
211) {
212	handle_remote_sqlite_response(
213		ctx,
214		response.request_id,
215		RemoteSqliteResponse::Exec(response.data),
216		"remote_sqlite_exec",
217	);
218}
219
220pub async fn handle_remote_sqlite_execute_response(
221	ctx: &mut EnvoyContext,
222	response: protocol::ToEnvoySqliteExecuteResponse,
223) {
224	handle_remote_sqlite_response(
225		ctx,
226		response.request_id,
227		RemoteSqliteResponse::Execute(response.data),
228		"remote_sqlite_execute",
229	);
230}
231
232pub async fn handle_remote_sqlite_execute_batch_response(
233	ctx: &mut EnvoyContext,
234	response: protocol::ToEnvoySqliteExecuteBatchResponse,
235) {
236	handle_remote_sqlite_response(
237		ctx,
238		response.request_id,
239		RemoteSqliteResponse::ExecuteBatch(response.data),
240		"remote_sqlite_execute_batch",
241	);
242}
243
244fn handle_sqlite_response(
245	ctx: &mut EnvoyContext,
246	request_id: u32,
247	response: SqliteResponse,
248	op: &str,
249) {
250	let request = ctx.sqlite_requests.remove(&request_id);
251
252	if let Some(request) = request {
253		METRICS.sqlite_requests_inflight.dec();
254		let _ = request.response_tx.send(Ok(response));
255	} else {
256		tracing::error!(
257			request_id,
258			op,
259			"received sqlite response for unknown request id"
260		);
261	}
262}
263
264fn handle_remote_sqlite_response(
265	ctx: &mut EnvoyContext,
266	request_id: u32,
267	response: RemoteSqliteResponse,
268	op: &str,
269) {
270	let request = ctx.remote_sqlite_requests.remove(&request_id);
271
272	if let Some(request) = request {
273		METRICS.remote_sqlite_requests_inflight.dec();
274		let session = request
275			.sent_session
276			.expect("a remote sqlite response must belong to a sent request");
277		let _ = request
278			.response_tx
279			.send(Ok(RemoteSqliteResponseEnvelope { response, session }));
280	} else {
281		tracing::error!(
282			request_id,
283			op,
284			"received remote sqlite response for unknown request id"
285		);
286	}
287}
288
289pub async fn send_single_sqlite_request(ctx: &mut EnvoyContext, request_id: u32) {
290	let request = ctx.sqlite_requests.get_mut(&request_id);
291	let Some(request) = request else { return };
292	if request.sent {
293		return;
294	}
295
296	let message =
297		match request.request.clone() {
298			SqliteRequest::GetPages(data) => protocol::ToRivet::ToRivetSqliteGetPagesRequest(
299				protocol::ToRivetSqliteGetPagesRequest { request_id, data },
300			),
301			SqliteRequest::Commit(data) => protocol::ToRivet::ToRivetSqliteCommitRequest(
302				protocol::ToRivetSqliteCommitRequest { request_id, data },
303			),
304			SqliteRequest::CommitStageBegin(data) => {
305				protocol::ToRivet::ToRivetSqliteCommitStageBeginRequest(
306					protocol::ToRivetSqliteCommitStageBeginRequest { request_id, data },
307				)
308			}
309			SqliteRequest::CommitStageSegment(data) => {
310				protocol::ToRivet::ToRivetSqliteCommitStageSegmentRequest(
311					protocol::ToRivetSqliteCommitStageSegmentRequest { request_id, data },
312				)
313			}
314			SqliteRequest::CommitFinalize(data) => {
315				protocol::ToRivet::ToRivetSqliteCommitFinalizeRequest(
316					protocol::ToRivetSqliteCommitFinalizeRequest { request_id, data },
317				)
318			}
319		};
320
321	ws_send(&ctx.shared, message).await;
322
323	if let Some(request) = ctx.sqlite_requests.get_mut(&request_id) {
324		request.sent = true;
325		request.timestamp = crate::time::Instant::now();
326	}
327}
328
329pub async fn send_single_remote_sqlite_request(ctx: &mut EnvoyContext, request_id: u32) {
330	let request = ctx.remote_sqlite_requests.get_mut(&request_id);
331	let Some(request) = request else { return };
332	if request.sent {
333		return;
334	}
335
336	let expected_session = request.expected_session;
337	let message = remote_sqlite_request_to_message(request_id, request.request.clone());
338
339	match ws_send_for_session(&ctx.shared, message, expected_session).await {
340		WsSendResult::Sent { session } => {
341			if let Some(request) = ctx.remote_sqlite_requests.get_mut(&request_id) {
342				request.sent = true;
343				request.sent_session = Some(session);
344				request.timestamp = crate::time::Instant::now();
345			}
346		}
347		WsSendResult::Unavailable if expected_session.is_none() => {
348			// Ordinary requests that have never crossed the socket remain eligible
349			// for replay. A transaction-affine request takes the error branch below.
350		}
351		WsSendResult::Unavailable => {
352			fail_remote_sqlite_session_request(ctx, request_id, None);
353		}
354		WsSendResult::StaleSession { current } => {
355			fail_remote_sqlite_session_request(ctx, request_id, current);
356		}
357	}
358}
359
360fn fail_remote_sqlite_session_request(
361	ctx: &mut EnvoyContext,
362	request_id: u32,
363	current: Option<u64>,
364) {
365	let Some(request) = ctx.remote_sqlite_requests.remove(&request_id) else {
366		return;
367	};
368	METRICS.remote_sqlite_requests_inflight.dec();
369	let expected = request
370		.expected_session
371		.expect("only session-affine requests fail session admission");
372	let _ = request.response_tx.send(Err(anyhow::anyhow!(
373		RemoteSqliteConnectionSessionLostError { expected, current }
374	)));
375}
376
377pub fn remote_sqlite_request_to_message(
378	request_id: u32,
379	request: RemoteSqliteRequest,
380) -> protocol::ToRivet {
381	match request {
382		RemoteSqliteRequest::Exec(data) => {
383			protocol::ToRivet::ToRivetSqliteExecRequest(protocol::ToRivetSqliteExecRequest {
384				request_id,
385				data,
386			})
387		}
388		RemoteSqliteRequest::Execute(data) => {
389			protocol::ToRivet::ToRivetSqliteExecuteRequest(protocol::ToRivetSqliteExecuteRequest {
390				request_id,
391				data,
392			})
393		}
394		RemoteSqliteRequest::ExecuteBatch(data) => {
395			protocol::ToRivet::ToRivetSqliteExecuteBatchRequest(
396				protocol::ToRivetSqliteExecuteBatchRequest { request_id, data },
397			)
398		}
399	}
400}
401
402pub async fn process_unsent_sqlite_requests(ctx: &mut EnvoyContext) {
403	let ws_available = {
404		let guard = ctx.shared.ws_tx.lock().await;
405		guard.is_some()
406	};
407
408	if !ws_available {
409		return;
410	}
411
412	let unsent: Vec<u32> = ctx
413		.sqlite_requests
414		.iter()
415		.filter(|(_, req)| !req.sent)
416		.map(|(id, _)| *id)
417		.collect();
418
419	for request_id in unsent {
420		send_single_sqlite_request(ctx, request_id).await;
421	}
422}
423
424pub async fn process_unsent_remote_sqlite_requests(ctx: &mut EnvoyContext) {
425	let ws_available = {
426		let guard = ctx.shared.ws_tx.lock().await;
427		guard.is_some()
428	};
429
430	if !ws_available {
431		return;
432	}
433
434	let unsent: Vec<u32> = ctx
435		.remote_sqlite_requests
436		.iter()
437		.filter(|(_, req)| !req.sent)
438		.map(|(id, _)| *id)
439		.collect();
440
441	for request_id in unsent {
442		send_single_remote_sqlite_request(ctx, request_id).await;
443	}
444}
445
446pub fn cleanup_old_sqlite_requests(ctx: &mut EnvoyContext) {
447	let now = crate::time::Instant::now();
448	let mut to_delete = Vec::new();
449
450	for (request_id, request) in &ctx.sqlite_requests {
451		if now.duration_since(request.timestamp).as_millis() > KV_EXPIRE_MS as u128 {
452			to_delete.push(*request_id);
453		}
454	}
455
456	for request_id in to_delete {
457		if let Some(request) = ctx.sqlite_requests.remove(&request_id) {
458			let kind = request.request.kind();
459			let was_sent = if request.sent { "true" } else { "false" };
460			METRICS
461				.sqlite_request_expired_total
462				.with_label_values(&[kind, was_sent])
463				.inc();
464			METRICS.sqlite_requests_inflight.dec();
465			tracing::warn!(
466				request_id,
467				kind,
468				was_sent = request.sent,
469				age_ms = now.duration_since(request.timestamp).as_millis() as u64,
470				"sqlite request expired by cleanup; if was_sent=true this indicates an abandoned in-flight request"
471			);
472			let _ = request
473				.response_tx
474				.send(Err(anyhow::anyhow!("sqlite request timed out")));
475		}
476	}
477}
478
479pub fn cleanup_old_remote_sqlite_requests(ctx: &mut EnvoyContext) {
480	let now = crate::time::Instant::now();
481	let mut to_delete = Vec::new();
482
483	for (request_id, request) in &ctx.remote_sqlite_requests {
484		if now.duration_since(request.timestamp).as_millis() > KV_EXPIRE_MS as u128 {
485			to_delete.push(*request_id);
486		}
487	}
488
489	for request_id in to_delete {
490		if let Some(request) = ctx.remote_sqlite_requests.remove(&request_id) {
491			let kind = request.request.kind();
492			let was_sent = if request.sent { "true" } else { "false" };
493			METRICS
494				.sqlite_request_expired_total
495				.with_label_values(&[kind, was_sent])
496				.inc();
497			METRICS.remote_sqlite_requests_inflight.dec();
498			tracing::warn!(
499				request_id,
500				kind,
501				was_sent = request.sent,
502				age_ms = now.duration_since(request.timestamp).as_millis() as u64,
503				"remote sqlite request expired by cleanup; if was_sent=true this indicates an abandoned in-flight request"
504			);
505			let _ = request
506				.response_tx
507				.send(Err(anyhow::anyhow!("remote sqlite request timed out")));
508		}
509	}
510}
511
512pub fn fail_sqlite_requests_with_shutdown(ctx: &mut EnvoyContext) {
513	for (_id, request) in ctx.sqlite_requests.drain() {
514		METRICS.sqlite_requests_inflight.dec();
515		let _ = request
516			.response_tx
517			.send(Err(anyhow::anyhow!(EnvoyShutdownError)));
518	}
519}
520
521/// Fails every sent-but-unanswered VFS request after the websocket drops.
522///
523/// Without this a sent request survives `ConnClose` and is only freed by the
524/// `KV_EXPIRE_MS` cleanup tick. The VFS callback that is parked on the response
525/// blocks the SQLite worker thread for that whole window, which stalls every
526/// subsequent SQLite call on the actor. Unsent requests are left in place for
527/// `process_unsent_sqlite_requests` to resend on reconnect.
528pub fn fail_sent_sqlite_requests_with_indeterminate_result(ctx: &mut EnvoyContext) {
529	let request_ids: Vec<u32> = ctx
530		.sqlite_requests
531		.iter()
532		.filter(|(_, request)| request.sent)
533		.map(|(request_id, _)| *request_id)
534		.collect();
535
536	for request_id in request_ids {
537		if let Some(request) = ctx.sqlite_requests.remove(&request_id) {
538			METRICS.sqlite_requests_inflight.dec();
539			let operation = request.request.kind();
540			tracing::warn!(
541				request_id,
542				operation,
543				"sqlite response lost after websocket disconnect"
544			);
545			let _ = request.response_tx.send(Err(anyhow::anyhow!(
546				RemoteSqliteIndeterminateResultError { operation }
547			)));
548		}
549	}
550}
551
552pub fn fail_remote_sqlite_requests_with_shutdown(ctx: &mut EnvoyContext) {
553	for (_id, request) in ctx.remote_sqlite_requests.drain() {
554		METRICS.remote_sqlite_requests_inflight.dec();
555		let _ = request
556			.response_tx
557			.send(Err(anyhow::anyhow!(EnvoyShutdownError)));
558	}
559}
560
561pub fn fail_sent_remote_sqlite_requests_with_indeterminate_result(ctx: &mut EnvoyContext) {
562	let request_ids: Vec<u32> = ctx
563		.remote_sqlite_requests
564		.iter()
565		.filter(|(_, request)| request.sent)
566		.map(|(request_id, _)| *request_id)
567		.collect();
568
569	for request_id in request_ids {
570		if let Some(request) = ctx.remote_sqlite_requests.remove(&request_id) {
571			METRICS.remote_sqlite_requests_inflight.dec();
572			let operation = request.request.operation();
573			tracing::warn!(
574				request_id,
575				operation,
576				"remote sqlite response lost after websocket disconnect"
577			);
578			let _ = request.response_tx.send(Err(anyhow::anyhow!(
579				RemoteSqliteIndeterminateResultError { operation }
580			)));
581		}
582	}
583}
584
585#[cfg(test)]
586mod tests {
587	use std::collections::HashMap;
588	use std::sync::Arc;
589
590	use vbare::OwnedVersionedData;
591
592	use super::*;
593	use crate::config::{
594		BoxFuture, EnvoyCallbacks, EnvoyConfig, HttpRequest, HttpResponse, WebSocketHandler,
595		WebSocketSender,
596	};
597	use crate::context::{SharedContext, WsTxMessage};
598	use crate::handle::EnvoyHandle;
599	use crate::utils::{BufferMap, RemoteSqliteIndeterminateResultError};
600
601	struct IdleCallbacks;
602
603	impl EnvoyCallbacks for IdleCallbacks {
604		fn on_actor_start(
605			&self,
606			_handle: EnvoyHandle,
607			_actor_id: String,
608			_generation: u32,
609			_config: protocol::ActorConfig,
610			_preloaded_kv: Option<protocol::PreloadedKv>,
611		) -> BoxFuture<anyhow::Result<()>> {
612			Box::pin(async { Ok(()) })
613		}
614
615		fn on_shutdown(&self) {}
616
617		fn fetch(
618			&self,
619			_handle: EnvoyHandle,
620			_actor_id: String,
621			_gateway_id: protocol::GatewayId,
622			_request_id: protocol::RequestId,
623			_request: HttpRequest,
624		) -> BoxFuture<anyhow::Result<HttpResponse>> {
625			Box::pin(async { anyhow::bail!("fetch should not be called in sqlite tests") })
626		}
627
628		fn websocket(
629			&self,
630			_handle: EnvoyHandle,
631			_actor_id: String,
632			_gateway_id: protocol::GatewayId,
633			_request_id: protocol::RequestId,
634			_request: HttpRequest,
635			_path: String,
636			_headers: HashMap<String, String>,
637			_is_hibernatable: bool,
638			_is_restoring_hibernatable: bool,
639			_sender: WebSocketSender,
640		) -> BoxFuture<anyhow::Result<WebSocketHandler>> {
641			Box::pin(async { anyhow::bail!("websocket should not be called in sqlite tests") })
642		}
643
644		fn can_hibernate(
645			&self,
646			_actor_id: &str,
647			_gateway_id: &protocol::GatewayId,
648			_request_id: &protocol::RequestId,
649			_request: &HttpRequest,
650		) -> BoxFuture<anyhow::Result<bool>> {
651			Box::pin(async { Ok(false) })
652		}
653	}
654
655	fn new_envoy_context() -> EnvoyContext {
656		let (envoy_tx, _envoy_rx) = tokio::sync::mpsc::unbounded_channel();
657		let shared = Arc::new(SharedContext {
658			config: EnvoyConfig {
659				version: 1,
660				endpoint: "http://127.0.0.1:1".to_string(),
661				token: None,
662				namespace: "test".to_string(),
663				pool_name: "test".to_string(),
664				prepopulate_actor_names: HashMap::new(),
665				metadata: None,
666				not_global: true,
667				debug_latency_ms: None,
668				callbacks: Arc::new(IdleCallbacks),
669			},
670			envoy_key: "test-envoy".to_string(),
671			envoy_tx,
672			actors: Arc::new(std::sync::Mutex::new(HashMap::new())),
673			actors_notify: Arc::new(tokio::sync::Notify::new()),
674			live_tunnel_requests: Arc::new(std::sync::Mutex::new(HashMap::new())),
675			pending_hibernation_restores: Arc::new(std::sync::Mutex::new(HashMap::new())),
676			ws_tx: Arc::new(tokio::sync::Mutex::new(
677				None::<tokio::sync::mpsc::UnboundedSender<WsTxMessage>>,
678			)),
679			http_ws_tx: Arc::new(tokio::sync::Mutex::new(None)),
680			connection_session: std::sync::atomic::AtomicU64::new(0),
681			next_connection_session: std::sync::atomic::AtomicU64::new(0),
682			connection_session_tx: tokio::sync::watch::channel(0).0,
683			protocol_metadata: Arc::new(tokio::sync::Mutex::new(None)),
684			shutting_down: std::sync::atomic::AtomicBool::new(false),
685			last_ping_ts: std::sync::atomic::AtomicI64::new(0),
686			stopped_tx: tokio::sync::watch::channel(true).0,
687		});
688
689		EnvoyContext {
690			shared,
691			shutting_down: false,
692			actors: HashMap::new(),
693			pending_events: HashMap::new(),
694			buffered_actor_messages: HashMap::new(),
695			kv_requests: HashMap::new(),
696			next_kv_request_id: 0,
697			sqlite_requests: HashMap::new(),
698			next_sqlite_request_id: 0,
699			remote_sqlite_requests: HashMap::new(),
700			next_remote_sqlite_request_id: 0,
701			request_to_actor: BufferMap::new(),
702			http_request_routes: BufferMap::new(),
703			http_message_indices: BufferMap::new(),
704			http_request_cancellations: HashMap::new(),
705			buffered_messages: Vec::new(),
706			processed_command_idx: HashMap::new(),
707		}
708	}
709
710	fn exec_request() -> protocol::SqliteExecRequest {
711		protocol::SqliteExecRequest {
712			namespace_id: "ns".to_string(),
713			actor_id: "actor".to_string(),
714			generation: 1,
715			sql: "select 1".to_string(),
716		}
717	}
718
719	fn execute_request() -> protocol::SqliteExecuteRequest {
720		protocol::SqliteExecuteRequest {
721			namespace_id: "ns".to_string(),
722			actor_id: "actor".to_string(),
723			generation: 1,
724			sql: "select ?".to_string(),
725			params: Some(vec![protocol::SqliteBindParam::SqliteValueInteger(
726				protocol::SqliteValueInteger { value: 1 },
727			)]),
728		}
729	}
730
731	fn execute_batch_request() -> protocol::SqliteExecuteBatchRequest {
732		protocol::SqliteExecuteBatchRequest {
733			namespace_id: "ns".to_string(),
734			actor_id: "actor".to_string(),
735			generation: 1,
736			statements: vec![
737				protocol::SqliteBatchStatement {
738					sql: "insert into t values (?)".to_string(),
739					params: Some(vec![protocol::SqliteBindParam::SqliteValueInteger(
740						protocol::SqliteValueInteger { value: 1 },
741					)]),
742				},
743				protocol::SqliteBatchStatement {
744					sql: "insert into t values (?)".to_string(),
745					params: Some(vec![protocol::SqliteBindParam::SqliteValueInteger(
746						protocol::SqliteValueInteger { value: 2 },
747					)]),
748				},
749			],
750		}
751	}
752
753	#[tokio::test]
754	async fn remote_sqlite_exec_response_matches_pending_request() {
755		let mut ctx = new_envoy_context();
756		let (ws_tx, mut ws_rx) = tokio::sync::mpsc::unbounded_channel();
757		let session = crate::connection::install_connection(&ctx.shared, ws_tx).await;
758		let (tx, rx) = oneshot::channel();
759
760		handle_remote_sqlite_request(
761			&mut ctx,
762			RemoteSqliteRequest::Exec(exec_request()),
763			None,
764			tx,
765		)
766		.await;
767		assert!(matches!(ws_rx.recv().await, Some(WsTxMessage::Send(_))));
768		assert!(ctx.remote_sqlite_requests.contains_key(&0));
769
770		handle_remote_sqlite_exec_response(
771			&mut ctx,
772			protocol::ToEnvoySqliteExecResponse {
773				request_id: 0,
774				data: protocol::SqliteExecResponse::SqliteExecOk(protocol::SqliteExecOk {
775					result: protocol::SqliteQueryResult {
776						columns: vec!["one".to_string()],
777						rows: vec![vec![protocol::SqliteColumnValue::SqliteValueInteger(
778							protocol::SqliteValueInteger { value: 1 },
779						)]],
780					},
781				}),
782			},
783		)
784		.await;
785
786		let response = rx
787			.await
788			.expect("response sender should complete")
789			.expect("response should succeed");
790		assert_eq!(response.session, session);
791		match response.response {
792			RemoteSqliteResponse::Exec(protocol::SqliteExecResponse::SqliteExecOk(ok)) => {
793				assert_eq!(ok.result.columns, vec!["one"]);
794				assert_eq!(ok.result.rows.len(), 1);
795			}
796			_ => panic!("unexpected response"),
797		}
798		assert!(ctx.remote_sqlite_requests.is_empty());
799	}
800
801	#[tokio::test]
802	async fn remote_sqlite_batch_uses_one_websocket_request() {
803		let mut ctx = new_envoy_context();
804		let (ws_tx, mut ws_rx) = tokio::sync::mpsc::unbounded_channel();
805		crate::connection::install_connection(&ctx.shared, ws_tx).await;
806		let (tx, _rx) = oneshot::channel();
807
808		handle_remote_sqlite_request(
809			&mut ctx,
810			RemoteSqliteRequest::ExecuteBatch(execute_batch_request()),
811			None,
812			tx,
813		)
814		.await;
815
816		assert!(matches!(ws_rx.recv().await, Some(WsTxMessage::Send(_))));
817		assert!(
818			ws_rx.try_recv().is_err(),
819			"a batch must serialize as one WebSocket message"
820		);
821		assert_eq!(ctx.remote_sqlite_requests.len(), 1);
822	}
823
824	#[test]
825	fn remote_sqlite_requests_reject_protocol_v3_serialization() {
826		let requests = vec![
827			RemoteSqliteRequest::Exec(exec_request()),
828			RemoteSqliteRequest::Execute(execute_request()),
829		];
830
831		for request in requests {
832			let message = remote_sqlite_request_to_message(7, request);
833			let err = protocol::versioned::ToRivet::wrap_latest(message)
834				.serialize(3)
835				.expect_err("remote sqlite requests should require protocol v4");
836			let compatibility = err
837				.downcast_ref::<protocol::versioned::ProtocolCompatibilityError>()
838				.expect("error should be a protocol compatibility error");
839			assert_eq!(
840				compatibility.feature,
841				protocol::versioned::ProtocolCompatibilityFeature::RemoteSqliteExecution
842			);
843			assert_eq!(compatibility.required_version, 4);
844			assert_eq!(compatibility.target_version, 3);
845		}
846	}
847
848	#[tokio::test]
849	async fn remote_sqlite_shutdown_cleanup_fails_pending_requests() {
850		let mut ctx = new_envoy_context();
851		let (tx, rx) = oneshot::channel();
852
853		handle_remote_sqlite_request(
854			&mut ctx,
855			RemoteSqliteRequest::Execute(execute_request()),
856			None,
857			tx,
858		)
859		.await;
860		fail_remote_sqlite_requests_with_shutdown(&mut ctx);
861
862		let err = rx
863			.await
864			.expect("response sender should complete")
865			.expect_err("pending request should fail during shutdown");
866		assert!(err.downcast_ref::<EnvoyShutdownError>().is_some());
867		assert!(ctx.remote_sqlite_requests.is_empty());
868	}
869
870	#[tokio::test]
871	async fn sent_remote_sqlite_request_fails_indeterminate_on_disconnect() {
872		let mut ctx = new_envoy_context();
873		let (ws_tx, mut ws_rx) = tokio::sync::mpsc::unbounded_channel();
874		crate::connection::install_connection(&ctx.shared, ws_tx).await;
875		let (tx, rx) = oneshot::channel();
876
877		handle_remote_sqlite_request(
878			&mut ctx,
879			RemoteSqliteRequest::Execute(execute_request()),
880			None,
881			tx,
882		)
883		.await;
884		assert!(matches!(ws_rx.recv().await, Some(WsTxMessage::Send(_))));
885		assert!(
886			ctx.remote_sqlite_requests
887				.get(&0)
888				.expect("request should be pending")
889				.sent
890		);
891
892		fail_sent_remote_sqlite_requests_with_indeterminate_result(&mut ctx);
893
894		let err = rx
895			.await
896			.expect("response sender should complete")
897			.expect_err("sent write should fail indeterminate on disconnect");
898		let indeterminate = err
899			.downcast_ref::<RemoteSqliteIndeterminateResultError>()
900			.expect("error should describe indeterminate remote sqlite result");
901		assert_eq!(indeterminate.operation, "execute");
902		assert!(ctx.remote_sqlite_requests.is_empty());
903	}
904
905	#[tokio::test]
906	async fn unsent_remote_sqlite_request_survives_disconnect_and_sends_on_reconnect() {
907		let mut ctx = new_envoy_context();
908		let (tx, mut rx) = oneshot::channel();
909
910		handle_remote_sqlite_request(
911			&mut ctx,
912			RemoteSqliteRequest::Execute(execute_request()),
913			None,
914			tx,
915		)
916		.await;
917		assert!(
918			!ctx.remote_sqlite_requests
919				.get(&0)
920				.expect("request should be pending")
921				.sent
922		);
923
924		fail_sent_remote_sqlite_requests_with_indeterminate_result(&mut ctx);
925		assert!(matches!(
926			rx.try_recv(),
927			Err(tokio::sync::oneshot::error::TryRecvError::Empty)
928		));
929		assert!(ctx.remote_sqlite_requests.contains_key(&0));
930
931		let (ws_tx, mut ws_rx) = tokio::sync::mpsc::unbounded_channel();
932		crate::connection::install_connection(&ctx.shared, ws_tx).await;
933		process_unsent_remote_sqlite_requests(&mut ctx).await;
934
935		assert!(matches!(ws_rx.recv().await, Some(WsTxMessage::Send(_))));
936		assert!(
937			ctx.remote_sqlite_requests
938				.get(&0)
939				.expect("request should still be pending")
940				.sent
941		);
942	}
943
944	#[tokio::test]
945	async fn transaction_request_never_crosses_connection_sessions() {
946		let mut ctx = new_envoy_context();
947		let (first_tx, _first_rx) = tokio::sync::mpsc::unbounded_channel();
948		let first_session = crate::connection::install_connection(&ctx.shared, first_tx).await;
949		crate::connection::remove_connection(&ctx.shared).await;
950		let (second_tx, mut second_rx) = tokio::sync::mpsc::unbounded_channel();
951		let second_session = crate::connection::install_connection(&ctx.shared, second_tx).await;
952		assert_ne!(first_session, second_session);
953
954		let (response_tx, response_rx) = oneshot::channel();
955		handle_remote_sqlite_request(
956			&mut ctx,
957			RemoteSqliteRequest::Execute(execute_request()),
958			Some(first_session),
959			response_tx,
960		)
961		.await;
962
963		let error = response_rx
964			.await
965			.expect("response sender should complete")
966			.expect_err("stale transaction session must fail before send");
967		let lost = error
968			.downcast_ref::<RemoteSqliteConnectionSessionLostError>()
969			.expect("error should identify the stale session");
970		assert_eq!(lost.expected, first_session);
971		assert_eq!(lost.current, Some(second_session));
972		assert!(second_rx.try_recv().is_err(), "stale request was sent");
973		assert!(ctx.remote_sqlite_requests.is_empty());
974	}
975}