GeezerVison is showing github.com — sqlite: add timeout options to DatabaseSync · nodejs/node@0df87e0
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/sqlite.md +7Lines changed: 7 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -78,6 +78,9 @@ console.log(query.all()); | 78 | 78 | <!-- YAML | 79 | 79 | added: v22.5.0 | 80 | 80 | changes: | | 81 | + - version: REPLACEME | | 82 | + pr-url: https://github.com/nodejs/node/pull/57752 | | 83 | + description: Add `timeout` option. | 81 | 84 | - version: v23.10.0 | 82 | 85 | pr-url: https://github.com/nodejs/node/pull/56991 | 83 | 86 | description: The `path` argument now supports Buffer and URL objects. | @@ -116,6 +119,9 @@ added: v22.5.0 | 116 | 119 | and the `loadExtension()` method are enabled. | 117 | 120 | You can call `enableLoadExtension(false)` later to disable this feature. | 118 | 121 | **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`. | 119 | 125 |
| 120 | 126 | Constructs a new `DatabaseSync` instance. | 121 | 127 |
| @@ -843,6 +849,7 @@ resolution handler passed to [`database.applyChangeset()`][]. See also | 843 | 849 | [`sqlite3session_create()`]: https://www.sqlite.org/session/sqlite3session_create.html | 844 | 850 | [`sqlite3session_delete()`]: https://www.sqlite.org/session/sqlite3session_delete.html | 845 | 851 | [`sqlite3session_patchset()`]: https://www.sqlite.org/session/sqlite3session_patchset.html | | 852 | +[busy timeout]: https://sqlite.org/c3ref/busy_timeout.html | 846 | 853 | [connection]: https://www.sqlite.org/c3ref/sqlite3.html | 847 | 854 | [data types]: https://www.sqlite.org/datatype3.html | 848 | 855 | [double-quoted string literals]: https://www.sqlite.org/quirks.html#dblquote | |
Collapse file Copy file name to clipboardExpand all lines: src/node_sqlite.cc +19Lines changed: 19 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -732,6 +732,8 @@ bool DatabaseSync::Open() { | 732 | 732 | CHECK_ERROR_OR_THROW(env()->isolate(), this, r, SQLITE_OK, false); | 733 | 733 | CHECK_EQ(foreign_keys_enabled, open_config_.get_enable_foreign_keys()); | 734 | 734 |
| | 735 | + sqlite3_busy_timeout(connection_, open_config_.get_timeout()); | | 736 | + | 735 | 737 | if (allow_load_extension_) { | 736 | 738 | if (env()->permission()->enabled()) [[unlikely]] { | 737 | 739 | THROW_ERR_LOAD_SQLITE_EXTENSION(env(), | @@ -942,6 +944,23 @@ void DatabaseSync::New(const FunctionCallbackInfo<Value>& args) { | 942 | 944 | } | 943 | 945 | allow_load_extension = allow_extension_v.As<Boolean>()->Value(); | 944 | 946 | } | | 947 | + | | 948 | + Local<Value> timeout_v; | | 949 | + if (!options->Get(env->context(), env->timeout_string()) | | 950 | + .ToLocal(&timeout_v)) { | | 951 | + return; | | 952 | + } | | 953 | + | | 954 | + if (!timeout_v->IsUndefined()) { | | 955 | + if (!timeout_v->IsInt32()) { | | 956 | + THROW_ERR_INVALID_ARG_TYPE( | | 957 | + env->isolate(), | | 958 | + "The \"options.timeout\" argument must be an integer."); | | 959 | + return; | | 960 | + } | | 961 | + | | 962 | + open_config.set_timeout(timeout_v.As<Int32>()->Value()); | | 963 | + } | 945 | 964 | } | 946 | 965 |
| 947 | 966 | new DatabaseSync( | |
Collapse file Copy file name to clipboardExpand all lines: src/node_sqlite.h +5Lines changed: 5 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -35,11 +35,16 @@ class DatabaseOpenConfiguration { | 35 | 35 |
| 36 | 36 | inline void set_enable_dqs(bool flag) { enable_dqs_ = flag; } | 37 | 37 |
| | 38 | + inline void set_timeout(int timeout) { timeout_ = timeout; } | | 39 | + | | 40 | + inline int get_timeout() { return timeout_; } | | 41 | + | 38 | 42 | private: | 39 | 43 | std::string location_; | 40 | 44 | bool read_only_ = false; | 41 | 45 | bool enable_foreign_keys_ = true; | 42 | 46 | bool enable_dqs_ = false; | | 47 | + int timeout_ = 0; | 43 | 48 | }; | 44 | 49 |
| 45 | 50 | class StatementSync; | |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-sqlite-database-sync.js +9Lines changed: 9 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -77,6 +77,15 @@ suite('DatabaseSync() constructor', () => { | 77 | 77 | }); | 78 | 78 | }); | 79 | 79 |
| | 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 | + | 80 | 89 | test('is not read-only by default', (t) => { | 81 | 90 | const dbPath = nextDb(); | 82 | 91 | const db = new DatabaseSync(dbPath); | |
Collapse file Copy file name to clipboard +72Lines changed: 72 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 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 | +}); |
|