Skip to content

Commit 456cd7a

Browse files
committed
fix(sqlite): wait through concurrent gateway writes
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
1 parent 56a155d commit 456cd7a

3 files changed

Lines changed: 47 additions & 1 deletion

File tree

‎architecture/gateway.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,9 @@ scope semantics.
738738
For in-memory SQLite, the adapter retains a dedicated keepalive connection for
739739
the store lifetime. Operational connection replacement therefore preserves the
740740
shared in-memory schema and objects instead of creating an empty database.
741+
SQLite connections wait up to 30 seconds for a concurrent writer to release
742+
the database lock, so short bursts of gateway writes do not surface as
743+
`SQLITE_BUSY` errors to callers.
741744

742745
Public protobuf APIs represent absolute times with `google.protobuf.Timestamp`
743746
and elapsed time with `google.protobuf.Duration`. The integer

‎crates/openshell-server/src/persistence/sqlite.rs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use std::path::{Path, PathBuf};
2121
use std::str::FromStr;
2222
use std::sync::Arc;
2323
use std::sync::atomic::{AtomicU64, Ordering};
24+
use std::time::Duration;
2425
use tokio::sync::Mutex;
2526

2627
static SQLITE_MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("./migrations/sqlite");
@@ -83,7 +84,12 @@ impl SqliteStore {
8384

8485
let mut options = SqliteConnectOptions::from_str(url)
8586
.map_err(|e| map_db_error(&e))?
86-
.create_if_missing(true);
87+
.create_if_missing(true)
88+
// A second gateway request can arrive while a write transaction
89+
// is committing. SQLite permits one writer at a time; wait for
90+
// the short-lived owner instead of returning SQLITE_BUSY to the
91+
// caller after the driver's default five seconds.
92+
.busy_timeout(Duration::from_secs(30));
8793

8894
if is_in_memory {
8995
if options.get_filename().as_os_str().is_empty()

‎crates/openshell-server/src/persistence/tests.rs‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,43 @@ async fn sqlite_connect_runs_embedded_migrations() {
158158
assert!(records.is_empty());
159159
}
160160

161+
#[tokio::test]
162+
async fn sqlite_write_waits_for_another_writer_to_finish() {
163+
use super::WriteCondition;
164+
use sqlx::{Connection, SqliteConnection};
165+
use std::time::Duration;
166+
167+
let tmp = tempfile::tempdir().expect("tempdir");
168+
let url = format!("sqlite:{}?mode=rwc", tmp.path().join("busy.db").display());
169+
let store = Store::connect(&url).await.expect("connect store");
170+
let mut locker = SqliteConnection::connect(&url)
171+
.await
172+
.expect("connect competing writer");
173+
sqlx::query("BEGIN IMMEDIATE")
174+
.execute(&mut locker)
175+
.await
176+
.expect("hold write lock");
177+
178+
let write = store.put_if(
179+
"workspace",
180+
"concurrent-workspace",
181+
"concurrent-workspace",
182+
"",
183+
b"workspace-payload",
184+
None,
185+
WriteCondition::MustCreate,
186+
);
187+
let unlock = async {
188+
tokio::time::sleep(Duration::from_secs(6)).await;
189+
sqlx::query("COMMIT")
190+
.execute(&mut locker)
191+
.await
192+
.expect("release write lock");
193+
};
194+
let (result, ()) = tokio::join!(write, unlock);
195+
result.expect("waiting writer should succeed after the lock is released");
196+
}
197+
161198
#[tokio::test]
162199
async fn sqlite_inference_route_removal_migration_deletes_only_managed_routes() {
163200
use sqlx::{Connection, SqliteConnection};

0 commit comments

Comments
 (0)