Skip to content

Commit bd30186

Browse files
authored
Add support for connection "timeout" option (#170)
Fixes #160
2 parents b9a839a + e3a6d73 commit bd30186

File tree

10 files changed

+81
-17
lines changed

10 files changed

+81
-17
lines changed

Cargo.lock

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ crate-type = ["cdylib"]
1212

1313
[dependencies]
1414
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
15-
libsql = { git = "https://github.com/tursodatabase/libsql/", rev = "9aa89fee3a096538336d30f3d9e9f2df8ba6677d", features = ["encryption"] }
15+
libsql = { git = "https://github.com/tursodatabase/libsql/", rev = "e40c4319829e0805313fadcaa19034b89b7708ad", features = ["encryption"] }
1616
tracing = "0.1"
1717
once_cell = "1.18.0"
1818
tokio = { version = "1.29.1", features = [ "rt-multi-thread" ] }

docs/api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ You can use the `options` parameter to specify various options. Options supporte
2121
- `syncUrl`: open the database as embedded replica synchronizing from the provided URL.
2222
- `syncPeriod`: synchronize the database periodically every `syncPeriod` seconds.
2323
- `authToken`: authentication token for the provider URL (optional).
24+
- `timeout`: number of milliseconds to wait on locked database before returning `SQLITE_BUSY` error
2425

2526
The function returns a `Database` object.
2627

index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ class Database {
8383
} else {
8484
const authToken = opts?.authToken ?? "";
8585
const encryptionKey = opts?.encryptionKey ?? "";
86-
this.db = databaseOpen(path, authToken, encryptionCipher, encryptionKey);
86+
const timeout = opts?.timeout ?? 0.0;
87+
this.db = databaseOpen(path, authToken, encryptionCipher, encryptionKey, timeout);
8788
}
8889
// TODO: Use a libSQL API for this?
8990
this.memory = path === ":memory:";

integration-tests/package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

integration-tests/tests/async.test.js

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import test from "ava";
2+
import crypto from 'crypto';
3+
import fs from 'fs';
4+
25

36
test.beforeEach(async (t) => {
47
const [db, errorType] = await connect();
@@ -343,6 +346,28 @@ test.serial("Statement.interrupt()", async (t) => {
343346
});
344347
});
345348

349+
test.serial("Timeout option", async (t) => {
350+
const timeout = 1000;
351+
const path = genDatabaseFilename();
352+
const [conn1] = await connect(path);
353+
await conn1.exec("CREATE TABLE t(x)");
354+
await conn1.exec("BEGIN IMMEDIATE");
355+
await conn1.exec("INSERT INTO t VALUES (1)")
356+
const options = { timeout };
357+
const [conn2] = await connect(path, options);
358+
const start = Date.now();
359+
try {
360+
await conn2.exec("INSERT INTO t VALUES (1)")
361+
} catch (e) {
362+
t.is(e.code, "SQLITE_BUSY");
363+
const end = Date.now();
364+
const elapsed = end - start;
365+
// Allow some tolerance for the timeout.
366+
t.is(elapsed > timeout/2, true);
367+
}
368+
fs.unlinkSync(path);
369+
});
370+
346371
test.serial("Concurrent writes over same connection", async (t) => {
347372
const db = t.context.db;
348373
await db.exec(`
@@ -360,12 +385,16 @@ test.serial("Concurrent writes over same connection", async (t) => {
360385
t.is(rows.length, 1000);
361386
});
362387

363-
const connect = async (path_opt) => {
388+
const connect = async (path_opt, options = {}) => {
364389
const path = path_opt ?? "hello.db";
365390
const provider = process.env.PROVIDER;
366391
const database = process.env.LIBSQL_DATABASE ?? path;
367392
const x = await import("libsql/promise");
368-
const options = {};
369393
const db = new x.default(database, options);
370394
return [db, x.SqliteError];
371395
};
396+
397+
/// Generate a unique database filename
398+
const genDatabaseFilename = () => {
399+
return `test-${crypto.randomBytes(8).toString('hex')}.db`;
400+
};

integration-tests/tests/sync.test.js

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import test from "ava";
2+
import crypto from 'crypto';
3+
import fs from 'fs';
24

35
test.beforeEach(async (t) => {
46
const [db, errorType, provider] = await connect();
@@ -408,21 +410,46 @@ test.serial("Database.exec() after close()", async (t) => {
408410
});
409411
});
410412

411-
const connect = async (path_opt) => {
413+
test.serial("Timeout option", async (t) => {
414+
const timeout = 1000;
415+
const path = genDatabaseFilename();
416+
const [conn1] = await connect(path);
417+
conn1.exec("CREATE TABLE t(x)");
418+
conn1.exec("BEGIN IMMEDIATE");
419+
conn1.exec("INSERT INTO t VALUES (1)")
420+
const options = { timeout };
421+
const [conn2] = await connect(path, options);
422+
const start = Date.now();
423+
try {
424+
conn2.exec("INSERT INTO t VALUES (1)")
425+
} catch (e) {
426+
t.is(e.code, "SQLITE_BUSY");
427+
const end = Date.now();
428+
const elapsed = end - start;
429+
// Allow some tolerance for the timeout.
430+
t.is(elapsed > timeout/2, true);
431+
}
432+
fs.unlinkSync(path);
433+
});
434+
435+
const connect = async (path_opt, options = {}) => {
412436
const path = path_opt ?? "hello.db";
413437
const provider = process.env.PROVIDER;
414438
if (provider === "libsql") {
415439
const database = process.env.LIBSQL_DATABASE ?? path;
416440
const x = await import("libsql");
417-
const options = {};
418441
const db = new x.default(database, options);
419442
return [db, x.SqliteError, provider];
420443
}
421444
if (provider == "sqlite") {
422445
const x = await import("better-sqlite3");
423-
const options = {};
424446
const db = x.default(path, options);
425447
return [db, x.SqliteError, provider];
426448
}
427449
throw new Error("Unknown provider: " + provider);
428450
};
451+
452+
/// Generate a unique database filename
453+
const genDatabaseFilename = () => {
454+
return `test-${crypto.randomBytes(8).toString('hex')}.db`;
455+
};

promise.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ class Database {
8686
} else {
8787
const authToken = opts?.authToken ?? "";
8888
const encryptionKey = opts?.encryptionKey ?? "";
89-
this.db = databaseOpen(path, authToken, encryptionCipher, encryptionKey);
89+
const timeout = opts?.timeout ?? 0.0;
90+
this.db = databaseOpen(path, authToken, encryptionCipher, encryptionKey, timeout);
9091
}
9192
// TODO: Use a libSQL API for this?
9293
this.memory = path === ":memory:";

src/database.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ impl Database {
3434
let auth_token = cx.argument::<JsString>(1)?.value(&mut cx);
3535
let encryption_cipher = cx.argument::<JsString>(2)?.value(&mut cx);
3636
let encryption_key = cx.argument::<JsString>(3)?.value(&mut cx);
37+
let busy_timeout = cx.argument::<JsNumber>(4)?.value(&mut cx);
3738
let db = if is_remote_path(&db_path) {
3839
let version = version("remote");
3940
trace!("Opening remote database: {}", db_path);
@@ -57,6 +58,10 @@ impl Database {
5758
let conn = db
5859
.connect()
5960
.or_else(|err| throw_libsql_error(&mut cx, err))?;
61+
if busy_timeout > 0.0 {
62+
conn.busy_timeout(Duration::from_millis(busy_timeout as u64))
63+
.or_else(|err| throw_libsql_error(&mut cx, err))?;
64+
}
6065
let db = Database::new(db, conn);
6166
Ok(cx.boxed(db))
6267
}

types/promise.d.ts.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)