velocious 1.0.709 → 1.0.710
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 -1
- package/build/configuration-types.js +17 -1
- package/build/configuration.js +74 -2
- package/build/database/advisory-lock-errors.js +59 -0
- package/build/database/advisory-lock-lock-server-backend.js +557 -0
- package/build/database/advisory-lock-runner.js +175 -2
- package/build/database/record/index.js +16 -2
- package/build/src/configuration-types.d.ts +73 -2
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +17 -2
- package/build/src/configuration.d.ts +32 -2
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +72 -3
- package/build/src/database/advisory-lock-errors.d.ts +54 -0
- package/build/src/database/advisory-lock-errors.d.ts.map +1 -0
- package/build/src/database/advisory-lock-errors.js +52 -0
- package/build/src/database/advisory-lock-lock-server-backend.d.ts +279 -0
- package/build/src/database/advisory-lock-lock-server-backend.d.ts.map +1 -0
- package/build/src/database/advisory-lock-lock-server-backend.js +474 -0
- package/build/src/database/advisory-lock-runner.d.ts +69 -1
- package/build/src/database/advisory-lock-runner.d.ts.map +1 -1
- package/build/src/database/advisory-lock-runner.js +150 -3
- package/build/src/database/record/index.d.ts +2 -2
- package/build/src/database/record/index.d.ts.map +1 -1
- package/build/src/database/record/index.js +3 -3
- package/package.json +1 -1
- package/src/configuration-types.js +17 -1
- package/src/configuration.js +74 -2
- package/src/database/advisory-lock-errors.js +59 -0
- package/src/database/advisory-lock-lock-server-backend.js +557 -0
- package/src/database/advisory-lock-runner.js +175 -2
- package/src/database/record/index.js +16 -2
|
@@ -7,6 +7,11 @@ import {
|
|
|
7
7
|
resolveAdvisoryLockBackendPolicy,
|
|
8
8
|
refuseAdvisoryLockTierBelowMinimum
|
|
9
9
|
} from "./advisory-lock-backend-policy.js"
|
|
10
|
+
import {
|
|
11
|
+
AdvisoryLockLostError,
|
|
12
|
+
AdvisoryLockServiceUnavailableError,
|
|
13
|
+
advisoryLockBackendUnavailable
|
|
14
|
+
} from "./advisory-lock-errors.js"
|
|
10
15
|
|
|
11
16
|
/**
|
|
12
17
|
* Provenance of one advisory-lock acquisition (STC-210 design §3): every
|
|
@@ -15,6 +20,7 @@ import {
|
|
|
15
20
|
* @typedef {object} AdvisoryLockBackendProvenance
|
|
16
21
|
* @property {import("./advisory-lock-backend-policy.js").AdvisoryLockTier} backend - Tier that served the acquisition.
|
|
17
22
|
* @property {string} [driverType] - Database driver type when the driver tier served it.
|
|
23
|
+
* @property {number} [epoch] - Fencing epoch when the serving tier issues one.
|
|
18
24
|
* @property {boolean} fencingSupported - Whether this tier carries fencing tokens (design §8); the file tier reports false rather than pretending.
|
|
19
25
|
* @property {string} [holderId] - Backend holder id for the file tier.
|
|
20
26
|
* @property {string} [lockName] - Lock name this provenance belongs to.
|
|
@@ -100,10 +106,15 @@ export default class AdvisoryLockRunner {
|
|
|
100
106
|
* @returns {Promise<T>} - Resolves with the callback result.
|
|
101
107
|
*/
|
|
102
108
|
async withAdvisoryLock(name, callback, args = {}) {
|
|
109
|
+
const lockServerResult = await this.attemptLockServer({args, callback, mode: "wait", name})
|
|
110
|
+
|
|
111
|
+
// The wait mode never returns a deferred outcome, so the value member is present.
|
|
112
|
+
if (lockServerResult) return /** @type {{value: T}} */ (lockServerResult).value
|
|
113
|
+
|
|
103
114
|
const fileBackend = await this.fileBackendForName(name)
|
|
104
115
|
|
|
105
116
|
if (fileBackend) {
|
|
106
|
-
const acquired = await fileBackend.acquire(name, {timeoutMs: this.
|
|
117
|
+
const acquired = await fileBackend.acquire(name, {timeoutMs: this.acquireTimeoutMs(args)})
|
|
107
118
|
|
|
108
119
|
if (!acquired) {
|
|
109
120
|
throw new AdvisoryLockTimeoutError(`Timed out waiting for advisory lock ${JSON.stringify(name)} on the file tier`, {name})
|
|
@@ -147,6 +158,11 @@ export default class AdvisoryLockRunner {
|
|
|
147
158
|
* @returns {Promise<T>} - Resolves with the callback result.
|
|
148
159
|
*/
|
|
149
160
|
async withAdvisoryLockOrFail(name, callback, args = {}) {
|
|
161
|
+
const lockServerResult = await this.attemptLockServer({args, callback, mode: "or-fail", name})
|
|
162
|
+
|
|
163
|
+
// The or-fail mode throws on contention, so the value member is present.
|
|
164
|
+
if (lockServerResult) return /** @type {{value: T}} */ (lockServerResult).value
|
|
165
|
+
|
|
150
166
|
const fileBackend = await this.fileBackendForName(name)
|
|
151
167
|
|
|
152
168
|
if (fileBackend) {
|
|
@@ -201,6 +217,14 @@ export default class AdvisoryLockRunner {
|
|
|
201
217
|
* @returns {Promise<{acquired: true, result: T} | {acquired: false, reason: "contended", lockName: string}>} - Either the held-callback result or the defer outcome.
|
|
202
218
|
*/
|
|
203
219
|
async withAdvisoryLockOrDefer(name, callback, args = {}) {
|
|
220
|
+
const lockServerResult = await this.attemptLockServer({args, callback, mode: "defer", name})
|
|
221
|
+
|
|
222
|
+
if (lockServerResult) {
|
|
223
|
+
if ("deferred" in lockServerResult) return lockServerResult.deferred
|
|
224
|
+
|
|
225
|
+
return {acquired: true, result: /** @type {{value: T}} */ (lockServerResult).value}
|
|
226
|
+
}
|
|
227
|
+
|
|
204
228
|
const fileBackend = await this.fileBackendForName(name)
|
|
205
229
|
|
|
206
230
|
if (fileBackend) {
|
|
@@ -248,6 +272,154 @@ export default class AdvisoryLockRunner {
|
|
|
248
272
|
}
|
|
249
273
|
}
|
|
250
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Serves one acquisition on the lock-server tier when the lock name prefers
|
|
277
|
+
* it (design §1/§9), falling *upward* to the driver path when the server is
|
|
278
|
+
* unreachable and the name's minimum tier permits a DB-native hold.
|
|
279
|
+
*
|
|
280
|
+
* `null` means "this tier did not serve the acquisition — continue with the
|
|
281
|
+
* next eligible tier". A returned value means the lock-server tier answered,
|
|
282
|
+
* including a definitive busy/timeout outcome.
|
|
283
|
+
* @template T
|
|
284
|
+
* @param {{args: {holdTimeoutMs?: number | null, timeoutMs?: number | null}, callback: (args?: {control?: import("awaitery/build/timeout.js").TimeoutControl, lockBackend: AdvisoryLockBackendProvenance}) => Promise<T>, mode: "wait" | "or-fail" | "defer", name: string}} request - Acquisition request.
|
|
285
|
+
* @returns {Promise<{deferred: {acquired: false, reason: "contended", lockName: string}} | {value: T} | null>} - Result when this tier served it, otherwise null.
|
|
286
|
+
*/
|
|
287
|
+
async attemptLockServer({args, callback, mode, name}) {
|
|
288
|
+
const policy = resolveAdvisoryLockBackendPolicy(name, this.configuration.getAdvisoryLockPolicy())
|
|
289
|
+
|
|
290
|
+
if (policy.preferredTier !== "lock-server") return null
|
|
291
|
+
if (!policy.allowedTiers.includes("lock-server")) return null
|
|
292
|
+
|
|
293
|
+
const backend = await this.configuration.getAdvisoryLockLockServerBackend()
|
|
294
|
+
|
|
295
|
+
if (!backend) {
|
|
296
|
+
this.reportAdvisoryLockFallback({reason: "lock-server tier is not enabled in configuration", servedBy: "driver-native", lockName: name})
|
|
297
|
+
|
|
298
|
+
return null
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** @type {boolean} */
|
|
302
|
+
let acquired
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
if (mode === "wait") {
|
|
306
|
+
acquired = await backend.acquire(name, {timeoutMs: this.acquireTimeoutMs(args)})
|
|
307
|
+
} else {
|
|
308
|
+
acquired = await backend.tryAcquire(name)
|
|
309
|
+
}
|
|
310
|
+
} catch (error) {
|
|
311
|
+
if (!(error instanceof AdvisoryLockServiceUnavailableError)) throw error
|
|
312
|
+
|
|
313
|
+
if (policy.minTier === "lock-server") {
|
|
314
|
+
throw advisoryLockBackendUnavailable({
|
|
315
|
+
lockName: name,
|
|
316
|
+
minTier: policy.minTier,
|
|
317
|
+
reasons: [`lock-server unavailable: ${error.message}`]
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
this.reportAdvisoryLockFallback({reason: error.message, servedBy: "driver-native", lockName: name})
|
|
322
|
+
|
|
323
|
+
return null
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (!acquired) {
|
|
327
|
+
if (mode === "defer") return {deferred: {acquired: false, reason: "contended", lockName: name}}
|
|
328
|
+
if (mode === "or-fail") throw new AdvisoryLockBusyError(`Advisory lock ${JSON.stringify(name)} is already held`, {name})
|
|
329
|
+
|
|
330
|
+
throw new AdvisoryLockTimeoutError(`Timed out waiting for advisory lock ${JSON.stringify(name)}`, {name})
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const epoch = backend.getHoldEpoch(name)
|
|
334
|
+
const lockBackend = /** @type {AdvisoryLockBackendProvenance} */ ({
|
|
335
|
+
backend: "lock-server",
|
|
336
|
+
epoch: epoch ?? undefined,
|
|
337
|
+
fencingSupported: true,
|
|
338
|
+
lockName: name
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
this.configuration.recordAdvisoryLockAcquisition({backend: "lock-server", fencingSupported: true, lockName: name})
|
|
342
|
+
|
|
343
|
+
const value = await this.runLockServerHold({
|
|
344
|
+
backend,
|
|
345
|
+
callback,
|
|
346
|
+
holdTimeoutMs: args.holdTimeoutMs,
|
|
347
|
+
lockBackend,
|
|
348
|
+
name
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
return {value}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Runs the critical section under a lock-server lease and surfaces a fenced
|
|
356
|
+
* lease as `AdvisoryLockLostError` (design §8): the section may have
|
|
357
|
+
* overlapped another holder, so its outcome must not be trusted as exclusive.
|
|
358
|
+
* @template T
|
|
359
|
+
* @param {{backend: import("./advisory-lock-lock-server-backend.js").default, callback: (args?: {control?: import("awaitery/build/timeout.js").TimeoutControl, lockBackend: AdvisoryLockBackendProvenance}) => Promise<T>, holdTimeoutMs?: number | null, lockBackend: AdvisoryLockBackendProvenance, name: string}} args - Hold request.
|
|
360
|
+
* @returns {Promise<T>} - Callback result.
|
|
361
|
+
*/
|
|
362
|
+
async runLockServerHold({backend, callback, holdTimeoutMs, lockBackend, name}) {
|
|
363
|
+
/** @type {T | undefined} */
|
|
364
|
+
let value
|
|
365
|
+
/** @type {unknown} */
|
|
366
|
+
let callbackError
|
|
367
|
+
let failed = false
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
value = await this.runLockedCallback({
|
|
371
|
+
callback,
|
|
372
|
+
holdTimeoutMs,
|
|
373
|
+
lockBackend,
|
|
374
|
+
name,
|
|
375
|
+
release: async () => await backend.release(name)
|
|
376
|
+
})
|
|
377
|
+
} catch (error) {
|
|
378
|
+
failed = true
|
|
379
|
+
callbackError = error
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const loss = backend.takeLoss(name, lockBackend.epoch ?? null)
|
|
383
|
+
|
|
384
|
+
if (loss) {
|
|
385
|
+
throw new AdvisoryLockLostError(
|
|
386
|
+
`Lost the lease for advisory lock ${JSON.stringify(name)} while the critical section was running`,
|
|
387
|
+
{
|
|
388
|
+
cause: callbackError instanceof Error ? callbackError : undefined,
|
|
389
|
+
currentEpoch: loss.currentEpoch,
|
|
390
|
+
epoch: lockBackend.epoch ?? null,
|
|
391
|
+
lockName: name
|
|
392
|
+
}
|
|
393
|
+
)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (failed) throw callbackError
|
|
397
|
+
|
|
398
|
+
return /** @type {T} */ (value)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Reports an acquisition served below the lock name's preferred tier
|
|
403
|
+
* (design §3): every fallback is visible on the framework-error channel, so a
|
|
404
|
+
* silent downgrade is impossible.
|
|
405
|
+
* @param {{lockName: string, reason: string, servedBy: import("./advisory-lock-backend-policy.js").AdvisoryLockTier}} args - Fallback details.
|
|
406
|
+
* @returns {void}
|
|
407
|
+
*/
|
|
408
|
+
reportAdvisoryLockFallback({lockName, reason, servedBy}) {
|
|
409
|
+
const policy = resolveAdvisoryLockBackendPolicy(lockName, this.configuration.getAdvisoryLockPolicy())
|
|
410
|
+
const payload = {
|
|
411
|
+
context: "advisory-lock-fallback",
|
|
412
|
+
lockName,
|
|
413
|
+
preferredTier: policy.preferredTier,
|
|
414
|
+
reason,
|
|
415
|
+
servedBy
|
|
416
|
+
}
|
|
417
|
+
const errorEvents = this.configuration.getErrorEvents()
|
|
418
|
+
|
|
419
|
+
errorEvents.emit("framework-error", payload)
|
|
420
|
+
errorEvents.emit("all-error", {...payload, errorType: "framework-error"})
|
|
421
|
+
}
|
|
422
|
+
|
|
251
423
|
/**
|
|
252
424
|
* Resolves the backend policy for one lock name and returns the file
|
|
253
425
|
* backend when the name explicitly opts into the file tier
|
|
@@ -320,7 +492,7 @@ export default class AdvisoryLockRunner {
|
|
|
320
492
|
* @param {{timeoutMs?: number | null}} args - Lock options.
|
|
321
493
|
* @returns {number | null} - Wait bound in milliseconds, or `null` for an indefinite wait.
|
|
322
494
|
*/
|
|
323
|
-
|
|
495
|
+
acquireTimeoutMs(args) {
|
|
324
496
|
if (args.timeoutMs !== undefined) return args.timeoutMs
|
|
325
497
|
|
|
326
498
|
return DEFAULT_ADVISORY_LOCK_ACQUIRE_TIMEOUT_MS
|
|
@@ -474,3 +646,4 @@ function advisoryLockPolicyAllowsFileTier(policy) {
|
|
|
474
646
|
|
|
475
647
|
export {AdvisoryLockBusyError, AdvisoryLockHoldTimeoutError, AdvisoryLockTimeoutError}
|
|
476
648
|
export {AdvisoryLockBackendUnavailableError, AdvisoryLockMinTierRefusedError} from "./advisory-lock-backend-policy.js"
|
|
649
|
+
export {AdvisoryLockLostError, AdvisoryLockServiceUnavailableError} from "./advisory-lock-errors.js"
|
|
@@ -29,7 +29,13 @@
|
|
|
29
29
|
* @typedef {boolean | null | string | undefined | Promise<void> | string[] | import("../drivers/base-column.js").default[] | import("../drivers/base-table.js").default | Record<string, string> | Record<string, string | undefined> | Record<string, import("../drivers/base-column.js").default>} RecordMetadataValue
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
-
import AdvisoryLockRunner, {
|
|
32
|
+
import AdvisoryLockRunner, {
|
|
33
|
+
AdvisoryLockBusyError,
|
|
34
|
+
AdvisoryLockHoldTimeoutError,
|
|
35
|
+
AdvisoryLockLostError,
|
|
36
|
+
AdvisoryLockServiceUnavailableError,
|
|
37
|
+
AdvisoryLockTimeoutError
|
|
38
|
+
} from "../advisory-lock-runner.js"
|
|
33
39
|
import BelongsToInstanceRelationship from "./instance-relationships/belongs-to.js"
|
|
34
40
|
import BelongsToRelationship from "./relationships/belongs-to.js"
|
|
35
41
|
import Configuration from "../../configuration.js"
|
|
@@ -5049,5 +5055,13 @@ VelociousDatabaseRecord.registerValidatorType("length", ValidatorsLength)
|
|
|
5049
5055
|
VelociousDatabaseRecord.registerValidatorType("presence", ValidatorsPresence)
|
|
5050
5056
|
VelociousDatabaseRecord.registerValidatorType("uniqueness", ValidatorsUniqueness)
|
|
5051
5057
|
|
|
5052
|
-
export {
|
|
5058
|
+
export {
|
|
5059
|
+
AdvisoryLockBusyError,
|
|
5060
|
+
AdvisoryLockHoldTimeoutError,
|
|
5061
|
+
AdvisoryLockLostError,
|
|
5062
|
+
AdvisoryLockServiceUnavailableError,
|
|
5063
|
+
AdvisoryLockTimeoutError,
|
|
5064
|
+
TenantDatabaseScopeError,
|
|
5065
|
+
ValidationError
|
|
5066
|
+
}
|
|
5053
5067
|
export default VelociousDatabaseRecord
|