velocious 1.0.709 → 1.0.711
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 +18 -2
- package/build/configuration.js +88 -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/advisory-lock-telemetry.js +20 -19
- package/build/database/record/index.js +16 -2
- package/build/src/configuration-types.d.ts +75 -4
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +18 -3
- package/build/src/configuration.d.ts +43 -2
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +85 -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/advisory-lock-telemetry.d.ts +14 -11
- package/build/src/database/advisory-lock-telemetry.d.ts.map +1 -1
- package/build/src/database/advisory-lock-telemetry.js +21 -20
- 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 +18 -2
- package/src/configuration.js +88 -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/advisory-lock-telemetry.js +20 -19
- package/src/database/record/index.js +16 -2
package/package.json
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Resolved lock-service configuration after environment and Beacon defaults.
|
|
5
|
+
* @typedef {object} LockServiceResolvedConfigType
|
|
6
|
+
* @property {string} [authToken] - Shared secret enforced by the host, when configured.
|
|
7
|
+
* @property {number} connectBackoffMs - Base backoff between connection retries.
|
|
8
|
+
* @property {number} connectRetries - Bounded connection retries before giving up.
|
|
9
|
+
* @property {number} connectTimeoutMs - Handshake timeout.
|
|
10
|
+
* @property {"database" | "none"} fallback - What to do when the service is unreachable.
|
|
11
|
+
* @property {string} host - Lock-service host.
|
|
12
|
+
* @property {number} leaseMs - Lease duration advertised to clients.
|
|
13
|
+
* @property {"beacon" | "in-process" | "off"} mode - Where the authority lives.
|
|
14
|
+
* @property {string} namespace - Prefix applied to every lock name.
|
|
15
|
+
* @property {number} port - Lock-service port.
|
|
16
|
+
* @property {string | null} statePath - Durable mirror for epochs and holders.
|
|
17
|
+
*/
|
|
18
|
+
|
|
3
19
|
/**
|
|
4
20
|
* @module types
|
|
5
21
|
*/
|
|
@@ -69,7 +85,7 @@
|
|
|
69
85
|
/**
|
|
70
86
|
* @typedef {object} DatabaseConfigurationType
|
|
71
87
|
* @property {number | null} [advisoryLockAcquireTimeoutMs] - Bound (ms) a blocking advisory-lock acquisition waits for a contended lock before reporting failure. Default: `900000` (15 minutes). `null` opts out and restores the driver's indefinite wait. A per-call `timeoutMs` argument always overrides this.
|
|
72
|
-
* @property {{slowAcquireMs?: number | null, slowHoldMs?: number | null}} [advisoryLockTelemetry] - Event-driven advisory-lock duration telemetry thresholds. `slowAcquireMs` (default `5000`) reports an acquire wait that completed slower than the threshold, including the eventual outcome; `advisory-lock-acquire-timeout` always fires when a wait exceeds its bound and carries the holder identity available on the tier at that moment. `slowHoldMs` (default `60000`) reports a hold that outlives the threshold at release. `null` disables that report. Reports fire on the events themselves — no periodic scanner — on `
|
|
88
|
+
* @property {{slowAcquireMs?: number | null, slowHoldMs?: number | null}} [advisoryLockTelemetry] - Event-driven advisory-lock duration telemetry thresholds. `slowAcquireMs` (default `5000`) reports an acquire wait that completed slower than the threshold, including the eventual outcome; `advisory-lock-acquire-timeout` always fires when a wait exceeds its bound and carries the holder identity available on the tier at that moment. `slowHoldMs` (default `60000`) reports a hold that outlives the threshold at release. `null` disables that report. Reports fire on the events themselves — no periodic scanner — on `getTelemetryEvents()` as `telemetry` with `context: "advisory-lock-telemetry"`. See [docs/advisory-locks.md](../docs/advisory-locks.md).
|
|
73
89
|
* @property {string} [databaseCharset] - Default character set applied by `db:create` via mysql/mariadb `CREATE DATABASE ... CHARACTER SET`. Distinct from `charset`, which is the client connection charset forwarded to the mysql2 driver.
|
|
74
90
|
* @property {string} [databaseCollation] - Default collation applied by `db:create` via mysql/mariadb `CREATE DATABASE ... COLLATE`.
|
|
75
91
|
* @property {string} [database] - Database name for this connection.
|
|
@@ -866,7 +882,7 @@
|
|
|
866
882
|
* behavior; the file backend is disabled by default and only selectable by
|
|
867
883
|
* explicit configuration.
|
|
868
884
|
* @typedef {object} AdvisoryLocksConfiguration
|
|
869
|
-
* @property {{file?: {directory?: string, enabled?: boolean, leaseMs?: number, renewIntervalMs?: number}}} [backends] - Backend enablement. `file.enabled` defaults to false; enabling it is the deployment's explicit single-host declaration.
|
|
885
|
+
* @property {{file?: {directory?: string, enabled?: boolean, leaseMs?: number, renewIntervalMs?: number}, lockServer?: {authToken?: string, connectTimeoutMs?: number, enabled?: boolean, host?: string, leaseMs?: number, port?: number, reprobeCooldownMs?: number, url?: string}}} [backends] - Backend enablement. `file.enabled` defaults to false; enabling it is the deployment's explicit single-host declaration. `lockServer` defaults to enabled when a host/port/url or the `VELOCIOUS_LOCK_SERVER_HOST`/`_PORT` env pair is configured.
|
|
870
886
|
* @property {import("./database/advisory-lock-backend-policy.js").AdvisoryLockBackendPolicyEntry[]} [policy] - Per-lock-name minimum/allowed/preferred tier rules (design §5).
|
|
871
887
|
*/
|
|
872
888
|
|
package/src/configuration.js
CHANGED
|
@@ -261,6 +261,12 @@ export default class VelociousConfiguration {
|
|
|
261
261
|
* (browser/Expo, no `fs`) cannot create one at all.
|
|
262
262
|
* @type {import("./database/advisory-lock-file-backend.js").default | undefined} */
|
|
263
263
|
_advisoryLockFileBackend = undefined
|
|
264
|
+
/**
|
|
265
|
+
* Lazily created lock-server (tier (b)) backend; undefined when the tier is
|
|
266
|
+
* not configured.
|
|
267
|
+
* @type {import("./database/advisory-lock-lock-server-backend.js").default | undefined}
|
|
268
|
+
*/
|
|
269
|
+
_advisoryLockLockServerBackend = undefined
|
|
264
270
|
|
|
265
271
|
/**
|
|
266
272
|
* Most recent advisory-lock acquisition per lock name — the "which backend
|
|
@@ -519,6 +525,7 @@ export default class VelociousConfiguration {
|
|
|
519
525
|
* @type {WeakSet<object>} */
|
|
520
526
|
this._appliedRouteMounts = new WeakSet()
|
|
521
527
|
this._errorEvents = new EventEmitter()
|
|
528
|
+
this._telemetryEvents = new EventEmitter()
|
|
522
529
|
|
|
523
530
|
/**
|
|
524
531
|
* Stores the database pools value.
|
|
@@ -3859,6 +3866,19 @@ export default class VelociousConfiguration {
|
|
|
3859
3866
|
return this._errorEvents
|
|
3860
3867
|
}
|
|
3861
3868
|
|
|
3869
|
+
/**
|
|
3870
|
+
* Framework telemetry emitter. Telemetry reports measurements — lock contention,
|
|
3871
|
+
* slow queries, queue depth — that are worth observing but are not failures. It is
|
|
3872
|
+
* deliberately separate from `getErrorEvents()`: a bug reporter subscribes to the
|
|
3873
|
+
* error channel on the promise that every payload there carries a thrown value, and
|
|
3874
|
+
* a contention measurement emitted as `framework-error` turns into a bug report on
|
|
3875
|
+
* every occurrence instead of a metric.
|
|
3876
|
+
* @returns {import("eventemitter3").EventEmitter} - Framework telemetry events emitter.
|
|
3877
|
+
*/
|
|
3878
|
+
getTelemetryEvents() {
|
|
3879
|
+
return this._telemetryEvents
|
|
3880
|
+
}
|
|
3881
|
+
|
|
3862
3882
|
/**
|
|
3863
3883
|
* Registers a reporter that can add client-safe metadata to frontend-model error payloads.
|
|
3864
3884
|
* @param {import("./configuration-types.js").ClientErrorPayloadReporterType} reporter - Reporter callback.
|
|
@@ -4205,11 +4225,21 @@ export default class VelociousConfiguration {
|
|
|
4205
4225
|
* Full backward compatibility: an app that configures nothing gets today's
|
|
4206
4226
|
* driver-native-only behavior — the file backend is **disabled by default**
|
|
4207
4227
|
* and only ever selectable by explicit configuration.
|
|
4208
|
-
* @returns {{backends: {file: {directory?: string, enabled: boolean, leaseMs: number, renewIntervalMs: number}}, policy: import("./database/advisory-lock-backend-policy.js").AdvisoryLockBackendPolicyEntry[]}} - Advisory-lock configuration with defaults applied.
|
|
4228
|
+
* @returns {{backends: {file: {directory?: string, enabled: boolean, leaseMs: number, renewIntervalMs: number}, lockServer: {authToken: string, connectTimeoutMs: number, enabled: boolean, leaseMs: number, reprobeCooldownMs: number, url: string}}, policy: import("./database/advisory-lock-backend-policy.js").AdvisoryLockBackendPolicyEntry[]}} - Advisory-lock configuration with defaults applied.
|
|
4209
4229
|
*/
|
|
4210
4230
|
getAdvisoryLockConfig() {
|
|
4211
4231
|
const configured = this._advisoryLocks || {}
|
|
4212
4232
|
const fileConfigured = configured.backends?.file || {}
|
|
4233
|
+
const lockServerConfigured = configured.backends?.lockServer || {}
|
|
4234
|
+
const envHost = process.env.VELOCIOUS_LOCK_SERVER_HOST
|
|
4235
|
+
const envPortRaw = process.env.VELOCIOUS_LOCK_SERVER_PORT
|
|
4236
|
+
const envPort = envPortRaw ? Number(envPortRaw) : undefined
|
|
4237
|
+
const lockServerHost = lockServerConfigured.host || envHost
|
|
4238
|
+
const lockServerPort = typeof lockServerConfigured.port === "number"
|
|
4239
|
+
? lockServerConfigured.port
|
|
4240
|
+
: (typeof envPort === "number" && Number.isFinite(envPort) ? envPort : 3006)
|
|
4241
|
+
const lockServerUrl = lockServerConfigured.url
|
|
4242
|
+
|| `ws://${lockServerHost || "127.0.0.1"}:${lockServerPort}/websocket`
|
|
4213
4243
|
|
|
4214
4244
|
return {
|
|
4215
4245
|
backends: {
|
|
@@ -4218,6 +4248,18 @@ export default class VelociousConfiguration {
|
|
|
4218
4248
|
enabled: fileConfigured.enabled === true,
|
|
4219
4249
|
leaseMs: typeof fileConfigured.leaseMs === "number" ? fileConfigured.leaseMs : 30_000,
|
|
4220
4250
|
renewIntervalMs: typeof fileConfigured.renewIntervalMs === "number" ? fileConfigured.renewIntervalMs : 10_000
|
|
4251
|
+
},
|
|
4252
|
+
lockServer: {
|
|
4253
|
+
// Enabled only by explicit configuration or an explicit env pair, so
|
|
4254
|
+
// an app that configures nothing keeps today's driver-native behavior.
|
|
4255
|
+
authToken: typeof lockServerConfigured.authToken === "string" ? lockServerConfigured.authToken : "",
|
|
4256
|
+
connectTimeoutMs: typeof lockServerConfigured.connectTimeoutMs === "number" ? lockServerConfigured.connectTimeoutMs : 3000,
|
|
4257
|
+
enabled: typeof lockServerConfigured.enabled === "boolean"
|
|
4258
|
+
? lockServerConfigured.enabled
|
|
4259
|
+
: Boolean(lockServerConfigured.url || lockServerConfigured.host || envHost || typeof lockServerConfigured.port === "number" || envPortRaw),
|
|
4260
|
+
leaseMs: typeof lockServerConfigured.leaseMs === "number" ? lockServerConfigured.leaseMs : 30_000,
|
|
4261
|
+
reprobeCooldownMs: typeof lockServerConfigured.reprobeCooldownMs === "number" ? lockServerConfigured.reprobeCooldownMs : 5000,
|
|
4262
|
+
url: lockServerUrl
|
|
4221
4263
|
}
|
|
4222
4264
|
},
|
|
4223
4265
|
policy: Array.isArray(configured.policy) ? configured.policy : []
|
|
@@ -4267,6 +4309,39 @@ export default class VelociousConfiguration {
|
|
|
4267
4309
|
return this._advisoryLockFileBackend
|
|
4268
4310
|
}
|
|
4269
4311
|
|
|
4312
|
+
/**
|
|
4313
|
+
* Returns the lock-server advisory-lock backend (tier (b)) when this
|
|
4314
|
+
* deployment configured one, creating it on first use. Returns `undefined`
|
|
4315
|
+
* when the tier is not configured — callers must never invent one.
|
|
4316
|
+
*
|
|
4317
|
+
* The backend keeps a single socket per process; it holds no database
|
|
4318
|
+
* connection while waiting for or holding a lease.
|
|
4319
|
+
* @returns {Promise<import("./database/advisory-lock-lock-server-backend.js").default | undefined>} - Resolves with the configured backend, or undefined.
|
|
4320
|
+
*/
|
|
4321
|
+
async getAdvisoryLockLockServerBackend() {
|
|
4322
|
+
const config = this.getAdvisoryLockConfig()
|
|
4323
|
+
|
|
4324
|
+
if (!config.backends.lockServer.enabled) return undefined
|
|
4325
|
+
if (this._advisoryLockLockServerBackend) return this._advisoryLockLockServerBackend
|
|
4326
|
+
|
|
4327
|
+
const {default: AdvisoryLockLockServerBackend} = await import("./database/advisory-lock-lock-server-backend.js")
|
|
4328
|
+
const backend = new AdvisoryLockLockServerBackend({
|
|
4329
|
+
authToken: config.backends.lockServer.authToken,
|
|
4330
|
+
connectTimeoutMs: config.backends.lockServer.connectTimeoutMs,
|
|
4331
|
+
leaseMs: config.backends.lockServer.leaseMs,
|
|
4332
|
+
onError: (error) => {
|
|
4333
|
+
this.getErrorEvents().emit("framework-error", {context: "advisory-lock-lock-server", error})
|
|
4334
|
+
this.getErrorEvents().emit("all-error", {context: "advisory-lock-lock-server", error, errorType: "framework-error"})
|
|
4335
|
+
},
|
|
4336
|
+
reprobeCooldownMs: config.backends.lockServer.reprobeCooldownMs,
|
|
4337
|
+
url: config.backends.lockServer.url
|
|
4338
|
+
})
|
|
4339
|
+
|
|
4340
|
+
this._advisoryLockLockServerBackend = backend
|
|
4341
|
+
|
|
4342
|
+
return backend
|
|
4343
|
+
}
|
|
4344
|
+
|
|
4270
4345
|
/**
|
|
4271
4346
|
* Records which backend tier served one advisory-lock acquisition so
|
|
4272
4347
|
* `getAdvisoryLockBackendStatus()` can answer "which backend served this
|
|
@@ -4287,11 +4362,12 @@ export default class VelociousConfiguration {
|
|
|
4287
4362
|
* carries fencing tokens, and the resolved policy (minimum/allowed/preferred
|
|
4288
4363
|
* tiers) for the name.
|
|
4289
4364
|
* @param {string} lockName - Lock name.
|
|
4290
|
-
* @returns {{acquisition: {backend: import("./database/advisory-lock-backend-policy.js").AdvisoryLockTier, fencingSupported: boolean, servedAt: string} | null, policy: import("./database/advisory-lock-backend-policy.js").ResolvedAdvisoryLockBackendPolicy}} - Backend status for the name.
|
|
4365
|
+
* @returns {{acquisition: {backend: import("./database/advisory-lock-backend-policy.js").AdvisoryLockTier, fencingSupported: boolean, servedAt: string} | null, lockServer: {connected: boolean, lastError: string | null, lastProbeAt: string | null, leasedNames: string[]} | null, policy: import("./database/advisory-lock-backend-policy.js").ResolvedAdvisoryLockBackendPolicy}} - Backend status for the name.
|
|
4291
4366
|
*/
|
|
4292
4367
|
getAdvisoryLockBackendStatus(lockName) {
|
|
4293
4368
|
return {
|
|
4294
4369
|
acquisition: this._advisoryLockAcquisitions.get(lockName) ?? null,
|
|
4370
|
+
lockServer: this._advisoryLockLockServerBackend?.getStatus() ?? null,
|
|
4295
4371
|
policy: this.resolveAdvisoryLockPolicy(lockName)
|
|
4296
4372
|
}
|
|
4297
4373
|
}
|
|
@@ -4384,6 +4460,16 @@ export default class VelociousConfiguration {
|
|
|
4384
4460
|
closeErrors.push(error instanceof Error ? error : new Error(String(error)))
|
|
4385
4461
|
}
|
|
4386
4462
|
|
|
4463
|
+
try {
|
|
4464
|
+
// Release lock-server leases explicitly first, then drop the socket:
|
|
4465
|
+
// the server reclaims a closed connection anyway, but an explicit
|
|
4466
|
+
// release hands the name to the next waiter without waiting for the
|
|
4467
|
+
// transport teardown.
|
|
4468
|
+
await this._advisoryLockLockServerBackend?.close()
|
|
4469
|
+
} catch (error) {
|
|
4470
|
+
closeErrors.push(error instanceof Error ? error : new Error(String(error)))
|
|
4471
|
+
}
|
|
4472
|
+
|
|
4387
4473
|
for (const pool of Object.values(this.databasePools)) {
|
|
4388
4474
|
if (!pool) continue
|
|
4389
4475
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {AdvisoryLockBackendUnavailableError} from "./advisory-lock-backend-policy.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Thrown when a lock-server acquisition could not be attempted because the
|
|
7
|
+
* lock server is unreachable. Distinct from `AdvisoryLockTimeoutError` ("the
|
|
8
|
+
* server was reachable, the holder never released") so a caller can react to
|
|
9
|
+
* an infrastructure outage without guessing.
|
|
10
|
+
*/
|
|
11
|
+
export class AdvisoryLockServiceUnavailableError extends Error {
|
|
12
|
+
/**
|
|
13
|
+
* Runs constructor.
|
|
14
|
+
* @param {string} message - Error message.
|
|
15
|
+
* @param {{lockName: string, reason: string}} args - Lock name and failure reason.
|
|
16
|
+
*/
|
|
17
|
+
constructor(message, {lockName, reason}) {
|
|
18
|
+
super(message)
|
|
19
|
+
this.name = "AdvisoryLockServiceUnavailableError"
|
|
20
|
+
this.lockName = lockName
|
|
21
|
+
this.reason = reason
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Thrown after a critical section ran on the lock-server tier while its lease
|
|
27
|
+
* was fenced or lost (design §7/§8): the section may have overlapped another
|
|
28
|
+
* holder, so its result must not be trusted as exclusive.
|
|
29
|
+
*/
|
|
30
|
+
export class AdvisoryLockLostError extends Error {
|
|
31
|
+
/**
|
|
32
|
+
* Runs constructor.
|
|
33
|
+
* @param {string} message - Error message.
|
|
34
|
+
* @param {{cause?: Error, currentEpoch: number | null, epoch: number | null, lockName: string}} args - Fencing details.
|
|
35
|
+
*/
|
|
36
|
+
constructor(message, {cause, currentEpoch, epoch, lockName}) {
|
|
37
|
+
super(message)
|
|
38
|
+
this.name = "AdvisoryLockLostError"
|
|
39
|
+
this.lockName = lockName
|
|
40
|
+
this.epoch = epoch
|
|
41
|
+
this.currentEpoch = currentEpoch
|
|
42
|
+
this.cause = cause
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Builds the error thrown when a lock name's minimum tier cannot be served and
|
|
48
|
+
* no eligible tier is available (design §9, fail-closed).
|
|
49
|
+
* @param {{lockName: string, minTier: import("./advisory-lock-backend-policy.js").AdvisoryLockTier, reasons: string[]}} args - Unavailability details.
|
|
50
|
+
* @returns {AdvisoryLockBackendUnavailableError} - Error to throw.
|
|
51
|
+
*/
|
|
52
|
+
export function advisoryLockBackendUnavailable(args) {
|
|
53
|
+
const {lockName, minTier, reasons} = args
|
|
54
|
+
|
|
55
|
+
return new AdvisoryLockBackendUnavailableError(
|
|
56
|
+
`No eligible advisory-lock backend is available for ${JSON.stringify(lockName)} (minimum tier ${JSON.stringify(minTier)}): ${reasons.join("; ")}`,
|
|
57
|
+
{lockName, minTier, reasons}
|
|
58
|
+
)
|
|
59
|
+
}
|