Skip to main content

depot_client/
database.rs

1use std::sync::Arc;
2use std::time::Instant;
3
4use anyhow::{Result, anyhow};
5use async_trait::async_trait;
6use rivet_envoy_protocol as protocol;
7use tokio::runtime::Handle;
8
9use crate::{
10	query::{BindParam, ExecResult, ExecuteResult, QueryResult},
11	vfs::{
12		NativeVfsHandle, SqliteOpenPhase, SqliteTransportHandle, SqliteVfs, SqliteVfsMetrics,
13		SqliteVfsMetricsSnapshot, VfsConfig, VfsPreloadHintSnapshot,
14		fetch_initial_pages_for_registration,
15	},
16	worker::{
17		SqliteWorkerCloseTimeoutError, SqliteWorkerFatalError, SqliteWorkerHandle,
18		SqliteWorkerResult,
19	},
20};
21
22#[derive(Clone)]
23pub struct NativeDatabaseHandle {
24	vfs: NativeVfsHandle,
25	worker: SqliteWorkerHandle,
26}
27
28pub fn vfs_name_for_actor_database(actor_id: &str, generation: u64) -> String {
29	format!("envoy-sqlite-{actor_id}-g{generation}")
30}
31
32struct GenerationFencedTransport {
33	inner: SqliteTransportHandle,
34	generation: u64,
35}
36
37#[async_trait]
38impl crate::vfs::SqliteTransport for GenerationFencedTransport {
39	async fn get_pages(
40		&self,
41		mut request: protocol::SqliteGetPagesRequest,
42	) -> Result<protocol::SqliteGetPagesResponse> {
43		request.expected_generation.get_or_insert(self.generation);
44		self.inner.get_pages(request).await
45	}
46
47	async fn commit(
48		&self,
49		mut request: protocol::SqliteCommitRequest,
50	) -> Result<protocol::SqliteCommitResponse> {
51		request.expected_generation.get_or_insert(self.generation);
52		self.inner.commit(request).await
53	}
54
55	async fn commit_stage_begin(
56		&self,
57		mut request: protocol::SqliteCommitStageBeginRequest,
58	) -> Result<protocol::SqliteCommitStageBeginResponse> {
59		request.expected_generation.get_or_insert(self.generation);
60		self.inner.commit_stage_begin(request).await
61	}
62
63	async fn commit_stage_segment(
64		&self,
65		mut request: protocol::SqliteCommitStageSegmentRequest,
66	) -> Result<protocol::SqliteCommitStageSegmentResponse> {
67		request.expected_generation.get_or_insert(self.generation);
68		self.inner.commit_stage_segment(request).await
69	}
70
71	async fn commit_finalize(
72		&self,
73		mut request: protocol::SqliteCommitFinalizeRequest,
74	) -> Result<protocol::SqliteCommitFinalizeResponse> {
75		request.expected_generation.get_or_insert(self.generation);
76		self.inner.commit_finalize(request).await
77	}
78}
79
80pub async fn open_database_from_transport(
81	transport: SqliteTransportHandle,
82	actor_id: String,
83	generation: u64,
84	rt_handle: Handle,
85	metrics: Option<Arc<dyn SqliteVfsMetrics>>,
86) -> Result<NativeDatabaseHandle> {
87	let open_timer = SqliteOpenTimer::new(&metrics);
88	let vfs_name = vfs_name_for_actor_database(&actor_id, generation);
89	let config = VfsConfig::default();
90	let transport: SqliteTransportHandle = Arc::new(GenerationFencedTransport {
91		inner: transport,
92		generation,
93	});
94	let preload_start = Instant::now();
95	let preload_result =
96		fetch_initial_pages_for_registration(transport.clone(), &actor_id, generation, &config)
97			.await
98			.map_err(|err| anyhow!("failed to preload sqlite pages: {err}"));
99	let initial_pages = observe_open_phase_result(
100		&metrics,
101		SqliteOpenPhase::InitialPreload,
102		preload_start,
103		preload_result,
104	)?;
105	record_startup_preload_pages(
106		&metrics,
107		u64::from(initial_pages.requested_page_count),
108		initial_pages.pages.len() as u64,
109	);
110
111	let vfs_register_start = Instant::now();
112	let vfs_result = SqliteVfs::register_with_transport_and_initial_pages(
113		&vfs_name,
114		transport,
115		actor_id.clone(),
116		rt_handle,
117		config,
118		initial_pages,
119		metrics.clone(),
120	)
121	.map(Arc::new)
122	.map_err(|err| anyhow!("failed to register sqlite VFS: {err}"));
123	let vfs = observe_open_phase_result(
124		&metrics,
125		SqliteOpenPhase::VfsRegister,
126		vfs_register_start,
127		vfs_result,
128	)?;
129
130	let worker_ready_start = Instant::now();
131	let worker_ready_result: Result<NativeDatabaseHandle> = async {
132		let native_db = NativeDatabaseHandle::new_with_metrics(vfs, actor_id, metrics.clone())?;
133		native_db.initialize().await?;
134		Ok(native_db)
135	}
136	.await;
137	let native_db = observe_open_phase_result(
138		&metrics,
139		SqliteOpenPhase::WorkerReady,
140		worker_ready_start,
141		worker_ready_result,
142	)?;
143	open_timer.finish_success();
144	Ok(native_db)
145}
146
147/// Records the total SQLite open metric when the open attempt leaves scope.
148///
149/// Individual open phases record their own durations. This guard owns the total
150/// duration so early returns cannot forget the total error metric.
151struct SqliteOpenTimer {
152	metrics: Option<Arc<dyn SqliteVfsMetrics>>,
153	started_at: Instant,
154	finished: bool,
155}
156
157impl SqliteOpenTimer {
158	fn new(metrics: &Option<Arc<dyn SqliteVfsMetrics>>) -> Self {
159		Self {
160			metrics: metrics.clone(),
161			started_at: Instant::now(),
162			finished: false,
163		}
164	}
165
166	fn finish_success(mut self) {
167		observe_open_phase(
168			&self.metrics,
169			SqliteOpenPhase::Total,
170			"success",
171			self.started_at,
172		);
173		self.finished = true;
174	}
175}
176
177impl Drop for SqliteOpenTimer {
178	fn drop(&mut self) {
179		if self.finished {
180			return;
181		}
182
183		observe_open_phase(
184			&self.metrics,
185			SqliteOpenPhase::Total,
186			"error",
187			self.started_at,
188		);
189	}
190}
191
192fn observe_open_phase_result<T, E>(
193	metrics: &Option<Arc<dyn SqliteVfsMetrics>>,
194	phase: SqliteOpenPhase,
195	start: Instant,
196	result: std::result::Result<T, E>,
197) -> std::result::Result<T, E> {
198	let outcome = if result.is_ok() { "success" } else { "error" };
199	observe_open_phase(metrics, phase, outcome, start);
200	result
201}
202
203fn observe_open_phase(
204	metrics: &Option<Arc<dyn SqliteVfsMetrics>>,
205	phase: SqliteOpenPhase,
206	outcome: &'static str,
207	start: Instant,
208) {
209	if let Some(metrics) = metrics {
210		metrics.observe_open_phase(phase, outcome, start.elapsed().as_nanos() as u64);
211	}
212}
213
214fn record_startup_preload_pages(
215	metrics: &Option<Arc<dyn SqliteVfsMetrics>>,
216	requested_pages: u64,
217	loaded_pages: u64,
218) {
219	if let Some(metrics) = metrics {
220		if requested_pages > 0 {
221			metrics.record_startup_preload_pages("requested", requested_pages);
222		}
223		if loaded_pages > 0 {
224			metrics.record_startup_preload_pages("loaded", loaded_pages);
225		}
226	}
227}
228
229impl NativeDatabaseHandle {
230	pub fn new(vfs: NativeVfsHandle, file_name: String) -> Result<Self> {
231		Self::new_with_metrics(vfs, file_name, None)
232	}
233
234	pub fn new_with_metrics(
235		vfs: NativeVfsHandle,
236		file_name: String,
237		metrics: Option<Arc<dyn SqliteVfsMetrics>>,
238	) -> Result<Self> {
239		Ok(Self {
240			worker: SqliteWorkerHandle::start(vfs.clone(), file_name, metrics)?,
241			vfs,
242		})
243	}
244
245	pub async fn exec(&self, sql: String) -> Result<QueryResult> {
246		self.check_fatal_error()?;
247		self.map_worker_result(self.worker.exec(sql).await)
248	}
249
250	pub async fn exec_profiled(&self, sql: String) -> Result<SqliteWorkerResult<QueryResult>> {
251		self.check_fatal_error()?;
252		self.map_worker_result(self.worker.exec_profiled(sql).await)
253	}
254
255	pub async fn query(&self, sql: String, params: Option<Vec<BindParam>>) -> Result<QueryResult> {
256		self.execute(sql, params).await.map(|result| QueryResult {
257			columns: result.columns,
258			rows: result.rows,
259		})
260	}
261
262	pub async fn run(&self, sql: String, params: Option<Vec<BindParam>>) -> Result<ExecResult> {
263		self.execute(sql, params).await.map(|result| ExecResult {
264			changes: result.changes,
265		})
266	}
267
268	pub async fn execute(
269		&self,
270		sql: String,
271		params: Option<Vec<BindParam>>,
272	) -> Result<ExecuteResult> {
273		self.check_fatal_error()?;
274		self.map_worker_result(self.worker.execute(sql, params).await)
275	}
276
277	pub async fn execute_profiled(
278		&self,
279		sql: String,
280		params: Option<Vec<BindParam>>,
281	) -> Result<SqliteWorkerResult<ExecuteResult>> {
282		self.check_fatal_error()?;
283		self.map_worker_result(self.worker.execute_profiled(sql, params).await)
284	}
285
286	pub async fn close(&self) -> Result<()> {
287		self.map_worker_close_result(self.worker.close().await)
288	}
289
290	/// Closes the database without returning while its worker still owns SQLite.
291	/// Any returned error is reported only after the worker thread has terminated.
292	pub async fn close_and_wait(&self) -> Result<()> {
293		self.map_worker_close_result(self.worker.close_and_wait().await)
294	}
295
296	fn map_worker_close_result(&self, result: Result<()>) -> Result<()> {
297		match result {
298			Ok(()) => Ok(()),
299			// The caller uses this concrete outcome to retain the native handle for
300			// a later strict join. Never hide it behind a previously recorded VFS
301			// failure while the worker may still own SQLite.
302			Err(error)
303				if error
304					.downcast_ref::<SqliteWorkerCloseTimeoutError>()
305					.is_some() =>
306			{
307				Err(error)
308			}
309			Err(error) => Err(self.fatal_error().unwrap_or(error)),
310		}
311	}
312
313	pub async fn wait_for_worker_failure(&self) -> bool {
314		self.worker.wait_for_failure().await
315	}
316
317	pub fn take_last_kv_error(&self) -> Option<String> {
318		self.vfs.take_last_error()
319	}
320
321	pub fn clone_fatal_error(&self) -> Option<String> {
322		self.vfs.clone_fatal_error()
323	}
324
325	pub fn snapshot_preload_hints(&self) -> VfsPreloadHintSnapshot {
326		self.vfs.snapshot_preload_hints()
327	}
328
329	/// Reads back a file the connection wrote through its own VFS, such as a `VACUUM INTO` output.
330	pub fn read_vfs_file(&self, path: &str) -> Option<Vec<u8>> {
331		self.vfs.read_aux_file(path)
332	}
333
334	/// Drops a file the connection wrote through its own VFS, releasing the memory holding it.
335	pub fn delete_vfs_file(&self, path: &str) {
336		self.vfs.delete_aux_file(path);
337	}
338
339	pub fn sqlite_vfs_metrics(&self) -> SqliteVfsMetricsSnapshot {
340		self.vfs.sqlite_vfs_metrics()
341	}
342
343	#[cfg(test)]
344	pub(crate) async fn pause_for_test(&self) -> tokio::sync::oneshot::Sender<()> {
345		self.worker.pause_for_test().await
346	}
347
348	#[cfg(test)]
349	pub(crate) fn is_closing_for_test(&self) -> bool {
350		self.worker.is_closing_for_test()
351	}
352
353	#[cfg(test)]
354	pub(crate) async fn panic_worker_for_test(&self) {
355		self.worker.panic_for_test().await
356	}
357
358	async fn initialize(&self) -> Result<()> {
359		self.map_worker_result(self.worker.wait_ready().await)
360	}
361
362	fn check_fatal_error(&self) -> Result<()> {
363		if let Some(error) = self.fatal_error() {
364			return Err(error);
365		}
366
367		Ok(())
368	}
369
370	fn map_worker_result<T>(&self, result: Result<T>) -> Result<T> {
371		match result {
372			Ok(value) => {
373				self.check_fatal_error()?;
374				Ok(value)
375			}
376			Err(error) => Err(self.fatal_error().unwrap_or(error)),
377		}
378	}
379
380	fn fatal_error(&self) -> Option<anyhow::Error> {
381		self.clone_fatal_error()
382			.map(|message| SqliteWorkerFatalError::new(message).into())
383	}
384}
385
386#[cfg(test)]
387mod tests {
388	use super::vfs_name_for_actor_database;
389
390	#[test]
391	fn vfs_name_includes_actor_and_generation() {
392		assert_eq!(
393			vfs_name_for_actor_database("actor-123", 42),
394			"envoy-sqlite-actor-123-g42"
395		);
396	}
397}