Skip to content

Commit c12cd2a

Browse files
geeksilva97aduh95
authored andcommitted
sqlite: add timeout options to DatabaseSync
PR-URL: #57752 Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
1 parent 8856712 commit c12cd2a

File tree

5 files changed

+112
-0
lines changed

5 files changed

+112
-0
lines changed

doc/api/sqlite.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ console.log(query.all());
7878
<!-- YAML
7979
added: v22.5.0
8080
changes:
81+
- version: REPLACEME
82+
pr-url: https://github.com/nodejs/node/pull/57752
83+
description: Add `timeout` option.
8184
- version: v22.15.0
8285
pr-url: https://github.com/nodejs/node/pull/56991
8386
description: The `path` argument now supports Buffer and URL objects.
@@ -116,6 +119,9 @@ added: v22.5.0
116119
and the `loadExtension()` method are enabled.
117120
You can call `enableLoadExtension(false)` later to disable this feature.
118121
**Default:** `false`.
122+
* `timeout` {number} The [busy timeout][] in milliseconds. This is the maximum amount of
123+
time that SQLite will wait for a database lock to be released before
124+
returning an error. **Default:** `0`.
119125

120126
Constructs a new `DatabaseSync` instance.
121127

@@ -821,6 +827,7 @@ resolution handler passed to [`database.applyChangeset()`][]. See also
821827
[`sqlite3session_create()`]: https://www.sqlite.org/session/sqlite3session_create.html
822828
[`sqlite3session_delete()`]: https://www.sqlite.org/session/sqlite3session_delete.html
823829
[`sqlite3session_patchset()`]: https://www.sqlite.org/session/sqlite3session_patchset.html
830+
[busy timeout]: https://sqlite.org/c3ref/busy_timeout.html
824831
[connection]: https://www.sqlite.org/c3ref/sqlite3.html
825832
[data types]: https://www.sqlite.org/datatype3.html
826833
[double-quoted string literals]: https://www.sqlite.org/quirks.html#dblquote

src/node_sqlite.cc

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,8 @@ bool DatabaseSync::Open() {
731731
CHECK_ERROR_OR_THROW(env()->isolate(), this, r, SQLITE_OK, false);
732732
CHECK_EQ(foreign_keys_enabled, open_config_.get_enable_foreign_keys());
733733

734+
sqlite3_busy_timeout(connection_, open_config_.get_timeout());
735+
734736
if (allow_load_extension_) {
735737
if (env()->permission()->enabled()) [[unlikely]] {
736738
THROW_ERR_LOAD_SQLITE_EXTENSION(env(),
@@ -940,6 +942,23 @@ void DatabaseSync::New(const FunctionCallbackInfo<Value>& args) {
940942
}
941943
allow_load_extension = allow_extension_v.As<Boolean>()->Value();
942944
}
945+
946+
Local<Value> timeout_v;
947+
if (!options->Get(env->context(), env->timeout_string())
948+
.ToLocal(&timeout_v)) {
949+
return;
950+
}
951+
952+
if (!timeout_v->IsUndefined()) {
953+
if (!timeout_v->IsInt32()) {
954+
THROW_ERR_INVALID_ARG_TYPE(
955+
env->isolate(),
956+
"The \"options.timeout\" argument must be an integer.");
957+
return;
958+
}
959+
960+
open_config.set_timeout(timeout_v.As<Int32>()->Value());
961+
}
943962
}
944963

945964
new DatabaseSync(

src/node_sqlite.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,16 @@ class DatabaseOpenConfiguration {
3535

3636
inline void set_enable_dqs(bool flag) { enable_dqs_ = flag; }
3737

38+
inline void set_timeout(int timeout) { timeout_ = timeout; }
39+
40+
inline int get_timeout() { return timeout_; }
41+
3842
private:
3943
std::string location_;
4044
bool read_only_ = false;
4145
bool enable_foreign_keys_ = true;
4246
bool enable_dqs_ = false;
47+
int timeout_ = 0;
4348
};
4449

4550
class StatementSync;

test/parallel/test-sqlite-database-sync.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,15 @@ suite('DatabaseSync() constructor', () => {
7777
});
7878
});
7979

80+
test('throws if options.timeout is provided but is not an integer', (t) => {
81+
t.assert.throws(() => {
82+
new DatabaseSync('foo', { timeout: .99 });
83+
}, {
84+
code: 'ERR_INVALID_ARG_TYPE',
85+
message: /The "options\.timeout" argument must be an integer/,
86+
});
87+
});
88+
8089
test('is not read-only by default', (t) => {
8190
const dbPath = nextDb();
8291
const db = new DatabaseSync(dbPath);

test/parallel/test-sqlite-timeout.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
'use strict';
2+
require('../common');
3+
const tmpdir = require('../common/tmpdir');
4+
const { join } = require('node:path');
5+
const { DatabaseSync } = require('node:sqlite');
6+
const { test } = require('node:test');
7+
const { once } = require('node:events');
8+
const { Worker } = require('node:worker_threads');
9+
let cnt = 0;
10+
11+
tmpdir.refresh();
12+
13+
function nextDb() {
14+
return join(tmpdir.path, `database-${cnt++}.db`);
15+
}
16+
17+
test('waits to acquire lock', async (t) => {
18+
const DB_PATH = nextDb();
19+
const conn = new DatabaseSync(DB_PATH);
20+
t.after(() => {
21+
try {
22+
conn.close();
23+
} catch {
24+
// Ignore.
25+
}
26+
});
27+
28+
conn.exec('CREATE TABLE IF NOT EXISTS data (value TEXT)');
29+
conn.exec('BEGIN EXCLUSIVE;');
30+
const worker = new Worker(`
31+
'use strict';
32+
const { DatabaseSync } = require('node:sqlite');
33+
const { workerData } = require('node:worker_threads');
34+
const conn = new DatabaseSync(workerData.database, { timeout: 30000 });
35+
conn.exec('SELECT * FROM data');
36+
conn.close();
37+
`, {
38+
eval: true,
39+
workerData: {
40+
database: DB_PATH,
41+
}
42+
});
43+
await once(worker, 'online');
44+
conn.exec('COMMIT;');
45+
await once(worker, 'exit');
46+
});
47+
48+
test('throws if the lock cannot be acquired before timeout', (t) => {
49+
const DB_PATH = nextDb();
50+
const conn1 = new DatabaseSync(DB_PATH);
51+
t.after(() => {
52+
try {
53+
conn1.close();
54+
} catch {
55+
// Ignore.
56+
}
57+
});
58+
const conn2 = new DatabaseSync(DB_PATH, { timeout: 1 });
59+
t.after(() => {
60+
try {
61+
conn2.close();
62+
} catch {
63+
// Ignore.
64+
}
65+
});
66+
67+
conn1.exec('CREATE TABLE IF NOT EXISTS data (value TEXT)');
68+
conn1.exec('PRAGMA locking_mode = EXCLUSIVE; BEGIN EXCLUSIVE;');
69+
t.assert.throws(() => {
70+
conn2.exec('SELECT * FROM data');
71+
}, /database is locked/);
72+
});

0 commit comments

Comments
 (0)