velocious 1.0.543 → 1.0.545
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/build/database/advisory-lock-runner.js +3 -1
- package/build/database/drivers/base.js +137 -13
- package/build/database/drivers/mssql/index.js +5 -5
- package/build/database/drivers/mysql/index.js +5 -5
- package/build/database/drivers/pgsql/index.js +5 -5
- package/build/database/drivers/sqlite/base.js +3 -3
- package/build/database/drivers/sqlite/index.js +11 -11
- package/build/database/drivers/sqlite/index.native.js +1 -1
- package/build/database/drivers/sqlite/index.web.js +1 -1
- package/build/database/pool/async-tracked-multi-connection.js +1 -0
- package/build/database/pool/single-multi-use.js +49 -1
- package/build/environment-handlers/node.js +16 -4
- package/build/src/database/advisory-lock-runner.d.ts.map +1 -1
- package/build/src/database/advisory-lock-runner.js +5 -2
- package/build/src/database/drivers/base.d.ts +56 -7
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +127 -14
- package/build/src/database/drivers/mssql/index.js +6 -6
- package/build/src/database/drivers/mysql/index.js +6 -6
- package/build/src/database/drivers/pgsql/index.js +6 -6
- package/build/src/database/drivers/sqlite/base.js +4 -4
- package/build/src/database/drivers/sqlite/index.js +12 -12
- package/build/src/database/drivers/sqlite/index.native.js +2 -2
- package/build/src/database/drivers/sqlite/index.web.js +2 -2
- package/build/src/database/pool/async-tracked-multi-connection.d.ts.map +1 -1
- package/build/src/database/pool/async-tracked-multi-connection.js +2 -1
- package/build/src/database/pool/single-multi-use.d.ts +8 -0
- package/build/src/database/pool/single-multi-use.d.ts.map +1 -1
- package/build/src/database/pool/single-multi-use.js +45 -2
- package/build/src/environment-handlers/node.d.ts.map +1 -1
- package/build/src/environment-handlers/node.js +17 -5
- package/package.json +1 -1
- package/src/database/advisory-lock-runner.js +3 -1
- package/src/database/drivers/base.js +137 -13
- package/src/database/drivers/mssql/index.js +5 -5
- package/src/database/drivers/mysql/index.js +5 -5
- package/src/database/drivers/pgsql/index.js +5 -5
- package/src/database/drivers/sqlite/base.js +3 -3
- package/src/database/drivers/sqlite/index.js +11 -11
- package/src/database/drivers/sqlite/index.native.js +1 -1
- package/src/database/drivers/sqlite/index.web.js +1 -1
- package/src/database/pool/async-tracked-multi-connection.js +1 -0
- package/src/database/pool/single-multi-use.js +49 -1
- package/src/environment-handlers/node.js +16 -4
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Database framework with familiar MVC concepts
|
|
5
5
|
* Database models with migrations and validations
|
|
6
6
|
* Database models that work almost the same in frontend and backend
|
|
7
|
+
* Connection-scoped advisory locks with automatic cleanup before pooled connections are reused or closed (see [docs/advisory-locks.md](docs/advisory-locks.md))
|
|
7
8
|
* Built-in record auditing for model lifecycle changes (see [docs/auditing.md](docs/auditing.md))
|
|
8
9
|
* Declarative state machines for models, with typed event methods generated into the base model (see [docs/state-machine.md](docs/state-machine.md))
|
|
9
10
|
* Migrations for schema changes and UTC datetime storage (see [docs/database-migrations.md](docs/database-migrations.md))
|
|
@@ -148,7 +148,9 @@ export default class AdvisoryLockRunner {
|
|
|
148
148
|
try {
|
|
149
149
|
return await callback(connection)
|
|
150
150
|
} finally {
|
|
151
|
-
if (
|
|
151
|
+
if (connection.getArgs().getConnection) {
|
|
152
|
+
await connection.releaseHeldAdvisoryLocks()
|
|
153
|
+
} else {
|
|
152
154
|
await connection.close()
|
|
153
155
|
}
|
|
154
156
|
}
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
* @property {string} [logName] - Query log subject.
|
|
58
58
|
* @property {boolean} [logQuery] - Whether to log the query.
|
|
59
59
|
* @property {boolean} [processListComment] - Whether to add process-list comments to the query.
|
|
60
|
+
* @property {boolean} [retry] - Whether retryable errors may retry the query; defaults to true.
|
|
60
61
|
* @property {boolean} [sessionTimeZone] - Whether to ensure the configured database session time zone before the query.
|
|
61
62
|
* @property {string} [sourceStack] - Stack captured at the caller boundary.
|
|
62
63
|
*/
|
|
@@ -170,6 +171,8 @@ export default class VelociousDatabaseDriversBase {
|
|
|
170
171
|
* Active query.
|
|
171
172
|
* @type {ActiveQueryState | null} */
|
|
172
173
|
_activeQuery = null
|
|
174
|
+
/** @type {Map<string, number>} */
|
|
175
|
+
_heldAdvisoryLocks = new Map()
|
|
173
176
|
|
|
174
177
|
/**
|
|
175
178
|
* Runs constructor.
|
|
@@ -239,10 +242,40 @@ export default class VelociousDatabaseDriversBase {
|
|
|
239
242
|
}
|
|
240
243
|
|
|
241
244
|
/**
|
|
242
|
-
*
|
|
243
|
-
* @returns {Promise<void>} - Resolves when complete.
|
|
245
|
+
* Releases tracked advisory locks and closes the physical database connection.
|
|
246
|
+
* @returns {Promise<void>} - Resolves when cleanup and close complete.
|
|
244
247
|
*/
|
|
245
248
|
async close() {
|
|
249
|
+
/** @type {Error | undefined} */
|
|
250
|
+
let advisoryLockError
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
await this.releaseHeldAdvisoryLocks()
|
|
254
|
+
} catch (error) {
|
|
255
|
+
advisoryLockError = error instanceof Error ? error : new Error("Failed to release held advisory locks", {cause: error})
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
await this._close()
|
|
260
|
+
this._heldAdvisoryLocks.clear()
|
|
261
|
+
} catch (error) {
|
|
262
|
+
const closeError = error instanceof Error ? error : new Error("Failed to close database connection", {cause: error})
|
|
263
|
+
|
|
264
|
+
if (advisoryLockError) {
|
|
265
|
+
throw new AggregateError([advisoryLockError, closeError], "Failed to release advisory locks and close database connection", {cause: error})
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
throw closeError
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (advisoryLockError) throw advisoryLockError
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Driver-specific physical close hook.
|
|
276
|
+
* @returns {Promise<void>} - Resolves when the underlying connection closes.
|
|
277
|
+
*/
|
|
278
|
+
async _close() {
|
|
246
279
|
// No-op by default
|
|
247
280
|
}
|
|
248
281
|
|
|
@@ -1127,7 +1160,7 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1127
1160
|
|
|
1128
1161
|
const retryInfo = this.retryableDatabaseError(error)
|
|
1129
1162
|
|
|
1130
|
-
if (tries < maxTries && retryInfo.retry) {
|
|
1163
|
+
if (options.retry !== false && tries < maxTries && retryInfo.retry) {
|
|
1131
1164
|
if (retryInfo.reconnect) {
|
|
1132
1165
|
if (this._transactionsCount > 0) {
|
|
1133
1166
|
throw new Error(`Cannot reconnect while a transaction is active (${this._transactionsCount}). Original error: ${error.message}`, {cause: error})
|
|
@@ -1773,33 +1806,124 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1773
1806
|
* table locks; they are purely cooperative between callers that use the
|
|
1774
1807
|
* same name and let you serialize functionality without blocking readers
|
|
1775
1808
|
* or writers that do not participate in the same lock.
|
|
1776
|
-
* @abstract
|
|
1777
1809
|
* @param {string} name - Lock name.
|
|
1778
|
-
* @param {{timeoutMs?: number | null}} [
|
|
1810
|
+
* @param {{timeoutMs?: number | null}} [args] - Optional timeout in milliseconds; `null` or undefined blocks forever.
|
|
1779
1811
|
* @returns {Promise<boolean>} - Resolves to true when the lock has been acquired, false if the timeout elapsed.
|
|
1780
1812
|
*/
|
|
1781
|
-
acquireAdvisoryLock(name,
|
|
1782
|
-
|
|
1813
|
+
async acquireAdvisoryLock(name, args = {}) {
|
|
1814
|
+
const acquired = await this._acquireAdvisoryLock(name, args)
|
|
1815
|
+
|
|
1816
|
+
if (acquired) this._trackAdvisoryLock(name)
|
|
1817
|
+
|
|
1818
|
+
return acquired
|
|
1783
1819
|
}
|
|
1784
1820
|
|
|
1785
1821
|
/**
|
|
1786
|
-
*
|
|
1822
|
+
* Driver-specific blocking advisory-lock acquisition hook.
|
|
1787
1823
|
* @abstract
|
|
1788
1824
|
* @param {string} name - Lock name.
|
|
1825
|
+
* @param {{timeoutMs?: number | null}} [_args] - Lock timeout options.
|
|
1826
|
+
* @returns {Promise<boolean>} - Whether the lock was acquired.
|
|
1827
|
+
*/
|
|
1828
|
+
_acquireAdvisoryLock(name, _args = {}) {
|
|
1829
|
+
throw new Error(`'_acquireAdvisoryLock' not implemented for ${this.constructor.name}`)
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
/**
|
|
1833
|
+
* Attempts to acquire a named advisory lock without blocking.
|
|
1834
|
+
* @param {string} name - Lock name.
|
|
1789
1835
|
* @returns {Promise<boolean>} - Resolves to true if the lock was acquired, false if it was already held.
|
|
1790
1836
|
*/
|
|
1791
|
-
tryAcquireAdvisoryLock(name) {
|
|
1792
|
-
|
|
1837
|
+
async tryAcquireAdvisoryLock(name) {
|
|
1838
|
+
const acquired = await this._tryAcquireAdvisoryLock(name)
|
|
1839
|
+
|
|
1840
|
+
if (acquired) this._trackAdvisoryLock(name)
|
|
1841
|
+
|
|
1842
|
+
return acquired
|
|
1793
1843
|
}
|
|
1794
1844
|
|
|
1795
1845
|
/**
|
|
1796
|
-
*
|
|
1846
|
+
* Driver-specific non-blocking advisory-lock acquisition hook.
|
|
1797
1847
|
* @abstract
|
|
1798
1848
|
* @param {string} name - Lock name.
|
|
1849
|
+
* @returns {Promise<boolean>} - Whether the lock was acquired.
|
|
1850
|
+
*/
|
|
1851
|
+
_tryAcquireAdvisoryLock(name) { // eslint-disable-line no-unused-vars
|
|
1852
|
+
throw new Error(`'_tryAcquireAdvisoryLock' not implemented for ${this.constructor.name}`)
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
/**
|
|
1856
|
+
* Releases a named advisory lock previously acquired on this connection.
|
|
1857
|
+
* @param {string} name - Lock name.
|
|
1799
1858
|
* @returns {Promise<boolean>} - Resolves to true if the lock was held by this session and has now been released.
|
|
1800
1859
|
*/
|
|
1801
|
-
releaseAdvisoryLock(name) {
|
|
1802
|
-
|
|
1860
|
+
async releaseAdvisoryLock(name) {
|
|
1861
|
+
const released = await this._releaseAdvisoryLock(name)
|
|
1862
|
+
|
|
1863
|
+
if (released) {
|
|
1864
|
+
this._untrackAdvisoryLock(name)
|
|
1865
|
+
} else {
|
|
1866
|
+
this._heldAdvisoryLocks.delete(name)
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
return released
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
/**
|
|
1873
|
+
* Driver-specific advisory-lock release hook.
|
|
1874
|
+
* @abstract
|
|
1875
|
+
* @param {string} name - Lock name.
|
|
1876
|
+
* @returns {Promise<boolean>} - Whether the lock was released.
|
|
1877
|
+
*/
|
|
1878
|
+
_releaseAdvisoryLock(name) { // eslint-disable-line no-unused-vars
|
|
1879
|
+
throw new Error(`'_releaseAdvisoryLock' not implemented for ${this.constructor.name}`)
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
/**
|
|
1883
|
+
* Releases every advisory lock still tracked on this connection.
|
|
1884
|
+
* @returns {Promise<void>} - Resolves when every tracked lock is released.
|
|
1885
|
+
*/
|
|
1886
|
+
async releaseHeldAdvisoryLocks() {
|
|
1887
|
+
/** @type {Error[]} */
|
|
1888
|
+
const errors = []
|
|
1889
|
+
|
|
1890
|
+
for (const name of [...this._heldAdvisoryLocks.keys()]) {
|
|
1891
|
+
while (this._heldAdvisoryLocks.has(name)) {
|
|
1892
|
+
try {
|
|
1893
|
+
await this.releaseAdvisoryLock(name)
|
|
1894
|
+
} catch (error) {
|
|
1895
|
+
errors.push(error instanceof Error ? error : new Error(`Failed to release advisory lock ${JSON.stringify(name)}`, {cause: error}))
|
|
1896
|
+
break
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
if (errors.length == 1) throw errors[0]
|
|
1902
|
+
if (errors.length > 1) throw new AggregateError(errors, "Failed to release held advisory locks")
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
/**
|
|
1906
|
+
* Records one successful acquisition, including re-entrant acquisitions.
|
|
1907
|
+
* @param {string} name - Lock name.
|
|
1908
|
+
* @returns {void}
|
|
1909
|
+
*/
|
|
1910
|
+
_trackAdvisoryLock(name) {
|
|
1911
|
+
this._heldAdvisoryLocks.set(name, (this._heldAdvisoryLocks.get(name) || 0) + 1)
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
/**
|
|
1915
|
+
* Removes one successful acquisition from the connection registry.
|
|
1916
|
+
* @param {string} name - Lock name.
|
|
1917
|
+
* @returns {void}
|
|
1918
|
+
*/
|
|
1919
|
+
_untrackAdvisoryLock(name) {
|
|
1920
|
+
const remainingCount = (this._heldAdvisoryLocks.get(name) || 0) - 1
|
|
1921
|
+
|
|
1922
|
+
if (remainingCount > 0) {
|
|
1923
|
+
this._heldAdvisoryLocks.set(name, remainingCount)
|
|
1924
|
+
} else {
|
|
1925
|
+
this._heldAdvisoryLocks.delete(name)
|
|
1926
|
+
}
|
|
1803
1927
|
}
|
|
1804
1928
|
|
|
1805
1929
|
/**
|
|
@@ -47,7 +47,7 @@ export default class VelociousDatabaseDriversMssql extends Base{
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
async
|
|
50
|
+
async _close() {
|
|
51
51
|
if (!this.connection) return
|
|
52
52
|
|
|
53
53
|
const connection = this.connection
|
|
@@ -616,7 +616,7 @@ export default class VelociousDatabaseDriversMssql extends Base{
|
|
|
616
616
|
* @param {{timeoutMs?: number | null}} [args] - Optional timeout in milliseconds; `null`, `undefined`, or negative blocks forever.
|
|
617
617
|
* @returns {Promise<boolean>} - True if the lock was acquired, false if the timeout elapsed.
|
|
618
618
|
*/
|
|
619
|
-
async
|
|
619
|
+
async _acquireAdvisoryLock(name, {timeoutMs} = {}) {
|
|
620
620
|
const timeoutValue = typeof timeoutMs === "number" && timeoutMs >= 0 ? Math.ceil(timeoutMs) : -1
|
|
621
621
|
const rows = await this.query(
|
|
622
622
|
`DECLARE @velocious_advisory_lock_result INT; EXEC @velocious_advisory_lock_result = sp_getapplock @Resource = ${this.quote(name)}, @LockMode = 'Exclusive', @LockOwner = 'Session', @LockTimeout = ${timeoutValue}; SELECT @velocious_advisory_lock_result AS velocious_advisory_lock_result`
|
|
@@ -634,8 +634,8 @@ export default class VelociousDatabaseDriversMssql extends Base{
|
|
|
634
634
|
* @param {string} name - Lock name.
|
|
635
635
|
* @returns {Promise<boolean>} - True if the lock was acquired, false if it was already held.
|
|
636
636
|
*/
|
|
637
|
-
async
|
|
638
|
-
return await this.
|
|
637
|
+
async _tryAcquireAdvisoryLock(name) {
|
|
638
|
+
return await this._acquireAdvisoryLock(name, {timeoutMs: 0})
|
|
639
639
|
}
|
|
640
640
|
|
|
641
641
|
/**
|
|
@@ -643,7 +643,7 @@ export default class VelociousDatabaseDriversMssql extends Base{
|
|
|
643
643
|
* @param {string} name - Lock name.
|
|
644
644
|
* @returns {Promise<boolean>} - True if the lock was held by this session and has now been released.
|
|
645
645
|
*/
|
|
646
|
-
async
|
|
646
|
+
async _releaseAdvisoryLock(name) {
|
|
647
647
|
const rows = await this.query(
|
|
648
648
|
`DECLARE @velocious_advisory_lock_result INT; EXEC @velocious_advisory_lock_result = sp_releaseapplock @Resource = ${this.quote(name)}, @LockOwner = 'Session'; SELECT @velocious_advisory_lock_result AS velocious_advisory_lock_result`
|
|
649
649
|
)
|
|
@@ -63,7 +63,7 @@ export default class VelociousDatabaseDriversMysql extends Base{
|
|
|
63
63
|
* Runs close.
|
|
64
64
|
* @returns {Promise<void>} - Resolves when complete.
|
|
65
65
|
*/
|
|
66
|
-
async
|
|
66
|
+
async _close() {
|
|
67
67
|
await this.pool?.end()
|
|
68
68
|
this.pool = undefined
|
|
69
69
|
this.resetCurrentSessionTimeZone()
|
|
@@ -574,7 +574,7 @@ export default class VelociousDatabaseDriversMysql extends Base{
|
|
|
574
574
|
* @param {{timeoutMs?: number | null}} [args] - Optional timeout in milliseconds; `null`, `undefined`, or negative blocks for `MYSQL_INDEFINITE_LOCK_TIMEOUT_SECONDS`.
|
|
575
575
|
* @returns {Promise<boolean>} - True if acquired, false if the timeout elapsed.
|
|
576
576
|
*/
|
|
577
|
-
async
|
|
577
|
+
async _acquireAdvisoryLock(name, {timeoutMs} = {}) {
|
|
578
578
|
const timeoutSeconds = typeof timeoutMs === "number" && timeoutMs >= 0
|
|
579
579
|
? Math.ceil(timeoutMs / 1000)
|
|
580
580
|
: MYSQL_INDEFINITE_LOCK_TIMEOUT_SECONDS
|
|
@@ -593,7 +593,7 @@ export default class VelociousDatabaseDriversMysql extends Base{
|
|
|
593
593
|
* @param {string} name - Lock name.
|
|
594
594
|
* @returns {Promise<boolean>} - True if the lock was acquired, false if it was already held.
|
|
595
595
|
*/
|
|
596
|
-
async
|
|
596
|
+
async _tryAcquireAdvisoryLock(name) {
|
|
597
597
|
const rows = await this.query(`SELECT GET_LOCK(${this.quote(name)}, 0) AS velocious_advisory_lock_result`)
|
|
598
598
|
const result = rows?.[0]?.velocious_advisory_lock_result
|
|
599
599
|
|
|
@@ -609,8 +609,8 @@ export default class VelociousDatabaseDriversMysql extends Base{
|
|
|
609
609
|
* @param {string} name - Lock name.
|
|
610
610
|
* @returns {Promise<boolean>} - True if the lock was held by this session and has now been released.
|
|
611
611
|
*/
|
|
612
|
-
async
|
|
613
|
-
const rows = await this.query(`SELECT RELEASE_LOCK(${this.quote(name)}) AS velocious_advisory_lock_result
|
|
612
|
+
async _releaseAdvisoryLock(name) {
|
|
613
|
+
const rows = await this.query(`SELECT RELEASE_LOCK(${this.quote(name)}) AS velocious_advisory_lock_result`, {retry: false})
|
|
614
614
|
const result = rows?.[0]?.velocious_advisory_lock_result
|
|
615
615
|
|
|
616
616
|
return Number(result) === 1
|
|
@@ -60,7 +60,7 @@ export default class VelociousDatabaseDriversPgsql extends Base{
|
|
|
60
60
|
return connectArgs
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
async
|
|
63
|
+
async _close() {
|
|
64
64
|
await this.connection?.end()
|
|
65
65
|
this.connection = undefined
|
|
66
66
|
this._transactionsCount = 0
|
|
@@ -411,7 +411,7 @@ export default class VelociousDatabaseDriversPgsql extends Base{
|
|
|
411
411
|
* @param {{timeoutMs?: number | null}} [args] - Optional timeout in milliseconds; `null`, `undefined`, or negative blocks forever.
|
|
412
412
|
* @returns {Promise<boolean>} - True if the lock was acquired, false if the timeout elapsed.
|
|
413
413
|
*/
|
|
414
|
-
async
|
|
414
|
+
async _acquireAdvisoryLock(name, {timeoutMs} = {}) {
|
|
415
415
|
const key = this.advisoryLockKey(name)
|
|
416
416
|
|
|
417
417
|
if (typeof timeoutMs !== "number" || timeoutMs < 0) {
|
|
@@ -423,7 +423,7 @@ export default class VelociousDatabaseDriversPgsql extends Base{
|
|
|
423
423
|
const pollIntervalMs = 50
|
|
424
424
|
|
|
425
425
|
while (true) {
|
|
426
|
-
if (await this.
|
|
426
|
+
if (await this._tryAcquireAdvisoryLock(name)) return true
|
|
427
427
|
if (Date.now() >= deadline) return false
|
|
428
428
|
|
|
429
429
|
const remaining = deadline - Date.now()
|
|
@@ -437,7 +437,7 @@ export default class VelociousDatabaseDriversPgsql extends Base{
|
|
|
437
437
|
* @param {string} name - Lock name.
|
|
438
438
|
* @returns {Promise<boolean>} - True if the lock was acquired, false if it was already held.
|
|
439
439
|
*/
|
|
440
|
-
async
|
|
440
|
+
async _tryAcquireAdvisoryLock(name) {
|
|
441
441
|
const key = this.advisoryLockKey(name)
|
|
442
442
|
const rows = await this.query(`SELECT pg_try_advisory_lock(${key}) AS velocious_advisory_lock_result`)
|
|
443
443
|
const result = rows?.[0]?.velocious_advisory_lock_result
|
|
@@ -450,7 +450,7 @@ export default class VelociousDatabaseDriversPgsql extends Base{
|
|
|
450
450
|
* @param {string} name - Lock name.
|
|
451
451
|
* @returns {Promise<boolean>} - True if the lock was held by this session and has now been released.
|
|
452
452
|
*/
|
|
453
|
-
async
|
|
453
|
+
async _releaseAdvisoryLock(name) {
|
|
454
454
|
const key = this.advisoryLockKey(name)
|
|
455
455
|
const rows = await this.query(`SELECT pg_advisory_unlock(${key}) AS velocious_advisory_lock_result`)
|
|
456
456
|
const result = rows?.[0]?.velocious_advisory_lock_result
|
|
@@ -385,7 +385,7 @@ export default class VelociousDatabaseDriversSqliteBase extends Base {
|
|
|
385
385
|
* @param {{timeoutMs?: number | null}} [args] - Optional timeout in milliseconds; `null`, `undefined`, or negative blocks forever.
|
|
386
386
|
* @returns {Promise<boolean>} - True if the lock was acquired, false if the timeout elapsed.
|
|
387
387
|
*/
|
|
388
|
-
async
|
|
388
|
+
async _acquireAdvisoryLock(name, {timeoutMs} = {}) {
|
|
389
389
|
const state = VelociousDatabaseDriversSqliteBase._advisoryLockState
|
|
390
390
|
|
|
391
391
|
while (state.ownersByName.has(name)) {
|
|
@@ -445,7 +445,7 @@ export default class VelociousDatabaseDriversSqliteBase extends Base {
|
|
|
445
445
|
* @param {string} name - Lock name.
|
|
446
446
|
* @returns {Promise<boolean>} - True if the lock was acquired, false if it was already held.
|
|
447
447
|
*/
|
|
448
|
-
async
|
|
448
|
+
async _tryAcquireAdvisoryLock(name) {
|
|
449
449
|
const state = VelociousDatabaseDriversSqliteBase._advisoryLockState
|
|
450
450
|
|
|
451
451
|
if (state.ownersByName.has(name)) return false
|
|
@@ -464,7 +464,7 @@ export default class VelociousDatabaseDriversSqliteBase extends Base {
|
|
|
464
464
|
* @param {string} name - Lock name.
|
|
465
465
|
* @returns {Promise<boolean>} - True if the lock was held by this driver and has now been released.
|
|
466
466
|
*/
|
|
467
|
-
async
|
|
467
|
+
async _releaseAdvisoryLock(name) {
|
|
468
468
|
const state = VelociousDatabaseDriversSqliteBase._advisoryLockState
|
|
469
469
|
const owner = state.ownersByName.get(name)
|
|
470
470
|
|
|
@@ -62,7 +62,7 @@ export default class VelociousDatabaseDriversSqliteNode extends Base {
|
|
|
62
62
|
return `VelociousDatabaseDriversSqlite---${args.name}`
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
async
|
|
65
|
+
async _close() {
|
|
66
66
|
await this.connection?.close()
|
|
67
67
|
this.connection = undefined
|
|
68
68
|
}
|
|
@@ -105,10 +105,10 @@ export default class VelociousDatabaseDriversSqliteNode extends Base {
|
|
|
105
105
|
* @param {{timeoutMs?: number | null}} [args] - Optional timeout in milliseconds; `null`, `undefined`, or negative blocks forever.
|
|
106
106
|
* @returns {Promise<boolean>} - Whether the advisory lock was acquired.
|
|
107
107
|
*/
|
|
108
|
-
async
|
|
108
|
+
async _acquireAdvisoryLock(name, {timeoutMs} = {}) {
|
|
109
109
|
const deadline = typeof timeoutMs === "number" && timeoutMs >= 0 ? Date.now() + timeoutMs : null
|
|
110
110
|
const remainingForInProcess = deadline !== null ? Math.max(0, deadline - Date.now()) : null
|
|
111
|
-
const inProcessAcquired = await super.
|
|
111
|
+
const inProcessAcquired = await super._acquireAdvisoryLock(name, {timeoutMs: remainingForInProcess})
|
|
112
112
|
|
|
113
113
|
if (!inProcessAcquired) return false
|
|
114
114
|
|
|
@@ -117,11 +117,11 @@ export default class VelociousDatabaseDriversSqliteNode extends Base {
|
|
|
117
117
|
const fileAcquired = await this._acquireAdvisoryLockFile(name, {timeoutMs: remainingForFile})
|
|
118
118
|
|
|
119
119
|
if (!fileAcquired) {
|
|
120
|
-
await super.
|
|
120
|
+
await super._releaseAdvisoryLock(name)
|
|
121
121
|
return false
|
|
122
122
|
}
|
|
123
123
|
} catch (error) {
|
|
124
|
-
await super.
|
|
124
|
+
await super._releaseAdvisoryLock(name)
|
|
125
125
|
throw error
|
|
126
126
|
}
|
|
127
127
|
|
|
@@ -133,8 +133,8 @@ export default class VelociousDatabaseDriversSqliteNode extends Base {
|
|
|
133
133
|
* @param {string} name - Lock name.
|
|
134
134
|
* @returns {Promise<boolean>} - Whether the advisory lock was acquired immediately.
|
|
135
135
|
*/
|
|
136
|
-
async
|
|
137
|
-
const inProcessAcquired = await super.
|
|
136
|
+
async _tryAcquireAdvisoryLock(name) {
|
|
137
|
+
const inProcessAcquired = await super._tryAcquireAdvisoryLock(name)
|
|
138
138
|
|
|
139
139
|
if (!inProcessAcquired) return false
|
|
140
140
|
|
|
@@ -142,11 +142,11 @@ export default class VelociousDatabaseDriversSqliteNode extends Base {
|
|
|
142
142
|
const fileAcquired = await this._tryAcquireAdvisoryLockFile(name)
|
|
143
143
|
|
|
144
144
|
if (!fileAcquired) {
|
|
145
|
-
await super.
|
|
145
|
+
await super._releaseAdvisoryLock(name)
|
|
146
146
|
return false
|
|
147
147
|
}
|
|
148
148
|
} catch (error) {
|
|
149
|
-
await super.
|
|
149
|
+
await super._releaseAdvisoryLock(name)
|
|
150
150
|
throw error
|
|
151
151
|
}
|
|
152
152
|
|
|
@@ -163,8 +163,8 @@ export default class VelociousDatabaseDriversSqliteNode extends Base {
|
|
|
163
163
|
* @param {string} name - Lock name.
|
|
164
164
|
* @returns {Promise<boolean>} - Whether the advisory lock was released.
|
|
165
165
|
*/
|
|
166
|
-
async
|
|
167
|
-
const inProcessReleased = await super.
|
|
166
|
+
async _releaseAdvisoryLock(name) {
|
|
167
|
+
const inProcessReleased = await super._releaseAdvisoryLock(name)
|
|
168
168
|
|
|
169
169
|
if (inProcessReleased) {
|
|
170
170
|
await this._releaseAdvisoryLockFile(name)
|
|
@@ -125,6 +125,7 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
125
125
|
|
|
126
126
|
try {
|
|
127
127
|
await this.rollbackLeftOpenTransaction(connection)
|
|
128
|
+
await connection.releaseHeldAdvisoryLocks()
|
|
128
129
|
await connection.clearConnectionCheckoutName()
|
|
129
130
|
} catch (error) {
|
|
130
131
|
await this.closeCheckedOutConnectionAfterCheckinFailure(connection, id, error)
|
|
@@ -3,15 +3,58 @@
|
|
|
3
3
|
import BasePool from "./base.js"
|
|
4
4
|
|
|
5
5
|
export default class VelociousDatabasePoolSingleMultiUser extends BasePool {
|
|
6
|
+
activeCheckoutCount = 0
|
|
6
7
|
suppressedConnectionContextCount = 0
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Checkout names of the currently-active nested checkouts, innermost last. The
|
|
11
|
+
* shared connection carries a single checkout name, so nesting requires restoring
|
|
12
|
+
* the enclosing scope's name when an inner checkout checks in.
|
|
13
|
+
* @type {Array<string | undefined>}
|
|
14
|
+
*/
|
|
15
|
+
checkoutNameStack = []
|
|
16
|
+
|
|
8
17
|
/**
|
|
9
18
|
* Runs checkin.
|
|
10
19
|
* @param {import("../drivers/base.js").default} connection - Connection.
|
|
11
20
|
* @returns {Promise<void>} - Resolves when complete.
|
|
12
21
|
*/
|
|
13
22
|
async checkin(connection) {
|
|
14
|
-
|
|
23
|
+
if (this.connection === connection && this.activeCheckoutCount > 0) {
|
|
24
|
+
this.activeCheckoutCount--
|
|
25
|
+
this.checkoutNameStack.pop()
|
|
26
|
+
|
|
27
|
+
// A nested checkout is checking in while an outer one is still active: restore
|
|
28
|
+
// the enclosing scope's checkout name instead of leaving this inner name (or
|
|
29
|
+
// clearing it) on the shared connection.
|
|
30
|
+
if (this.activeCheckoutCount > 0) {
|
|
31
|
+
await connection.setConnectionCheckoutName(this.checkoutNameStack[this.checkoutNameStack.length - 1])
|
|
32
|
+
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
await connection.releaseHeldAdvisoryLocks()
|
|
39
|
+
await connection.clearConnectionCheckoutName()
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (this.connection === connection) {
|
|
42
|
+
this.activeCheckoutCount = 0
|
|
43
|
+
this.checkoutNameStack = []
|
|
44
|
+
this.connection = undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
await connection.close()
|
|
49
|
+
} catch (closeError) {
|
|
50
|
+
const cleanupError = error instanceof Error ? error : new Error("Failed to clean checked-in database connection", {cause: error})
|
|
51
|
+
const connectionCloseError = closeError instanceof Error ? closeError : new Error("Failed to close database connection after check-in cleanup failed", {cause: closeError})
|
|
52
|
+
|
|
53
|
+
throw new AggregateError([cleanupError, connectionCloseError], "Failed to clean and close checked-in database connection", {cause: closeError})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
throw error
|
|
57
|
+
}
|
|
15
58
|
}
|
|
16
59
|
|
|
17
60
|
/**
|
|
@@ -23,6 +66,8 @@ export default class VelociousDatabasePoolSingleMultiUser extends BasePool {
|
|
|
23
66
|
if (this.connection && !this.connectionMatchesCurrentConfiguration(this.connection)) {
|
|
24
67
|
const previousConnection = this.connection
|
|
25
68
|
|
|
69
|
+
this.activeCheckoutCount = 0
|
|
70
|
+
this.checkoutNameStack = []
|
|
26
71
|
this.connection = undefined
|
|
27
72
|
|
|
28
73
|
await previousConnection.close()
|
|
@@ -32,7 +77,9 @@ export default class VelociousDatabasePoolSingleMultiUser extends BasePool {
|
|
|
32
77
|
this.connection = await this.spawnConnection()
|
|
33
78
|
}
|
|
34
79
|
|
|
80
|
+
this.checkoutNameStack.push(options.name)
|
|
35
81
|
await this.connection.setConnectionCheckoutName(options.name)
|
|
82
|
+
this.activeCheckoutCount++
|
|
36
83
|
|
|
37
84
|
return this.connection
|
|
38
85
|
}
|
|
@@ -103,6 +150,7 @@ export default class VelociousDatabasePoolSingleMultiUser extends BasePool {
|
|
|
103
150
|
|
|
104
151
|
const connection = this.connection
|
|
105
152
|
|
|
153
|
+
this.activeCheckoutCount = 0
|
|
106
154
|
this.connection = undefined
|
|
107
155
|
|
|
108
156
|
await connection.close()
|
|
@@ -1020,12 +1020,24 @@ export default class VelociousEnvironmentHandlerNode extends Base{
|
|
|
1020
1020
|
const {default: BackgroundJobsStore} = await import("../background-jobs/store.js")
|
|
1021
1021
|
const store = new BackgroundJobsStore({configuration: this.getConfiguration()})
|
|
1022
1022
|
const databaseIdentifier = store.getDatabaseIdentifier() ?? "default"
|
|
1023
|
+
const frameworkDb = dbs[databaseIdentifier]
|
|
1024
|
+
|
|
1025
|
+
// Only ensure the framework schema when the background-jobs database is actually
|
|
1026
|
+
// part of this migrate operation. When it isn't among the migrated set — e.g.
|
|
1027
|
+
// `db:tenants:migrate <tenant>`, which migrates only tenant databases — the
|
|
1028
|
+
// framework store lives elsewhere (typically the default DB) and was already
|
|
1029
|
+
// ensured by the plain `db:migrate` that precedes it. Reaching into it here would
|
|
1030
|
+
// open a fresh connection to that shared database and re-run the concurrency
|
|
1031
|
+
// reconcile UPDATEs; under `db:tenants:migrate --parallel N` that happens once per
|
|
1032
|
+
// tenant worker, and the concurrent auto-committed UPDATEs on the shared
|
|
1033
|
+
// background_jobs / background_job_concurrency rows InnoDB-deadlock
|
|
1034
|
+
// (ER_LOCK_DEADLOCK). So skip when the framework DB isn't in this set; the runtime
|
|
1035
|
+
// store still creates it lazily if a plain migrate never ran.
|
|
1036
|
+
if (!frameworkDb) return
|
|
1023
1037
|
|
|
1024
1038
|
// Reuse the connection db:migrate already holds for this database; opening a
|
|
1025
|
-
// nested checkout would deadlock a database whose pool is capped at one
|
|
1026
|
-
|
|
1027
|
-
// this passes undefined and the store checks out its own (that pool is free).
|
|
1028
|
-
await store.ensureSchema(dbs[databaseIdentifier])
|
|
1039
|
+
// nested checkout would deadlock a database whose pool is capped at one connection.
|
|
1040
|
+
await store.ensureSchema(frameworkDb)
|
|
1029
1041
|
}
|
|
1030
1042
|
|
|
1031
1043
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"advisory-lock-runner.d.ts","sourceRoot":"","sources":["../../../src/database/advisory-lock-runner.js"],"names":[],"mappings":"AAoDA;;;GAGG;AACH;
|
|
1
|
+
{"version":3,"file":"advisory-lock-runner.d.ts","sourceRoot":"","sources":["../../../src/database/advisory-lock-runner.js"],"names":[],"mappings":"AAoDA;;;GAGG;AACH;IAsGE;;;;;;;;;OASG;IACH,sCANa,CAAC,QACH,MAAM,YACN,MAAM,OAAO,CAAC,CAAC,CAAC,kBAChB,MAAM,GAAG,IAAI,GACX,OAAO,CAAC,CAAC,CAAC,CAwBtB;IArID;;;OAGG;IACH,uEAFW;QAAC,aAAa,EAAE,OAAO,qBAAqB,EAAE,OAAO,CAAC;QAAC,kBAAkB,EAAE,MAAM,OAAO,mBAAmB,EAAE,OAAO,CAAC;QAAC,kBAAkB,EAAE,MAAM,CAAA;KAAC,EAM3J;IAHC,qDAAkC;IAClC,0BAJ0F,OAAO,mBAAmB,EAAE,OAAO,CAIjF;IAC5C,2BAA4C;IAG9C;;;;;;;OAOG;IACH,iBANa,CAAC,QACH,MAAM,YACN,MAAM,OAAO,CAAC,CAAC,CAAC,SAChB;QAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAC,GACxD,OAAO,CAAC,CAAC,CAAC,CAYtB;IAED;;;;;;;OAOG;IACH,uBANa,CAAC,QACH,MAAM,YACN,MAAM,OAAO,CAAC,CAAC,CAAC,SAChB;QAAC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAC,GAC7B,OAAO,CAAC,CAAC,CAAC,CAYtB;IAED;;;;;OAKG;IACH,kBAJa,CAAC,iDACH;QAAC,QAAQ,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,CAAC;QAAC,UAAU,EAAE,OAAO,mBAAmB,EAAE,OAAO,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAC,GACxH,OAAO,CAAC,CAAC,CAAC,CAQtB;IAED;;;;;;;OAOG;IACH,mBALa,CAAC,iBACH,MAAM,GAAG,IAAI,GAAG,SAAS,YACzB,CAAC,UAAU,EAAE,OAAO,mBAAmB,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,GAC7D,OAAO,CAAC,CAAC,CAAC,CAQtB;IAED;;;;;;OAMG;IACH,wBAJa,CAAC,YACH,CAAC,UAAU,EAAE,OAAO,mBAAmB,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,GAC7D,OAAO,CAAC,CAAC,CAAC,CActB;CAmCF;AA3KD;;GAEG;AACH;IACE;;;;OAIG;IACH,qBAHW,MAAM,YACN;QAAC,IAAI,EAAE,MAAM,CAAA;KAAC,EAMxB;IADC,iBAAoB;CAEvB;AAED;;GAEG;AACH;IACE;;;;OAIG;IACH,qBAHW,MAAM,YACN;QAAC,IAAI,EAAE,MAAM,CAAA;KAAC,EAMxB;IADC,iBAAoB;CAEvB;AA9CD;;GAEG;AACH;IACE;;;;OAIG;IACH,qBAHW,MAAM,YACN;QAAC,IAAI,EAAE,MAAM,CAAA;KAAC,EAMxB;IADC,iBAAoB;CAEvB"}
|