velocious 1.0.708 → 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 +19 -3
- package/build/configuration.js +85 -3
- 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/migrator.js +29 -1
- package/build/database/record/index.js +16 -2
- package/build/src/configuration-types.d.ts +76 -4
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +19 -4
- package/build/src/configuration.d.ts +42 -2
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +82 -4
- 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/migrator.d.ts +13 -0
- package/build/src/database/migrator.d.ts.map +1 -1
- package/build/src/database/migrator.js +27 -2
- 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 +19 -3
- package/src/configuration.js +85 -3
- 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/migrator.js +29 -1
- 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"
|
package/src/database/migrator.js
CHANGED
|
@@ -124,6 +124,28 @@ export default class VelociousDatabaseMigrator {
|
|
|
124
124
|
return false
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Application migrations belong to the schema-owning lifecycle. HTTP worker
|
|
129
|
+
* threads (`processContext.type === "worker-handler"`) each run the
|
|
130
|
+
* application's `initializeModels` hook, so an app that migrates from that hook
|
|
131
|
+
* would apply the pending set (and write the structure dump) once per worker.
|
|
132
|
+
* The server process and deploy migration tasks migrate before workers start,
|
|
133
|
+
* so workers skip here instead of repeating that work.
|
|
134
|
+
* @returns {Promise<boolean>} - Whether this lifecycle must skip application migrations.
|
|
135
|
+
*/
|
|
136
|
+
async _skipApplicationMigrationsInWorkerLifecycle() {
|
|
137
|
+
const processContext = this.configuration.getApplicationProcessContext()
|
|
138
|
+
|
|
139
|
+
if (processContext?.type !== "worker-handler") return false
|
|
140
|
+
|
|
141
|
+
await this.logger.debug("Skipping application migrations in an HTTP worker thread - the schema-owning process migrates before workers start", {
|
|
142
|
+
instanceId: processContext.instanceId,
|
|
143
|
+
processType: processContext.type
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
return true
|
|
147
|
+
}
|
|
148
|
+
|
|
127
149
|
/**
|
|
128
150
|
* Runs migrate files.
|
|
129
151
|
* @param {import("./migrator/types.js").MigrationObjectType[]} files - Files.
|
|
@@ -131,8 +153,9 @@ export default class VelociousDatabaseMigrator {
|
|
|
131
153
|
* @returns {Promise<number>} - Number of migrations actually applied (not skipped as already-run).
|
|
132
154
|
*/
|
|
133
155
|
async migrateFiles(files, importCallback) {
|
|
156
|
+
if (await this._skipApplicationMigrationsInWorkerLifecycle()) return 0
|
|
157
|
+
|
|
134
158
|
let appliedCount = 0
|
|
135
|
-
console.error("[MIG-DEBUG] migrateFiles count=" + files.length)
|
|
136
159
|
|
|
137
160
|
await this.configuration.ensureConnections({databaseIdentifiers: this.databaseIdentifiers, name: "Database migrator: migrate files"}, async () => {
|
|
138
161
|
for (const migration of files) {
|
|
@@ -162,10 +185,15 @@ export default class VelociousDatabaseMigrator {
|
|
|
162
185
|
|
|
163
186
|
/**
|
|
164
187
|
* Runs migrate files from require context.
|
|
188
|
+
*
|
|
189
|
+
* Application migrations belong to the schema-owning lifecycle; see
|
|
190
|
+
* {@link VelociousDatabaseMigrator#_skipApplicationMigrationsInWorkerLifecycle}.
|
|
165
191
|
* @param {import("./migrator/types.js").RequireMigrationContextType} requireContext - Require context.
|
|
166
192
|
* @returns {Promise<void>} - Resolves when complete.
|
|
167
193
|
*/
|
|
168
194
|
async migrateFilesFromRequireContext(requireContext) {
|
|
195
|
+
if (await this._skipApplicationMigrationsInWorkerLifecycle()) return
|
|
196
|
+
|
|
169
197
|
const files = this.migrationsFromRequireContext(requireContext)
|
|
170
198
|
|
|
171
199
|
await this.configuration.ensureConnections({databaseIdentifiers: this.databaseIdentifiers, name: "Database migrator: migrate require-context files"}, async () => {
|
|
@@ -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
|