velocious 1.0.693 → 1.0.695
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 +8 -0
- package/build/configuration.js +61 -1
- package/build/database/advisory-lock-runner.js +31 -0
- package/build/database/advisory-lock-telemetry.js +129 -0
- package/build/database/drivers/base.js +114 -2
- package/build/database/drivers/mssql/index.js +16 -0
- package/build/database/drivers/mysql/index.js +14 -0
- package/build/database/drivers/pgsql/index.js +17 -0
- package/build/database/drivers/sqlite/base.js +20 -0
- package/build/database/drivers/sqlite/index.js +33 -0
- package/build/database/record/index.js +28 -0
- package/build/routes/built-in/mcp/controller.js +63 -0
- package/build/src/configuration-types.d.ts +28 -0
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +8 -1
- package/build/src/configuration.d.ts +24 -1
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +57 -2
- package/build/src/database/advisory-lock-runner.d.ts +32 -0
- package/build/src/database/advisory-lock-runner.d.ts.map +1 -1
- package/build/src/database/advisory-lock-runner.js +29 -1
- package/build/src/database/advisory-lock-telemetry.d.ts +129 -0
- package/build/src/database/advisory-lock-telemetry.d.ts.map +1 -0
- package/build/src/database/advisory-lock-telemetry.js +122 -0
- package/build/src/database/drivers/base.d.ts +51 -0
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +104 -3
- package/build/src/database/drivers/mssql/index.d.ts +8 -0
- package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mssql/index.js +13 -1
- package/build/src/database/drivers/mysql/index.d.ts +8 -0
- package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mysql/index.js +13 -1
- package/build/src/database/drivers/pgsql/index.d.ts +8 -0
- package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
- package/build/src/database/drivers/pgsql/index.js +14 -1
- package/build/src/database/drivers/sqlite/base.d.ts +12 -0
- package/build/src/database/drivers/sqlite/base.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/base.js +18 -1
- package/build/src/database/drivers/sqlite/index.d.ts +15 -0
- package/build/src/database/drivers/sqlite/index.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/index.js +31 -1
- package/build/src/database/record/index.d.ts +54 -0
- package/build/src/database/record/index.d.ts.map +1 -1
- package/build/src/database/record/index.js +26 -1
- package/build/src/routes/built-in/mcp/controller.d.ts +14 -0
- package/build/src/routes/built-in/mcp/controller.d.ts.map +1 -0
- package/build/src/routes/built-in/mcp/controller.js +55 -0
- package/package.json +1 -1
- package/src/configuration-types.js +8 -0
- package/src/configuration.js +61 -1
- package/src/database/advisory-lock-runner.js +31 -0
- package/src/database/advisory-lock-telemetry.js +129 -0
- package/src/database/drivers/base.js +114 -2
- package/src/database/drivers/mssql/index.js +16 -0
- package/src/database/drivers/mysql/index.js +14 -0
- package/src/database/drivers/pgsql/index.js +17 -0
- package/src/database/drivers/sqlite/base.js +20 -0
- package/src/database/drivers/sqlite/index.js +33 -0
- package/src/database/record/index.js +28 -0
- package/src/routes/built-in/mcp/controller.js +63 -0
package/README.md
CHANGED
|
@@ -4,7 +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, including online CRUD for [composite primary keys](docs/composite-primary-keys.md)
|
|
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
|
+
* Connection-scoped advisory locks with automatic cleanup before pooled connections are reused or closed, a non-throwing `withAdvisoryLockOrDefer` result for contended hot paths, and event-driven slow acquire/hold duration telemetry with holder attribution (see [docs/advisory-locks.md](docs/advisory-locks.md)) (see [docs/advisory-locks.md](docs/advisory-locks.md))
|
|
8
8
|
* Built-in record auditing for model lifecycle changes (see [docs/auditing.md](docs/auditing.md))
|
|
9
9
|
* Declarative state machines for models, with typed event methods generated into the base model (see [docs/state-machine.md](docs/state-machine.md))
|
|
10
10
|
* Migrations for schema changes and UTC datetime storage, including caller-selected `pre-runtime` and `post-publication` execution sets and recorded `changeTable` batches that combine operations into one `ALTER` on bulk-capable drivers (see [docs/database-migrations.md](docs/database-migrations.md), [docs/migration-execution-phases.md](docs/migration-execution-phases.md), and [docs/change-table.md](docs/change-table.md))
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
/**
|
|
70
70
|
* @typedef {object} DatabaseConfigurationType
|
|
71
71
|
* @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 `getErrorEvents()` as `framework-error` with `context: "advisory-lock-telemetry"`. See [docs/advisory-locks.md](../docs/advisory-locks.md).
|
|
72
73
|
* @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.
|
|
73
74
|
* @property {string} [databaseCollation] - Default collation applied by `db:create` via mysql/mariadb `CREATE DATABASE ... COLLATE`.
|
|
74
75
|
* @property {string} [database] - Database name for this connection.
|
|
@@ -744,6 +745,12 @@
|
|
|
744
745
|
* @property {string} [path] - HTTP path for the built-in API manifest endpoint. Defaults to `/api/manifest`.
|
|
745
746
|
* @property {string} [token] - Bearer token required in the `Authorization: Bearer <token>` header. When set, requests without a matching token are not routed (404), so the endpoint stays hidden.
|
|
746
747
|
*/
|
|
748
|
+
/**
|
|
749
|
+
* @typedef {object} McpConfiguration
|
|
750
|
+
* @property {string} [path] - HTTP path for the built-in MCP endpoint. Defaults to `/mcp`.
|
|
751
|
+
* @property {string} [token] - Bearer token required in the `Authorization` header. When set, requests without a matching token are not routed (404), so the endpoint stays hidden.
|
|
752
|
+
*/
|
|
753
|
+
|
|
747
754
|
|
|
748
755
|
/**
|
|
749
756
|
* @typedef {object} DebugEndpointConfiguration
|
|
@@ -776,6 +783,7 @@
|
|
|
776
783
|
* @property {boolean} [debug] - Enable debug logging.
|
|
777
784
|
* @property {boolean | DebugEndpointConfiguration} [debugEndpoint] - Enable the built-in debug endpoint. Defaults to false.
|
|
778
785
|
* @property {boolean | ApiManifestConfiguration} [apiManifest] - Enable the built-in API manifest endpoint. Defaults to false.
|
|
786
|
+
* @property {boolean | McpConfiguration} [mcp] - Enable the built-in MCP endpoint on the HTTP server (JSON-RPC over HTTP in addition to the `velocious mcp` stdio server). Defaults to false.
|
|
779
787
|
* @property {string} [directory] - Base directory for the project.
|
|
780
788
|
* @property {boolean} [enforceTenantDatabaseScopes] - Require tenant-switched model queries to resolve a tenant database identifier. Defaults to true.
|
|
781
789
|
* @property {string} [environment] - Current environment name.
|
package/build/configuration.js
CHANGED
|
@@ -260,7 +260,7 @@ export default class VelociousConfiguration {
|
|
|
260
260
|
* Runs constructor.
|
|
261
261
|
* @param {import("./configuration-types.js").ConfigurationArgsType} args - Configuration arguments.
|
|
262
262
|
*/
|
|
263
|
-
constructor({abilityResolver, abilityResources, attachments, autoload = true, backgroundJobs, backendProjects, beacon, cookieSecret, cors, database, debug = false, debugEndpoint = false, apiManifest = false, directory, enforceTenantDatabaseScopes = true, environment, environmentHandler, exposeInternalErrorsToClients, frontendTenantSqlite, httpServer, initializeModels, initializers, locale, localeFallbacks, locales, logging, mailerBackend, packages, requestTimeoutMs, routeResolverHooks, scheduledBackgroundJobs, secureFrontendModelErrors, structureSql, sync, tenantDatabaseProviders, tenantDatabaseResolver, tenantResolver, testing, timeZone, timezoneOffsetMinutes, trustedProxies, websocketChannelResolver, websocketMessageHandlerResolver, ...restArgs}) {
|
|
263
|
+
constructor({abilityResolver, abilityResources, attachments, autoload = true, backgroundJobs, backendProjects, beacon, cookieSecret, cors, database, debug = false, debugEndpoint = false, apiManifest = false, directory, enforceTenantDatabaseScopes = true, environment, environmentHandler, exposeInternalErrorsToClients, frontendTenantSqlite, httpServer, initializeModels, initializers, locale, localeFallbacks, locales, logging, mailerBackend, mcp = false, packages, requestTimeoutMs, routeResolverHooks, scheduledBackgroundJobs, secureFrontendModelErrors, structureSql, sync, tenantDatabaseProviders, tenantDatabaseResolver, tenantResolver, testing, timeZone, timezoneOffsetMinutes, trustedProxies, websocketChannelResolver, websocketMessageHandlerResolver, ...restArgs}) {
|
|
264
264
|
restArgsError(restArgs)
|
|
265
265
|
|
|
266
266
|
this._abilityResolver = abilityResolver
|
|
@@ -304,6 +304,7 @@ export default class VelociousConfiguration {
|
|
|
304
304
|
this.debug = debug
|
|
305
305
|
this._debugEndpoint = this._normalizeDebugEndpoint(debugEndpoint)
|
|
306
306
|
this._apiManifest = this._normalizeApiManifest(apiManifest)
|
|
307
|
+
this._mcp = this._normalizeMcp(mcp)
|
|
307
308
|
this._environment = environment || globalThis.process?.env.VELOCIOUS_ENV || globalThis.process?.env.NODE_ENV || "development"
|
|
308
309
|
this._environmentHandler = environmentHandler
|
|
309
310
|
this._enforceTenantDatabaseScopes = enforceTenantDatabaseScopes
|
|
@@ -486,6 +487,7 @@ export default class VelociousConfiguration {
|
|
|
486
487
|
this._routeResolverHooks = [...(routeResolverHooks || [])]
|
|
487
488
|
this._addDebugEndpointRouteHook()
|
|
488
489
|
this._addApiManifestRouteHook()
|
|
490
|
+
this._addMcpRouteHook()
|
|
489
491
|
|
|
490
492
|
/**
|
|
491
493
|
* Stores the applied route mounts value.
|
|
@@ -625,6 +627,63 @@ export default class VelociousConfiguration {
|
|
|
625
627
|
})
|
|
626
628
|
}
|
|
627
629
|
|
|
630
|
+
/**
|
|
631
|
+
* Runs normalize mcp.
|
|
632
|
+
* @param {boolean | {path?: string, token?: string}} value - MCP endpoint configuration.
|
|
633
|
+
* @returns {{enabled: boolean, path: string, token: string | null}} - Normalized MCP endpoint configuration.
|
|
634
|
+
*/
|
|
635
|
+
_normalizeMcp(value) {
|
|
636
|
+
if (value === false || value === undefined) return {enabled: false, path: "/mcp", token: null}
|
|
637
|
+
if (value === true) return {enabled: true, path: "/mcp", token: null}
|
|
638
|
+
|
|
639
|
+
if (typeof value !== "object" || value === null) {
|
|
640
|
+
throw new Error(`Expected mcp to be a boolean or object, got: ${String(value)}`)
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const path = value.path || "/mcp"
|
|
644
|
+
|
|
645
|
+
if (typeof path !== "string" || !path.startsWith("/")) {
|
|
646
|
+
throw new Error(`Expected mcp.path to be a string starting with '/', got: ${String(path)}`)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const token = value.token === undefined || value.token === null ? null : value.token
|
|
650
|
+
|
|
651
|
+
if (token !== null && (typeof token !== "string" || !token.trim())) {
|
|
652
|
+
throw new Error(`Expected mcp.token to be a non-empty string, got: ${String(token)}`)
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return {enabled: true, path, token: token === null ? null : token.trim()}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Runs add mcp route hook.
|
|
660
|
+
* @returns {void} - No return value.
|
|
661
|
+
*/
|
|
662
|
+
_addMcpRouteHook() {
|
|
663
|
+
if (!this._mcp.enabled) return
|
|
664
|
+
|
|
665
|
+
this.addRouteResolverHook(({currentPath, request}) => {
|
|
666
|
+
if (request.httpMethod() !== "POST") return null
|
|
667
|
+
if (currentPath !== this._mcp.path) return null
|
|
668
|
+
|
|
669
|
+
// When a token is configured, an unauthenticated request gets no route at
|
|
670
|
+
// all (404) rather than a 401, so the endpoint's existence stays hidden.
|
|
671
|
+
if (this._mcp.token && !this.debugEndpointRequestAuthorized(request, this._mcp.token)) return null
|
|
672
|
+
|
|
673
|
+
return {
|
|
674
|
+
action: "handle",
|
|
675
|
+
controller: "velociousMcp",
|
|
676
|
+
controllerPath: "./built-in/mcp/controller.js",
|
|
677
|
+
// Tenant and ability resolution belong to the frontend-model dispatcher
|
|
678
|
+
// per command (withFrontendModelRequestContext), exactly like the regular
|
|
679
|
+
// /frontend-models route — the MCP transport never bypasses them.
|
|
680
|
+
skipAbilityResolution: true,
|
|
681
|
+
skipTenantResolution: true,
|
|
682
|
+
viewPath: "./built-in/mcp"
|
|
683
|
+
}
|
|
684
|
+
})
|
|
685
|
+
}
|
|
686
|
+
|
|
628
687
|
/**
|
|
629
688
|
* Runs add debug endpoint route hook.
|
|
630
689
|
* @returns {void} - No return value.
|
|
@@ -1042,6 +1101,7 @@ export default class VelociousConfiguration {
|
|
|
1042
1101
|
enforceTenantDatabaseScopes: this.getEnforceTenantDatabaseScopes(),
|
|
1043
1102
|
exposeInternalErrorsToClients: this.getExposeInternalErrorsToClients(),
|
|
1044
1103
|
initialized: this._isInitialized,
|
|
1104
|
+
mcp: this._mcp.enabled ? {enabled: true, path: this._mcp.path, tokenConfigured: Boolean(this._mcp.token)} : {enabled: false},
|
|
1045
1105
|
logging: {
|
|
1046
1106
|
debugLowLevel: this._logging?.debugLowLevel === true,
|
|
1047
1107
|
outputs: this._logging ? Object.keys(this._logging) : []
|
|
@@ -118,6 +118,37 @@ export default class AdvisoryLockRunner {
|
|
|
118
118
|
})
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Runs a callback if the advisory lock can be acquired without waiting;
|
|
123
|
+
* otherwise returns a `{acquired: false}` defer result instead of throwing.
|
|
124
|
+
*
|
|
125
|
+
* This is the non-throwing counterpart of `withAdvisoryLockOrFail` for hot
|
|
126
|
+
* paths: "holder busy, defer me" is a first-class outcome, so a contended
|
|
127
|
+
* lock produces zero exception traffic and — because the probe is a single
|
|
128
|
+
* immediate try-acquire — zero pinned connections. Callers reschedule via
|
|
129
|
+
* their own event-driven retry machinery. The result form is identical
|
|
130
|
+
* across tiers: the driver's `tryAcquireAdvisoryLock` maps per-tier (MySQL
|
|
131
|
+
* `GET_LOCK(name, 0)`, PostgreSQL `pg_try_advisory_lock`, SQL Server
|
|
132
|
+
* `sp_getapplock @LockTimeout = 0`, SQLite process-local queue, SQLite Node
|
|
133
|
+
* file lock), so a caller never branches on the backend.
|
|
134
|
+
* @template T
|
|
135
|
+
* @param {string} name - Lock name.
|
|
136
|
+
* @param {(args?: {control: import("awaitery/build/timeout.js").TimeoutControl}) => Promise<T>} callback - Callback to invoke while the lock is held.
|
|
137
|
+
* @param {{holdTimeoutMs?: number | null, dedicatedConnection?: boolean}} [args] - Hold timeout options.
|
|
138
|
+
* @returns {Promise<{acquired: true, result: T} | {acquired: false, reason: "contended", lockName: string}>} - Either the held-callback result or the defer outcome.
|
|
139
|
+
*/
|
|
140
|
+
async withAdvisoryLockOrDefer(name, callback, args = {}) {
|
|
141
|
+
return await this.withLockConnection(args, async (connection) => {
|
|
142
|
+
const acquired = await connection.tryAcquireAdvisoryLock(name)
|
|
143
|
+
|
|
144
|
+
if (!acquired) return {acquired: false, reason: "contended", lockName: name}
|
|
145
|
+
|
|
146
|
+
const result = await this.runLockedCallback({callback, connection, holdTimeoutMs: args.holdTimeoutMs, name})
|
|
147
|
+
|
|
148
|
+
return {acquired: true, result}
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
121
152
|
/**
|
|
122
153
|
* Runs the lock holder callback and releases the lock from its owning connection.
|
|
123
154
|
* @template T
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Event-driven advisory-lock duration telemetry (STC-261, parent epic STC-209).
|
|
5
|
+
*
|
|
6
|
+
* A lock convoy is only fixable when it is *attributable*: which lock name was
|
|
7
|
+
* contended, on which backend tier, for how long, and who held it. These reports
|
|
8
|
+
* fire on the acquisition/release events themselves — never from a periodic
|
|
9
|
+
* scanner — so a contended lock in production surfaces on the framework-error
|
|
10
|
+
* channel with the holder identity available on that tier at that moment,
|
|
11
|
+
* without SSH forensics.
|
|
12
|
+
*
|
|
13
|
+
* Three events, each with its own threshold (all configurable per database via
|
|
14
|
+
* `advisoryLockTelemetry`, see `docs/advisory-locks.md`):
|
|
15
|
+
*
|
|
16
|
+
* - `advisory-lock-slow-acquire` — an acquire wait completed slower than
|
|
17
|
+
* `slowAcquireMs` (default 5000), including the eventual outcome (`acquired`
|
|
18
|
+
* or `timeout`).
|
|
19
|
+
* - `advisory-lock-acquire-timeout` — a wait exceeded its timeout. The report
|
|
20
|
+
* carries the holder identity available on the tier at that moment: the
|
|
21
|
+
* driver-native tiers attribute the holder connection (MariaDB
|
|
22
|
+
* `IS_USED_LOCK`, PostgreSQL `pg_locks` joined to `pg_stat_activity`, SQL
|
|
23
|
+
* Server `sys.dm_tran_locks` request session, SQLite Node file tier
|
|
24
|
+
* `owner.json` pid/hostname); the process-local tier reports the owning
|
|
25
|
+
* driver's `holderId`.
|
|
26
|
+
* - `advisory-lock-slow-hold` — a hold lasted longer than `slowHoldMs`
|
|
27
|
+
* (default 60000), emitted at release.
|
|
28
|
+
*
|
|
29
|
+
* Reports are emitted on `configuration.getErrorEvents()` as `framework-error`
|
|
30
|
+
* (plus the matching `all-error`) with `{context: "advisory-lock-telemetry",
|
|
31
|
+
* event, lockName, backendTier, ...}` payloads, matching the repo's "never
|
|
32
|
+
* swallow" rule so process-level bug reporters capture them.
|
|
33
|
+
*
|
|
34
|
+
* Holder lookups are strictly best-effort *after* the acquisition already
|
|
35
|
+
* settled: a failed lookup never changes the acquisition outcome and never
|
|
36
|
+
* throws to the caller.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* One advisory-lock telemetry report.
|
|
41
|
+
* @typedef {object} AdvisoryLockTelemetryReport
|
|
42
|
+
* @property {"advisory-lock-slow-acquire" | "advisory-lock-acquire-timeout" | "advisory-lock-slow-hold"} event - Which event fired.
|
|
43
|
+
* @property {string} lockName - The contended advisory-lock name.
|
|
44
|
+
* @property {string} backendTier - Tier that served (or refused) the acquisition: `driver-native`, `process-local`, or `file`.
|
|
45
|
+
* @property {number} durationMs - Measured duration of the acquire wait (or the hold).
|
|
46
|
+
* @property {"acquired" | "timeout" | null} outcome - For acquire events: whether the wait eventually acquired the lock. Null for hold reports.
|
|
47
|
+
* @property {number | null} timeoutMs - The effective acquire bound for acquire events. Null for hold reports or an unbounded wait.
|
|
48
|
+
* @property {string | null} holderId - Process-local holder attribution when the tier can name its local holder.
|
|
49
|
+
* @property {string | null} holderConnectionId - Driver-native holder attribution captured at the event moment (MariaDB `IS_USED_LOCK` connection id, PostgreSQL pid, SQL Server session id, SQLite file `pid@hostname`).
|
|
50
|
+
* @property {string | null} holderLookupError - Set when the best-effort holder lookup itself failed; the lookup never affects the acquisition.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolved per-database telemetry thresholds.
|
|
55
|
+
* @typedef {object} AdvisoryLockTelemetryThresholds
|
|
56
|
+
* @property {number | null} slowAcquireMs - Report an acquire wait slower than this; `null` disables slow-acquire reports.
|
|
57
|
+
* @property {number | null} slowHoldMs - Report a hold longer than this at release; `null` disables hold reports.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/** Framework default: report an acquire wait that completed slower than this. */
|
|
61
|
+
export const DEFAULT_ADVISORY_LOCK_SLOW_ACQUIRE_MS = 5000
|
|
62
|
+
|
|
63
|
+
/** Framework default: report a hold that outlives this at release. */
|
|
64
|
+
export const DEFAULT_ADVISORY_LOCK_SLOW_HOLD_MS = 60 * 1000
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the telemetry thresholds for one database configuration.
|
|
68
|
+
* An explicit per-database `advisoryLockTelemetry` value wins; `null` opts out
|
|
69
|
+
* of that report; only when neither is configured does the framework default
|
|
70
|
+
* apply.
|
|
71
|
+
* @param {import("../configuration-types.js").DatabaseConfigurationType} args - Database configuration args.
|
|
72
|
+
* @returns {AdvisoryLockTelemetryThresholds} - Resolved thresholds.
|
|
73
|
+
*/
|
|
74
|
+
export function advisoryLockTelemetryThresholds(args) {
|
|
75
|
+
const configured = args?.advisoryLockTelemetry
|
|
76
|
+
/** @type {AdvisoryLockTelemetryThresholds} */
|
|
77
|
+
const thresholds = {
|
|
78
|
+
slowAcquireMs: DEFAULT_ADVISORY_LOCK_SLOW_ACQUIRE_MS,
|
|
79
|
+
slowHoldMs: DEFAULT_ADVISORY_LOCK_SLOW_HOLD_MS
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (configured?.slowAcquireMs === null) thresholds.slowAcquireMs = null
|
|
83
|
+
else if (typeof configured?.slowAcquireMs === "number" && configured.slowAcquireMs >= 0) thresholds.slowAcquireMs = configured.slowAcquireMs
|
|
84
|
+
|
|
85
|
+
if (configured?.slowHoldMs === null) thresholds.slowHoldMs = null
|
|
86
|
+
else if (typeof configured?.slowHoldMs === "number" && configured.slowHoldMs >= 0) thresholds.slowHoldMs = configured.slowHoldMs
|
|
87
|
+
|
|
88
|
+
return thresholds
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Emit one telemetry report on the framework-error channel.
|
|
93
|
+
*
|
|
94
|
+
* Mirrors the repo's framework-error emission shape (see `configuration.js`
|
|
95
|
+
* `_reportBeaconError`): when no listener is attached to `framework-error` or
|
|
96
|
+
* `all-error`, a one-line summary goes to `stderr` so a convoy is never
|
|
97
|
+
* completely silent. A listener failure can never break lock handling, so
|
|
98
|
+
* emissions are guarded.
|
|
99
|
+
* @param {import("../configuration.js").default} configuration - Configuration instance owning the error channel.
|
|
100
|
+
* @param {AdvisoryLockTelemetryReport} report - The report payload.
|
|
101
|
+
* @returns {void} - No return value.
|
|
102
|
+
*/
|
|
103
|
+
export function emitAdvisoryLockTelemetryReport(configuration, report) {
|
|
104
|
+
/** @type {import("eventemitter3").EventEmitter | undefined} */
|
|
105
|
+
let errorEvents
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
errorEvents = configuration?.getErrorEvents()
|
|
109
|
+
} catch {
|
|
110
|
+
return // No configuration to report through — never break lock handling.
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!errorEvents) return
|
|
114
|
+
|
|
115
|
+
const hasListener = errorEvents.listenerCount("framework-error") > 0
|
|
116
|
+
|| errorEvents.listenerCount("all-error") > 0
|
|
117
|
+
const payload = {context: "advisory-lock-telemetry", ...report}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
errorEvents.emit("framework-error", payload)
|
|
121
|
+
errorEvents.emit("all-error", {...payload, errorType: "framework-error"})
|
|
122
|
+
} catch {
|
|
123
|
+
// A broken listener must never surface as a lock failure.
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!hasListener) {
|
|
127
|
+
console.error(`[velocious advisory-lock-telemetry ${report.event}] lock=${JSON.stringify(report.lockName)} tier=${report.backendTier} durationMs=${report.durationMs}${report.outcome ? ` outcome=${report.outcome}` : ""}${report.holderConnectionId ? ` holderConnectionId=${report.holderConnectionId}` : ""}${report.holderId ? ` holderId=${report.holderId}` : ""} — subscribe via configuration.getErrorEvents().on("framework-error", …) to route these reports to your bug reporter`)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -161,6 +161,7 @@ import { getDatabaseAnnotations } from "../annotations.js"
|
|
|
161
161
|
import { formatDateForDatabase } from "../datetime-storage.js"
|
|
162
162
|
import isDate from "../../utils/is-date.js"
|
|
163
163
|
import {DEFAULT_ADVISORY_LOCK_ACQUIRE_TIMEOUT_MS} from "../advisory-lock-bound.js"
|
|
164
|
+
import {advisoryLockTelemetryThresholds, emitAdvisoryLockTelemetryReport} from "../advisory-lock-telemetry.js"
|
|
164
165
|
import Logger from "../../logger.js"
|
|
165
166
|
import Query from "../query/index.js"
|
|
166
167
|
import QueryAbortedError from "../query-aborted-error.js"
|
|
@@ -328,6 +329,18 @@ export default class VelociousDatabaseDriversBase {
|
|
|
328
329
|
_failedQueryDiagnostics = new WeakMap()
|
|
329
330
|
/** @type {Map<string, number>} */
|
|
330
331
|
_heldAdvisoryLocks = new Map()
|
|
332
|
+
/**
|
|
333
|
+
* When each tracked advisory lock was first acquired, so the final release can
|
|
334
|
+
* report a hold that outlived `advisoryLockTelemetry.slowHoldMs`.
|
|
335
|
+
* @type {Map<string, number>}
|
|
336
|
+
*/
|
|
337
|
+
_advisoryLockAcquiredAtByName = new Map()
|
|
338
|
+
/**
|
|
339
|
+
* Stable per-connection holder identity used by advisory-lock telemetry when
|
|
340
|
+
* the tier can only name local holders.
|
|
341
|
+
* @type {string | undefined}
|
|
342
|
+
*/
|
|
343
|
+
_advisoryLockHolderId = undefined
|
|
331
344
|
/**
|
|
332
345
|
* Exclusive operation lease installed by a single-multi-use pool.
|
|
333
346
|
* @type {import("../operation-lease.js").default | undefined}
|
|
@@ -2933,9 +2946,24 @@ export default class VelociousDatabaseDriversBase {
|
|
|
2933
2946
|
* @returns {Promise<boolean>} - Resolves to true when the lock has been acquired, false if the timeout elapsed.
|
|
2934
2947
|
*/
|
|
2935
2948
|
async acquireAdvisoryLock(name, args = {}) {
|
|
2936
|
-
const
|
|
2949
|
+
const timeoutMs = this.advisoryLockAcquireTimeoutMs(args)
|
|
2950
|
+
const startedAt = Date.now()
|
|
2951
|
+
const acquired = await this._acquireAdvisoryLock(name, {timeoutMs})
|
|
2952
|
+
const durationMs = Date.now() - startedAt
|
|
2937
2953
|
|
|
2938
|
-
if (acquired)
|
|
2954
|
+
if (acquired) {
|
|
2955
|
+
this._trackAdvisoryLock(name)
|
|
2956
|
+
|
|
2957
|
+
const slowAcquireMs = advisoryLockTelemetryThresholds(this.getArgs()).slowAcquireMs
|
|
2958
|
+
|
|
2959
|
+
if (slowAcquireMs !== null && durationMs > slowAcquireMs) {
|
|
2960
|
+
await this._reportAdvisoryLockTelemetry({event: "advisory-lock-slow-acquire", lockName: name, durationMs, outcome: "acquired", timeoutMs})
|
|
2961
|
+
}
|
|
2962
|
+
} else {
|
|
2963
|
+
// The wait exceeded its bound: attribute the lock to the holder that
|
|
2964
|
+
// blocked it, captured at this moment on this tier.
|
|
2965
|
+
await this._reportAdvisoryLockTelemetry({event: "advisory-lock-acquire-timeout", lockName: name, durationMs, outcome: "timeout", timeoutMs, captureHolder: true})
|
|
2966
|
+
}
|
|
2939
2967
|
|
|
2940
2968
|
return acquired
|
|
2941
2969
|
}
|
|
@@ -3008,6 +3036,21 @@ export default class VelociousDatabaseDriversBase {
|
|
|
3008
3036
|
this._heldAdvisoryLocks.delete(name)
|
|
3009
3037
|
}
|
|
3010
3038
|
|
|
3039
|
+
if (!this._heldAdvisoryLocks.has(name)) {
|
|
3040
|
+
const acquiredAt = this._advisoryLockAcquiredAtByName.get(name)
|
|
3041
|
+
|
|
3042
|
+
this._advisoryLockAcquiredAtByName.delete(name)
|
|
3043
|
+
|
|
3044
|
+
if (acquiredAt !== undefined) {
|
|
3045
|
+
const holdDurationMs = Date.now() - acquiredAt
|
|
3046
|
+
const slowHoldMs = advisoryLockTelemetryThresholds(this.getArgs()).slowHoldMs
|
|
3047
|
+
|
|
3048
|
+
if (slowHoldMs !== null && holdDurationMs > slowHoldMs) {
|
|
3049
|
+
await this._reportAdvisoryLockTelemetry({event: "advisory-lock-slow-hold", lockName: name, durationMs: holdDurationMs, outcome: null, timeoutMs: null})
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3011
3054
|
return released
|
|
3012
3055
|
}
|
|
3013
3056
|
|
|
@@ -3051,6 +3094,8 @@ export default class VelociousDatabaseDriversBase {
|
|
|
3051
3094
|
*/
|
|
3052
3095
|
_trackAdvisoryLock(name) {
|
|
3053
3096
|
this._heldAdvisoryLocks.set(name, (this._heldAdvisoryLocks.get(name) || 0) + 1)
|
|
3097
|
+
|
|
3098
|
+
if (!this._advisoryLockAcquiredAtByName.has(name)) this._advisoryLockAcquiredAtByName.set(name, Date.now())
|
|
3054
3099
|
}
|
|
3055
3100
|
|
|
3056
3101
|
/**
|
|
@@ -3079,4 +3124,71 @@ export default class VelociousDatabaseDriversBase {
|
|
|
3079
3124
|
isAdvisoryLockHeld(name) { // eslint-disable-line no-unused-vars
|
|
3080
3125
|
throw new Error(`'isAdvisoryLockHeld' not implemented for ${this.constructor.name}`)
|
|
3081
3126
|
}
|
|
3127
|
+
|
|
3128
|
+
/**
|
|
3129
|
+
* Which advisory-lock backend tier this connection serves (STC-210 chain
|
|
3130
|
+
* naming). The base driver is the driver-native tier; the SQLite drivers
|
|
3131
|
+
* override to `process-local` / `file`.
|
|
3132
|
+
* @returns {string} - Tier name reported on telemetry payloads.
|
|
3133
|
+
*/
|
|
3134
|
+
advisoryLockBackendTier() {
|
|
3135
|
+
return "driver-native"
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
/**
|
|
3139
|
+
* Stable identity of this connection as a lock holder, reported on
|
|
3140
|
+
* telemetry payloads so a slow acquire/hold is attributable to the reporting
|
|
3141
|
+
* connection itself.
|
|
3142
|
+
* @returns {string} - Holder id.
|
|
3143
|
+
*/
|
|
3144
|
+
advisoryLockHolderId() {
|
|
3145
|
+
if (!this._advisoryLockHolderId) this._advisoryLockHolderId = `advisory-lock-holder:${new UUID(4).format()}`
|
|
3146
|
+
|
|
3147
|
+
return this._advisoryLockHolderId
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
/**
|
|
3151
|
+
* Best-effort identity of the connection currently holding the named lock,
|
|
3152
|
+
* captured on the tier that served the acquisition. The base driver has no
|
|
3153
|
+
* portable server-side lookup; MySQL/PostgreSQL/SQL Server/Node-SQLite
|
|
3154
|
+
* override with their tier's holder attribution.
|
|
3155
|
+
* @param {string} name - Lock name.
|
|
3156
|
+
* @returns {Promise<string | null>} - Holder connection identity, or null when the tier cannot attribute it.
|
|
3157
|
+
*/
|
|
3158
|
+
async advisoryLockHolderConnectionId(name) { // eslint-disable-line no-unused-vars
|
|
3159
|
+
return null
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
/**
|
|
3163
|
+
* Emit one event-driven advisory-lock telemetry report (slow acquire,
|
|
3164
|
+
* acquire timeout with holder attribution, slow hold). The holder lookup is
|
|
3165
|
+
* best-effort: a failed lookup is recorded on the report and never affects
|
|
3166
|
+
* the acquisition that already settled.
|
|
3167
|
+
* @param {{event: "advisory-lock-slow-acquire" | "advisory-lock-acquire-timeout" | "advisory-lock-slow-hold", lockName: string, durationMs: number, outcome: "acquired" | "timeout" | null, timeoutMs: number | null, captureHolder?: boolean}} args - Report args.
|
|
3168
|
+
* @returns {Promise<void>} - Resolves after the report is emitted.
|
|
3169
|
+
*/
|
|
3170
|
+
async _reportAdvisoryLockTelemetry(args) {
|
|
3171
|
+
/** @type {import("../advisory-lock-telemetry.js").AdvisoryLockTelemetryReport} */
|
|
3172
|
+
const report = {
|
|
3173
|
+
event: args.event,
|
|
3174
|
+
lockName: args.lockName,
|
|
3175
|
+
backendTier: this.advisoryLockBackendTier(),
|
|
3176
|
+
durationMs: args.durationMs,
|
|
3177
|
+
outcome: args.outcome,
|
|
3178
|
+
timeoutMs: args.timeoutMs,
|
|
3179
|
+
holderId: this.advisoryLockHolderId(),
|
|
3180
|
+
holderConnectionId: null,
|
|
3181
|
+
holderLookupError: null
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
if (args.captureHolder) {
|
|
3185
|
+
try {
|
|
3186
|
+
report.holderConnectionId = await this.advisoryLockHolderConnectionId(args.lockName)
|
|
3187
|
+
} catch (error) {
|
|
3188
|
+
report.holderLookupError = error instanceof Error ? error.message : String(error)
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
emitAdvisoryLockTelemetryReport(this.configuration, report)
|
|
3193
|
+
}
|
|
3082
3194
|
}
|
|
@@ -954,4 +954,20 @@ export default class VelociousDatabaseDriversMssql extends Base{
|
|
|
954
954
|
|
|
955
955
|
return testResult === 0
|
|
956
956
|
}
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* Attributes the current holder of the named application lock to its
|
|
960
|
+
* requesting session from `sys.dm_tran_locks`, captured at the telemetry
|
|
961
|
+
* event moment (best-effort; the caller guards failures).
|
|
962
|
+
* @param {string} name - Lock name.
|
|
963
|
+
* @returns {Promise<string | null>} - SQL Server session id of the holder, or null when no session holds the lock.
|
|
964
|
+
*/
|
|
965
|
+
async advisoryLockHolderConnectionId(name) {
|
|
966
|
+
const rows = await this.query(
|
|
967
|
+
`SELECT TOP 1 request_session_id AS velocious_advisory_lock_holder FROM sys.dm_tran_locks WHERE resource_type = 'APPLICATIONLOCK' AND resource_description = ${this.quote(name)}`
|
|
968
|
+
)
|
|
969
|
+
const holder = rows?.[0]?.velocious_advisory_lock_holder
|
|
970
|
+
|
|
971
|
+
return holder === null || holder === undefined ? null : String(holder)
|
|
972
|
+
}
|
|
957
973
|
}
|
|
@@ -882,6 +882,20 @@ export default class VelociousDatabaseDriversMysql extends Base{
|
|
|
882
882
|
return Number(result) === 1
|
|
883
883
|
}
|
|
884
884
|
|
|
885
|
+
/**
|
|
886
|
+
* Attributes the current holder of the named lock to its physical session
|
|
887
|
+
* connection id via `IS_USED_LOCK(name)`, captured at the telemetry event
|
|
888
|
+
* moment (best-effort; the caller guards failures).
|
|
889
|
+
* @param {string} name - Lock name.
|
|
890
|
+
* @returns {Promise<string | null>} - MariaDB/MySQL connection id of the holder, or null when no session holds the lock.
|
|
891
|
+
*/
|
|
892
|
+
async advisoryLockHolderConnectionId(name) {
|
|
893
|
+
const rows = await this.query(`SELECT IS_USED_LOCK(${this.quote(name)}) AS velocious_advisory_lock_holder`)
|
|
894
|
+
const holder = rows?.[0]?.velocious_advisory_lock_holder
|
|
895
|
+
|
|
896
|
+
return holder === null || holder === undefined ? null : String(holder)
|
|
897
|
+
}
|
|
898
|
+
|
|
885
899
|
/**
|
|
886
900
|
* Runs is advisory lock held.
|
|
887
901
|
* @param {string} name - Lock name.
|
|
@@ -505,6 +505,23 @@ export default class VelociousDatabaseDriversPgsql extends Base{
|
|
|
505
505
|
return result === true || result === "t" || result === 1 || result === "1"
|
|
506
506
|
}
|
|
507
507
|
|
|
508
|
+
/**
|
|
509
|
+
* Attributes the current holder of the named advisory lock to its backend
|
|
510
|
+
* process id from `pg_locks`, captured at the telemetry event moment
|
|
511
|
+
* (best-effort; the caller guards failures).
|
|
512
|
+
* @param {string} name - Lock name.
|
|
513
|
+
* @returns {Promise<string | null>} - PostgreSQL backend pid of the holder, or null when no session holds the lock.
|
|
514
|
+
*/
|
|
515
|
+
async advisoryLockHolderConnectionId(name) {
|
|
516
|
+
const key = this.advisoryLockKey(name)
|
|
517
|
+
const rows = await this.query(
|
|
518
|
+
`SELECT pid FROM pg_locks WHERE locktype = 'advisory' AND ((classid::bigint << 32) | (objid::bigint & 4294967295)) = ${key} LIMIT 1`
|
|
519
|
+
)
|
|
520
|
+
const holder = rows?.[0]?.pid
|
|
521
|
+
|
|
522
|
+
return holder === null || holder === undefined ? null : String(holder)
|
|
523
|
+
}
|
|
524
|
+
|
|
508
525
|
/**
|
|
509
526
|
* Runs is advisory lock held.
|
|
510
527
|
* @param {string} name - Lock name.
|
|
@@ -524,4 +524,24 @@ export default class VelociousDatabaseDriversSqliteBase extends Base {
|
|
|
524
524
|
async isAdvisoryLockHeld(name) {
|
|
525
525
|
return VelociousDatabaseDriversSqliteBase._advisoryLockState.ownersByName.has(name)
|
|
526
526
|
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* The SQLite emulation is the process-local tier of the STC-210 chain.
|
|
530
|
+
* @returns {string} - Tier name reported on telemetry payloads.
|
|
531
|
+
*/
|
|
532
|
+
advisoryLockBackendTier() {
|
|
533
|
+
return "process-local"
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Attribute the named lock to the driver instance that owns it in the
|
|
538
|
+
* shared in-process registry (best-effort; the caller guards failures).
|
|
539
|
+
* @param {string} name - Lock name.
|
|
540
|
+
* @returns {Promise<string | null>} - Owning driver's holder id, or null when no instance holds the lock.
|
|
541
|
+
*/
|
|
542
|
+
async advisoryLockHolderConnectionId(name) {
|
|
543
|
+
const owner = VelociousDatabaseDriversSqliteBase._advisoryLockState.ownersByName.get(name)
|
|
544
|
+
|
|
545
|
+
return owner ? owner.advisoryLockHolderId() : null
|
|
546
|
+
}
|
|
527
547
|
}
|
|
@@ -204,6 +204,39 @@ export default class VelociousDatabaseDriversSqliteNode extends Base {
|
|
|
204
204
|
return await this._isAdvisoryLockFileHeld(name)
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
/**
|
|
208
|
+
* The Node SQLite driver layers the opt-in file tier on top of the
|
|
209
|
+
* process-local pre-filter, so telemetry reports it as `file`.
|
|
210
|
+
* @returns {string} - Tier name reported on telemetry payloads.
|
|
211
|
+
*/
|
|
212
|
+
advisoryLockBackendTier() {
|
|
213
|
+
return "file"
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Attribute the named lock to the file-tier owner recorded in
|
|
218
|
+
* `owner.json` (`pid@hostname`), captured at the telemetry event moment
|
|
219
|
+
* (best-effort; the caller guards failures). Falls back to the
|
|
220
|
+
* process-local owner when the filesystem lock is not the holder.
|
|
221
|
+
* @param {string} name - Lock name.
|
|
222
|
+
* @returns {Promise<string | null>} - File lock owner identity, or null when no process holds the lock.
|
|
223
|
+
*/
|
|
224
|
+
async advisoryLockHolderConnectionId(name) {
|
|
225
|
+
const lockPath = this._advisoryLockPath(name)
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const content = await fs.readFile(path.join(lockPath, "owner.json"), "utf8")
|
|
229
|
+
const owner = JSON.parse(content)
|
|
230
|
+
|
|
231
|
+
if (owner && typeof owner.pid === "number" && typeof owner.hostname === "string") return `${owner.pid}@${owner.hostname}`
|
|
232
|
+
} catch {
|
|
233
|
+
// No file lock directory or unreadable metadata: fall through to the
|
|
234
|
+
// in-process owner below.
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return await super.advisoryLockHolderConnectionId(name)
|
|
238
|
+
}
|
|
239
|
+
|
|
207
240
|
/**
|
|
208
241
|
* Runs resolve advisory lock directory.
|
|
209
242
|
* @returns {string} - The advisory-lock directory.
|
|
@@ -3033,6 +3033,34 @@ class VelociousDatabaseRecord {
|
|
|
3033
3033
|
return await runner.withAdvisoryLockOrFail(name, callback, args)
|
|
3034
3034
|
}
|
|
3035
3035
|
|
|
3036
|
+
/**
|
|
3037
|
+
* Runs `callback` if the named advisory lock can be acquired immediately;
|
|
3038
|
+
* otherwise resolves with a `{acquired: false, reason: "contended"}` defer
|
|
3039
|
+
* result instead of throwing. This is the non-throwing counterpart of
|
|
3040
|
+
* `withAdvisoryLockOrFail` for hot paths: contention is a normal outcome,
|
|
3041
|
+
* so callers reschedule via their own event-driven retry machinery with zero
|
|
3042
|
+
* exception traffic and zero pinned connections (the probe is a single
|
|
3043
|
+
* immediate try-acquire on the tier's native try primitive). The result form
|
|
3044
|
+
* is identical across tiers.
|
|
3045
|
+
* @template T
|
|
3046
|
+
* @param {string} name - Lock name.
|
|
3047
|
+
* @param {() => Promise<T>} callback - Callback to invoke while the lock is held.
|
|
3048
|
+
* @param {{holdTimeoutMs?: number | null, dedicatedConnection?: boolean}} [args] - `holdTimeoutMs` caps how long the callback may hold the lock; `dedicatedConnection` spawns a separate lock session without enabling a hold timeout.
|
|
3049
|
+
* @returns {Promise<{acquired: true, result: T} | {acquired: false, reason: "contended", lockName: string}>} - Either the held-callback result or the defer outcome.
|
|
3050
|
+
* @throws {AdvisoryLockHoldTimeoutError} - If `holdTimeoutMs` elapses while the callback holds the lock.
|
|
3051
|
+
*/
|
|
3052
|
+
static async withAdvisoryLockOrDefer(name, callback, args = {}) {
|
|
3053
|
+
await this.ensureInitialized()
|
|
3054
|
+
|
|
3055
|
+
const runner = new AdvisoryLockRunner({
|
|
3056
|
+
configuration: this._getConfiguration(),
|
|
3057
|
+
connectionProvider: () => this.connection(),
|
|
3058
|
+
databaseIdentifier: this.getDatabaseIdentifier()
|
|
3059
|
+
})
|
|
3060
|
+
|
|
3061
|
+
return await runner.withAdvisoryLockOrDefer(name, callback, args)
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3036
3064
|
/**
|
|
3037
3065
|
* Runs `callback`, rejecting with `AdvisoryLockHoldTimeoutError` if it has
|
|
3038
3066
|
* not settled within `holdTimeoutMs`. The callback is not cancelled — this is
|