1mod enqueue;
8mod error;
9mod finalize;
10mod hydrate;
11mod import;
12mod lifecycle;
13mod migration;
14mod page;
15mod schema;
16
17pub use enqueue::enqueue;
18pub use error::{
19 ClaimError, EnqueueError, FinalizeError, ImportError, MutationError, PageError, SchemaError,
20 TransientKind,
21};
22pub use finalize::finalize_pending_delivery_for_migration;
23pub use import::import_for_migration;
24pub use lifecycle::{ack, claim, quarantine, release, renew, retry};
25pub use migration::{
26 CrateVersion, MIGRATIONS, Migration, MigrationCompatibility, MigrationCompatibilityError,
27 SCHEMA_VERSION,
28};
29pub use page::{SnapshotPager, begin_snapshot, page};
30pub use schema::check_schema;
31
32use dovecote::{EnqueueOutcome, FinalizeOutcome, ImportOutcome, ImportedDeliveryState, NewEvent};
33use sqlx::{AssertSqlSafe, SqlSafeStr, Sqlite, SqlitePool, Transaction, query, query_scalar};
34use std::time::Duration;
35
36pub const DEFAULT_BUSY_RETRIES: u32 = 3;
38pub const DEFAULT_BUSY_TIMEOUT: Duration = Duration::from_secs(5);
40
41pub async fn begin_write(pool: &SqlitePool) -> Result<Transaction<'static, Sqlite>, EnqueueError> {
44 begin_write_with_config(pool, BusyConfig::default()).await
45}
46
47pub async fn begin_enqueue(
49 pool: &SqlitePool,
50) -> Result<Transaction<'static, Sqlite>, EnqueueError> {
51 begin_write(pool).await
52}
53
54#[derive(Clone, Copy, Debug, Eq, PartialEq)]
56pub struct BusyConfig {
57 timeout: Duration,
58 retries: u32,
59}
60
61impl BusyConfig {
62 pub const fn new(timeout: Duration, retries: u32) -> Self {
66 Self { timeout, retries }
67 }
68
69 pub const fn with_retries(retries: u32) -> Self {
71 Self::new(DEFAULT_BUSY_TIMEOUT, retries)
72 }
73
74 pub const fn timeout(self) -> Duration {
76 self.timeout
77 }
78
79 pub const fn retries(self) -> u32 {
81 self.retries
82 }
83}
84
85impl Default for BusyConfig {
86 fn default() -> Self {
87 Self::new(DEFAULT_BUSY_TIMEOUT, DEFAULT_BUSY_RETRIES)
88 }
89}
90
91#[derive(Clone)]
93pub struct SqliteDovecote {
94 pool: SqlitePool,
95 busy: BusyConfig,
96}
97
98impl SqliteDovecote {
99 pub fn new(pool: SqlitePool) -> Self {
101 Self {
102 pool,
103 busy: BusyConfig::default(),
104 }
105 }
106
107 pub const fn with_busy_config(pool: SqlitePool, busy: BusyConfig) -> Self {
109 Self { pool, busy }
110 }
111
112 pub fn pool(&self) -> &SqlitePool {
113 &self.pool
114 }
115
116 pub const fn busy_config(&self) -> BusyConfig {
117 self.busy
118 }
119
120 pub async fn begin_write(&self) -> Result<Transaction<'static, Sqlite>, EnqueueError> {
123 begin_write_with_config(&self.pool, self.busy).await
124 }
125
126 pub async fn begin_enqueue(&self) -> Result<Transaction<'static, Sqlite>, EnqueueError> {
129 self.begin_write().await
130 }
131
132 pub async fn enqueue<'c>(
133 &self,
134 transaction: &mut Transaction<'c, Sqlite>,
135 event: NewEvent,
136 ) -> Result<EnqueueOutcome, EnqueueError> {
137 enqueue(transaction, event).await
138 }
139
140 pub async fn import_for_migration<'c>(
144 &self,
145 transaction: &mut Transaction<'c, Sqlite>,
146 event: NewEvent,
147 state: ImportedDeliveryState,
148 ) -> Result<ImportOutcome, ImportError> {
149 import_for_migration(transaction, event, state).await
150 }
151
152 pub async fn finalize_pending_delivery_for_migration<'c>(
156 &self,
157 transaction: &mut Transaction<'c, Sqlite>,
158 row_id: dovecote::RowId,
159 delivered_at: time::OffsetDateTime,
160 ) -> Result<FinalizeOutcome, FinalizeError> {
161 finalize_pending_delivery_for_migration(transaction, row_id, delivered_at).await
162 }
163
164 pub async fn check_schema(&self) -> Result<(), SchemaError> {
165 check_schema(&self.pool).await
166 }
167
168 pub async fn page(
169 &self,
170 after_row_id: Option<dovecote::RowId>,
171 limit: dovecote::Limit,
172 ) -> Result<Vec<dovecote::PagedEvent>, PageError> {
173 page(&self.pool, after_row_id, limit).await
174 }
175
176 pub async fn begin_snapshot(&self) -> Result<SnapshotPager, PageError> {
177 begin_snapshot(&self.pool).await
178 }
179
180 pub async fn claim(
181 &self,
182 worker: dovecote::WorkerId,
183 lease_for: dovecote::Lease,
184 limit: dovecote::Limit,
185 ) -> Result<Vec<dovecote::ClaimedEvent>, ClaimError> {
186 lifecycle::claim_with_config(&self.pool, worker, lease_for, limit, self.busy).await
187 }
188
189 pub async fn renew(
190 &self,
191 row_id: dovecote::RowId,
192 claim_token: &dovecote::ClaimToken,
193 lease_for: dovecote::Lease,
194 ) -> Result<(), MutationError> {
195 lifecycle::renew_with_config(&self.pool, row_id, claim_token, lease_for, self.busy).await
196 }
197
198 pub async fn ack(
199 &self,
200 row_id: dovecote::RowId,
201 claim_token: &dovecote::ClaimToken,
202 ) -> Result<(), MutationError> {
203 lifecycle::ack_with_config(&self.pool, row_id, claim_token, self.busy).await
204 }
205
206 pub async fn retry(
207 &self,
208 row_id: dovecote::RowId,
209 claim_token: &dovecote::ClaimToken,
210 failure: &dovecote::Failure,
211 backoff: dovecote::Delay,
212 ) -> Result<(), MutationError> {
213 lifecycle::retry_with_config(&self.pool, row_id, claim_token, failure, backoff, self.busy)
214 .await
215 }
216
217 pub async fn release(
218 &self,
219 row_id: dovecote::RowId,
220 claim_token: &dovecote::ClaimToken,
221 delay: dovecote::Delay,
222 ) -> Result<(), MutationError> {
223 lifecycle::release_with_config(&self.pool, row_id, claim_token, delay, self.busy).await
224 }
225
226 pub async fn quarantine(
227 &self,
228 row_id: dovecote::RowId,
229 claim_token: &dovecote::ClaimToken,
230 reason: &dovecote::QuarantineReason,
231 ) -> Result<(), MutationError> {
232 lifecycle::quarantine_with_config(&self.pool, row_id, claim_token, reason, self.busy).await
233 }
234}
235
236pub(crate) fn checked_milliseconds(value: Duration) -> Result<i64, String> {
238 if !value.is_zero() && !value.subsec_nanos().is_multiple_of(1_000_000) {
239 return Err("duration must be an exact whole number of milliseconds".to_owned());
240 }
241 i64::try_from(value.as_millis()).map_err(|_| "duration exceeds SQLite integer range".to_owned())
242}
243
244pub(crate) fn checked_busy_timeout(value: Duration) -> Result<i64, String> {
245 let milliseconds = checked_milliseconds(value)?;
246 if milliseconds > i64::from(i32::MAX) {
247 return Err("busy timeout exceeds SQLite's signed 32-bit millisecond range".to_owned());
248 }
249 Ok(milliseconds)
250}
251
252pub(crate) async fn begin_immediate(
255 pool: &SqlitePool,
256 busy: BusyConfig,
257 _operation: &'static str,
258) -> Result<Transaction<'static, Sqlite>, sqlx::Error> {
259 let mut connection = pool.acquire().await?;
260 install_foreign_keys(&mut connection).await?;
261 install_busy_timeout(&mut connection, busy).await?;
262 Transaction::begin(
263 sqlx::pool::MaybePoolConnection::PoolConnection(connection),
264 Some(AssertSqlSafe("BEGIN IMMEDIATE").into_sql_str()),
265 )
266 .await
267}
268
269async fn begin_write_with_config(
270 pool: &SqlitePool,
271 busy: BusyConfig,
272) -> Result<Transaction<'static, Sqlite>, EnqueueError> {
273 validate_busy_config(busy).map_err(|detail| EnqueueError::Configuration { detail })?;
274 let mut tries = 0;
275 loop {
276 match begin_immediate(pool, busy, "begin write transaction").await {
277 Ok(transaction) => return Ok(transaction),
278 Err(source) if error::is_busy(&source) && tries < busy.retries() => {
279 tries += 1;
280 }
281 Err(source) => return Err(EnqueueError::sql("begin write transaction", source)),
282 }
283 }
284}
285
286pub(crate) fn validate_busy_config(busy: BusyConfig) -> Result<(), String> {
287 checked_busy_timeout(busy.timeout()).map(|_| ())
288}
289
290pub(crate) async fn begin_read(
291 pool: &SqlitePool,
292) -> Result<Transaction<'static, Sqlite>, sqlx::Error> {
293 let mut connection = pool.acquire().await?;
294 install_foreign_keys(&mut connection).await?;
295 Transaction::begin(
296 sqlx::pool::MaybePoolConnection::PoolConnection(connection),
297 Some(AssertSqlSafe("BEGIN").into_sql_str()),
298 )
299 .await
300}
301
302pub(crate) async fn commit_transaction(
308 mut transaction: Transaction<'static, Sqlite>,
309) -> Result<(), sqlx::Error> {
310 use sqlx_core::transaction::TransactionManager;
311
312 let result =
313 <sqlx::sqlite::SqliteTransactionManager as TransactionManager>::commit(&mut *transaction)
314 .await;
315 if result.is_err() {
316 let _ = <sqlx::sqlite::SqliteTransactionManager as TransactionManager>::rollback(
317 &mut *transaction,
318 )
319 .await;
320 }
321 result
322}
323
324pub(crate) async fn install_foreign_keys(
325 connection: &mut sqlx::pool::PoolConnection<Sqlite>,
326) -> Result<(), sqlx::Error> {
327 query("PRAGMA foreign_keys = ON")
328 .execute(&mut **connection)
329 .await?;
330 let enabled: i64 = query_scalar("PRAGMA foreign_keys")
331 .fetch_one(&mut **connection)
332 .await?;
333 if enabled != 1 {
334 return Err(sqlx::Error::Protocol(
335 "SQLite foreign-key enforcement could not be enabled".to_owned(),
336 ));
337 }
338 Ok(())
339}
340
341pub(crate) async fn install_busy_timeout(
342 connection: &mut sqlx::pool::PoolConnection<Sqlite>,
343 busy: BusyConfig,
344) -> Result<(), sqlx::Error> {
345 let milliseconds = checked_busy_timeout(busy.timeout())
346 .map_err(|detail| sqlx::Error::Protocol(format!("invalid busy configuration: {detail}")))?;
347 let statement = AssertSqlSafe(format!("PRAGMA busy_timeout = {milliseconds}"));
348 query(statement).execute(&mut **connection).await?;
349 let installed: i64 = query_scalar("PRAGMA busy_timeout")
350 .fetch_one(&mut **connection)
351 .await?;
352 if installed != milliseconds {
353 return Err(sqlx::Error::Protocol(format!(
354 "SQLite busy timeout installation mismatch: requested {milliseconds}, installed {installed}"
355 )));
356 }
357 Ok(())
358}
359
360pub(crate) async fn transaction_is_write(
363 transaction: &mut Transaction<'_, Sqlite>,
364) -> Result<bool, sqlx::Error> {
365 let mut handle = transaction.lock_handle().await?;
366 let state = unsafe {
367 libsqlite3_sys::sqlite3_txn_state(handle.as_raw_handle().as_ptr(), std::ptr::null())
368 };
369 Ok(state == libsqlite3_sys::SQLITE_TXN_WRITE)
370}