aion_store_libsql/
connection.rs1use std::path::Path;
4use std::time::Duration;
5
6use aion_store::StoreError;
7
8use crate::config::{LibSqlConfig, LibSqlMode};
9
10const BUSY_TIMEOUT: Duration = Duration::from_secs(10);
29
30pub struct OpenedConnection {
32 pub database: libsql::Database,
34 pub connection: libsql::Connection,
36}
37
38pub async fn open_connection(config: &LibSqlConfig) -> Result<OpenedConnection, StoreError> {
49 let opened = match &config.mode {
50 LibSqlMode::Embedded { path } => open_embedded(path).await?,
51 LibSqlMode::EmbeddedReplica {
52 path,
53 primary_url,
54 auth_token,
55 } => {
56 open_embedded_replica(
57 path,
58 primary_url.clone(),
59 auth_token.clone(),
60 config.sync_interval_seconds,
61 )
62 .await?
63 }
64 };
65
66 opened
67 .connection
68 .busy_timeout(BUSY_TIMEOUT)
69 .map_err(|error| crate::error::libsql_error(&error))?;
70
71 let default_journal_mode =
82 matches!(config.mode, LibSqlMode::Embedded { .. }) && config.journal_mode.is_none();
83 if default_journal_mode {
84 execute_pragma(&opened.connection, "journal_mode", "wal").await?;
85 }
86
87 apply_pragmas(
88 &opened.connection,
89 config.journal_mode.as_deref(),
90 config.synchronous.as_deref(),
91 )
92 .await?;
93
94 Ok(opened)
95}
96
97async fn open_embedded(path: &Path) -> Result<OpenedConnection, StoreError> {
98 let database = libsql::Builder::new_local(path)
99 .build()
100 .await
101 .map_err(|error| crate::error::libsql_error(&error))?;
102
103 let connection = database
104 .connect()
105 .map_err(|error| crate::error::libsql_error(&error))?;
106
107 Ok(OpenedConnection {
108 database,
109 connection,
110 })
111}
112
113async fn open_embedded_replica(
114 path: &Path,
115 primary_url: String,
116 auth_token: String,
117 sync_interval_seconds: Option<u64>,
118) -> Result<OpenedConnection, StoreError> {
119 let mut builder = libsql::Builder::new_remote_replica(path, primary_url, auth_token);
120 if let Some(seconds) = sync_interval_seconds {
121 builder = builder.sync_interval(Duration::from_secs(seconds));
122 }
123
124 let db = builder
125 .build()
126 .await
127 .map_err(|error| crate::error::libsql_error(&error))?;
128
129 let connection = db
130 .connect()
131 .map_err(|error| crate::error::libsql_error(&error))?;
132
133 Ok(OpenedConnection {
134 database: db,
135 connection,
136 })
137}
138
139async fn apply_pragmas(
140 conn: &libsql::Connection,
141 journal_mode: Option<&str>,
142 synchronous: Option<&str>,
143) -> Result<(), StoreError> {
144 if let Some(value) = journal_mode {
145 execute_pragma(conn, "journal_mode", value).await?;
146 }
147
148 if let Some(value) = synchronous {
149 execute_pragma(conn, "synchronous", value).await?;
150 }
151
152 Ok(())
153}
154
155async fn execute_pragma(
156 conn: &libsql::Connection,
157 name: &str,
158 value: &str,
159) -> Result<(), StoreError> {
160 let value = validate_pragma_value(value)?;
161 let sql = format!("PRAGMA {name} = {value}");
162 conn.query(&sql, ())
163 .await
164 .map_err(|error| crate::error::libsql_error(&error))?;
165
166 Ok(())
167}
168
169fn validate_pragma_value(value: &str) -> Result<&str, StoreError> {
170 if !value.is_empty()
171 && value
172 .chars()
173 .all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
174 {
175 Ok(value)
176 } else {
177 Err(StoreError::Backend(format!(
178 "invalid libSQL PRAGMA value {value:?}"
179 )))
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use std::path::PathBuf;
186 use std::time::{SystemTime, UNIX_EPOCH};
187
188 use aion_store::StoreError;
189
190 use super::open_connection;
191 use crate::config::{LibSqlConfig, LibSqlMode};
192
193 #[tokio::test]
194 async fn opens_embedded_connection_and_queries() -> Result<(), StoreError> {
195 let config = LibSqlConfig {
196 mode: LibSqlMode::Embedded {
197 path: unique_temp_path("embedded-select"),
198 },
199 journal_mode: Some(String::from("wal")),
200 synchronous: Some(String::from("normal")),
201 sync_interval_seconds: None,
202 };
203
204 let opened = open_connection(&config).await?;
205 let conn = opened.connection;
206 let mut rows = conn
207 .query("SELECT 1", ())
208 .await
209 .map_err(|error| crate::error::libsql_error(&error))?;
210 let row = rows
211 .next()
212 .await
213 .map_err(|error| crate::error::libsql_error(&error))?
214 .ok_or_else(|| StoreError::Backend(String::from("SELECT 1 returned no rows")))?;
215 let value: i64 = row
216 .get(0)
217 .map_err(|error| crate::error::libsql_error(&error))?;
218
219 assert_eq!(value, 1);
220 Ok(())
221 }
222
223 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
224 async fn concurrent_writers_on_the_same_file_do_not_lock_each_other_out()
225 -> Result<(), StoreError> {
226 const ROUNDS: i64 = 40;
236 let path = unique_temp_path("concurrent-writers-shared-file");
237 let config = |path: &std::path::Path| LibSqlConfig {
238 mode: LibSqlMode::Embedded {
239 path: path.to_path_buf(),
240 },
241 journal_mode: None,
242 synchronous: None,
243 sync_interval_seconds: None,
244 };
245
246 let setup = open_connection(&config(&path)).await?;
247 setup
248 .connection
249 .execute("CREATE TABLE t (id INTEGER PRIMARY KEY, who INTEGER)", ())
250 .await
251 .map_err(|error| crate::error::libsql_error(&error))?;
252 drop(setup);
253
254 let writer = |who: i64, path: std::path::PathBuf| async move {
255 let opened = open_connection(&config(&path)).await?;
256 for _ in 0..ROUNDS {
257 let tx = opened
258 .connection
259 .transaction_with_behavior(libsql::TransactionBehavior::Immediate)
260 .await
261 .map_err(|error| crate::error::libsql_error(&error))?;
262 tx.execute("INSERT INTO t (who) VALUES (?1)", [who])
263 .await
264 .map_err(|error| crate::error::libsql_error(&error))?;
265 tx.commit()
266 .await
267 .map_err(|error| crate::error::libsql_error(&error))?;
268 }
269 Ok::<(), StoreError>(())
270 };
271
272 let a = tokio::spawn(writer(1, path.clone()));
273 let b = tokio::spawn(writer(2, path.clone()));
274 a.await
275 .map_err(|error| StoreError::Backend(error.to_string()))??;
276 b.await
277 .map_err(|error| StoreError::Backend(error.to_string()))??;
278
279 let reader = open_connection(&config(&path)).await?;
280 let mut rows = reader
281 .connection
282 .query("SELECT COUNT(*) FROM t", ())
283 .await
284 .map_err(|error| crate::error::libsql_error(&error))?;
285 let row = rows
286 .next()
287 .await
288 .map_err(|error| crate::error::libsql_error(&error))?
289 .ok_or_else(|| StoreError::Backend(String::from("count returned no row")))?;
290 let count: i64 = row
291 .get(0)
292 .map_err(|error| crate::error::libsql_error(&error))?;
293 assert_eq!(count, ROUNDS * 2, "every concurrent write committed");
294 Ok(())
295 }
296
297 #[tokio::test]
298 async fn maps_replica_open_failure_to_backend() -> Result<(), Box<dyn std::error::Error>> {
299 let config = LibSqlConfig {
300 mode: LibSqlMode::EmbeddedReplica {
301 path: unique_temp_path("replica-unavailable-primary"),
302 primary_url: String::from("http://127.0.0.1:9"),
303 auth_token: String::from("token"),
304 },
305 journal_mode: None,
306 synchronous: None,
307 sync_interval_seconds: Some(1),
308 };
309
310 match open_connection(&config).await {
311 Ok(_) => Err("expected embedded-replica open to fail for an invalid URL".into()),
312 Err(StoreError::Backend(_)) => Ok(()),
313 Err(other) => Err(format!("expected backend error, got {other:?}").into()),
314 }
315 }
316
317 fn unique_temp_path(name: &str) -> PathBuf {
318 let nanos = SystemTime::now()
319 .duration_since(UNIX_EPOCH)
320 .map_or(0, |duration| duration.as_nanos());
321 std::env::temp_dir().join(format!(
322 "aion-store-libsql-{name}-{}-{nanos}.db",
323 std::process::id()
324 ))
325 }
326}