velocious 1.0.719 → 1.0.720
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/build/configuration.js +20 -2
- package/build/database/advisory-lock-file-backend.js +21 -3
- package/build/database/advisory-lock-lock-server-backend.js +12 -2
- package/build/database/advisory-lock-runner.js +91 -1
- package/build/src/configuration.d.ts +18 -1
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +20 -3
- package/build/src/database/advisory-lock-file-backend.d.ts +2 -1
- package/build/src/database/advisory-lock-file-backend.d.ts.map +1 -1
- package/build/src/database/advisory-lock-file-backend.js +18 -5
- package/build/src/database/advisory-lock-lock-server-backend.d.ts +5 -1
- package/build/src/database/advisory-lock-lock-server-backend.d.ts.map +1 -1
- package/build/src/database/advisory-lock-lock-server-backend.js +11 -3
- package/build/src/database/advisory-lock-runner.d.ts +42 -0
- package/build/src/database/advisory-lock-runner.d.ts.map +1 -1
- package/build/src/database/advisory-lock-runner.js +82 -2
- package/package.json +1 -1
- package/src/configuration.js +20 -2
- package/src/database/advisory-lock-file-backend.js +21 -3
- package/src/database/advisory-lock-lock-server-backend.js +12 -2
- package/src/database/advisory-lock-runner.js +91 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
+
import {AsyncLocalStorage} from "node:async_hooks"
|
|
3
4
|
import timeout, {TimeoutError} from "awaitery/build/timeout.js"
|
|
4
5
|
import {DEFAULT_ADVISORY_LOCK_ACQUIRE_TIMEOUT_MS} from "./advisory-lock-bound.js"
|
|
5
6
|
import {
|
|
@@ -59,6 +60,27 @@ class AdvisoryLockBusyError extends Error {
|
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Advisory locks held by one async context, keyed by
|
|
65
|
+
* `<databaseIdentifier>\u0000<lock name>` and carrying the re-entry depth plus
|
|
66
|
+
* the provenance of the tier that actually holds the lock.
|
|
67
|
+
*
|
|
68
|
+
* Re-entry is scoped to the async context, not to the process: a nested call
|
|
69
|
+
* made inside a held callback belongs to the same logical operation and must
|
|
70
|
+
* not contend with itself, while an unrelated concurrent request in the same
|
|
71
|
+
* process is a different context and must still wait. That is Ruby's `Monitor`
|
|
72
|
+
* ("re-entrant for the holding thread") expressed with the async-context
|
|
73
|
+
* analogue of a thread.
|
|
74
|
+
*
|
|
75
|
+
* Without this, the four tiers disagreed on re-entry: a nested acquisition on
|
|
76
|
+
* a `dedicatedConnection` opened a second connection and self-deadlocked, the
|
|
77
|
+
* file tier refused it as held, and the lock-server tier received
|
|
78
|
+
* `already-held` and treated it as contention. Applications worked around it
|
|
79
|
+
* with hand-passed `*LockHeld` booleans, which is exactly the parameter a
|
|
80
|
+
* monitor makes redundant.
|
|
81
|
+
*/
|
|
82
|
+
const heldAdvisoryLocks = new AsyncLocalStorage()
|
|
83
|
+
|
|
62
84
|
/**
|
|
63
85
|
* Thrown when a callback holds an advisory lock longer than `holdTimeoutMs`.
|
|
64
86
|
*/
|
|
@@ -107,6 +129,10 @@ export default class AdvisoryLockRunner {
|
|
|
107
129
|
* @returns {Promise<T>} - Resolves with the callback result.
|
|
108
130
|
*/
|
|
109
131
|
async withAdvisoryLock(name, callback, args = {}) {
|
|
132
|
+
const heldEntry = this.heldEntryFor(name)
|
|
133
|
+
|
|
134
|
+
if (heldEntry) return await this.runReentrantCallback({callback, entry: heldEntry, name})
|
|
135
|
+
|
|
110
136
|
const lockServerResult = await this.attemptLockServer({args, callback, mode: "wait", name})
|
|
111
137
|
|
|
112
138
|
// The wait mode never returns a deferred outcome, so the value member is present.
|
|
@@ -159,6 +185,10 @@ export default class AdvisoryLockRunner {
|
|
|
159
185
|
* @returns {Promise<T>} - Resolves with the callback result.
|
|
160
186
|
*/
|
|
161
187
|
async withAdvisoryLockOrFail(name, callback, args = {}) {
|
|
188
|
+
const heldEntry = this.heldEntryFor(name)
|
|
189
|
+
|
|
190
|
+
if (heldEntry) return await this.runReentrantCallback({callback, entry: heldEntry, name})
|
|
191
|
+
|
|
162
192
|
const lockServerResult = await this.attemptLockServer({args, callback, mode: "or-fail", name})
|
|
163
193
|
|
|
164
194
|
// The or-fail mode throws on contention, so the value member is present.
|
|
@@ -218,6 +248,10 @@ export default class AdvisoryLockRunner {
|
|
|
218
248
|
* @returns {Promise<{acquired: true, result: T} | {acquired: false, reason: "contended", lockName: string}>} - Either the held-callback result or the defer outcome.
|
|
219
249
|
*/
|
|
220
250
|
async withAdvisoryLockOrDefer(name, callback, args = {}) {
|
|
251
|
+
const heldEntry = this.heldEntryFor(name)
|
|
252
|
+
|
|
253
|
+
if (heldEntry) return {acquired: true, result: await this.runReentrantCallback({callback, entry: heldEntry, name})}
|
|
254
|
+
|
|
221
255
|
const lockServerResult = await this.attemptLockServer({args, callback, mode: "defer", name})
|
|
222
256
|
|
|
223
257
|
if (lockServerResult) {
|
|
@@ -266,13 +300,69 @@ export default class AdvisoryLockRunner {
|
|
|
266
300
|
* @returns {Promise<T>} - Resolves with the callback result.
|
|
267
301
|
*/
|
|
268
302
|
async runLockedCallback({callback, holdTimeoutMs, lockBackend, name, release}) {
|
|
303
|
+
const store = new Map(heldAdvisoryLocks.getStore() ?? [])
|
|
304
|
+
|
|
305
|
+
store.set(this.lockStorageKey(name), {depth: 1, lockBackend})
|
|
306
|
+
|
|
269
307
|
try {
|
|
270
|
-
|
|
308
|
+
// The lock is released outside the context scope: a release path that
|
|
309
|
+
// touches the same name (a cleanup hook, an error reporter) must not look
|
|
310
|
+
// like a re-entry of a hold that is already over.
|
|
311
|
+
return await heldAdvisoryLocks.run(store, async () => await AdvisoryLockRunner.runWithAdvisoryLockHoldTimeout(name, callback, holdTimeoutMs, lockBackend))
|
|
271
312
|
} finally {
|
|
272
313
|
await release()
|
|
273
314
|
}
|
|
274
315
|
}
|
|
275
316
|
|
|
317
|
+
/**
|
|
318
|
+
* Returns the held entry when this async context already holds the lock.
|
|
319
|
+
* @param {string} name - Lock name.
|
|
320
|
+
* @returns {{depth: number, lockBackend: AdvisoryLockBackendProvenance} | null} - The held entry, or null when this context does not hold the lock.
|
|
321
|
+
*/
|
|
322
|
+
heldEntryFor(name) {
|
|
323
|
+
const store = heldAdvisoryLocks.getStore()
|
|
324
|
+
|
|
325
|
+
if (!store) return null
|
|
326
|
+
|
|
327
|
+
return store.get(this.lockStorageKey(name)) ?? null
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Runs a callback as a re-entry of a lock this async context already holds.
|
|
332
|
+
*
|
|
333
|
+
* The re-entry adds no backend round trip and cannot fail on contention: the
|
|
334
|
+
* outer hold already owns the lock, so mutual exclusion is unchanged. The
|
|
335
|
+
* outer hold owns the hold timeout and the lease; a re-entry does not start a
|
|
336
|
+
* second one, and the callback therefore receives the outer provenance without
|
|
337
|
+
* a `control`.
|
|
338
|
+
* @template T
|
|
339
|
+
* @param {{callback: (args?: {control?: import("awaitery/build/timeout.js").TimeoutControl, lockBackend: AdvisoryLockBackendProvenance}) => Promise<T>, entry: {depth: number, lockBackend: AdvisoryLockBackendProvenance}, name: string}} args - Re-entry request.
|
|
340
|
+
* @returns {Promise<T>} - Callback result.
|
|
341
|
+
*/
|
|
342
|
+
async runReentrantCallback({callback, entry, name}) {
|
|
343
|
+
entry.depth += 1
|
|
344
|
+
this.configuration.recordAdvisoryLockReentry({lockName: name})
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
return await callback({lockBackend: entry.lockBackend})
|
|
348
|
+
} finally {
|
|
349
|
+
entry.depth -= 1
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Builds the async-context storage key for one lock name.
|
|
355
|
+
*
|
|
356
|
+
* The database identifier is part of the key because it is part of the lock's
|
|
357
|
+
* identity: two tenant databases reusing one name hold two different locks,
|
|
358
|
+
* and a nested call in the other database is not a re-entry.
|
|
359
|
+
* @param {string} name - Lock name.
|
|
360
|
+
* @returns {string} - Storage key for the held-entry map.
|
|
361
|
+
*/
|
|
362
|
+
lockStorageKey(name) {
|
|
363
|
+
return `${this.databaseIdentifier}\u0000${name}`
|
|
364
|
+
}
|
|
365
|
+
|
|
276
366
|
/**
|
|
277
367
|
* Serves one acquisition on the lock-server tier when the lock name prefers
|
|
278
368
|
* it (design §1/§9), falling *upward* to the driver path when the server is
|