velocious 1.0.698 → 1.0.700
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 +2 -2
- package/build/configuration-types.js +23 -0
- package/build/configuration.js +136 -1
- package/build/database/advisory-lock-backend-policy.js +204 -0
- package/build/database/advisory-lock-file-backend.js +348 -0
- package/build/database/advisory-lock-runner.js +239 -16
- package/build/database/drivers/base.js +318 -15
- package/build/database/drivers/mssql/index.js +1 -1
- package/build/database/drivers/mysql/index.js +4 -2
- package/build/database/drivers/pgsql/index.js +4 -2
- package/build/database/drivers/sqlite/base.js +13 -129
- package/build/database/drivers/sqlite/index.js +41 -62
- package/build/database/migrator.js +120 -50
- package/build/database/record/attachments/store.js +84 -1
- package/build/environment-handlers/base.js +13 -0
- package/build/environment-handlers/node.js +11 -0
- package/build/http-server/server-lock.js +21 -115
- package/build/src/configuration-types.d.ts +68 -0
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +21 -1
- package/build/src/configuration.d.ts +100 -1
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +124 -2
- package/build/src/database/advisory-lock-backend-policy.d.ts +145 -0
- package/build/src/database/advisory-lock-backend-policy.d.ts.map +1 -0
- package/build/src/database/advisory-lock-backend-policy.js +184 -0
- package/build/src/database/advisory-lock-file-backend.d.ts +194 -0
- package/build/src/database/advisory-lock-file-backend.d.ts.map +1 -0
- package/build/src/database/advisory-lock-file-backend.js +313 -0
- package/build/src/database/advisory-lock-runner.d.ts +116 -15
- package/build/src/database/advisory-lock-runner.d.ts.map +1 -1
- package/build/src/database/advisory-lock-runner.js +190 -17
- package/build/src/database/drivers/base.d.ts +125 -4
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +282 -17
- package/build/src/database/drivers/mssql/index.d.ts +1 -1
- package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mssql/index.js +2 -2
- package/build/src/database/drivers/mysql/index.d.ts +4 -2
- package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mysql/index.js +5 -3
- package/build/src/database/drivers/pgsql/index.d.ts +4 -2
- package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
- package/build/src/database/drivers/pgsql/index.js +5 -3
- package/build/src/database/drivers/sqlite/base.d.ts +11 -49
- package/build/src/database/drivers/sqlite/base.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/base.js +14 -114
- package/build/src/database/drivers/sqlite/index.d.ts +28 -19
- package/build/src/database/drivers/sqlite/index.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/index.js +41 -59
- package/build/src/database/migrator.d.ts.map +1 -1
- package/build/src/database/migrator.js +110 -56
- package/build/src/database/record/attachments/store.d.ts +0 -3
- package/build/src/database/record/attachments/store.d.ts.map +1 -1
- package/build/src/database/record/attachments/store.js +71 -2
- package/build/src/environment-handlers/base.d.ts +14 -0
- package/build/src/environment-handlers/base.d.ts.map +1 -1
- package/build/src/environment-handlers/base.js +13 -1
- package/build/src/environment-handlers/node.d.ts +11 -0
- package/build/src/environment-handlers/node.d.ts.map +1 -1
- package/build/src/environment-handlers/node.js +11 -1
- package/build/src/http-server/server-lock.d.ts +13 -37
- package/build/src/http-server/server-lock.d.ts.map +1 -1
- package/build/src/http-server/server-lock.js +22 -109
- package/build/src/testing/test-runner.d.ts +74 -0
- package/build/src/testing/test-runner.d.ts.map +1 -1
- package/build/src/testing/test-runner.js +140 -1
- package/build/src/testing/test-tenant-database-pool.d.ts +183 -0
- package/build/src/testing/test-tenant-database-pool.d.ts.map +1 -0
- package/build/src/testing/test-tenant-database-pool.js +388 -0
- package/build/src/testing/velocious-attempt-executor.d.ts.map +1 -1
- package/build/src/testing/velocious-attempt-executor.js +43 -1
- package/build/testing/test-runner.js +152 -0
- package/build/testing/test-tenant-database-pool.js +438 -0
- package/build/testing/velocious-attempt-executor.js +44 -0
- package/package.json +1 -1
- package/src/configuration-types.js +23 -0
- package/src/configuration.js +136 -1
- package/src/database/advisory-lock-backend-policy.js +204 -0
- package/src/database/advisory-lock-file-backend.js +348 -0
- package/src/database/advisory-lock-runner.js +239 -16
- package/src/database/drivers/base.js +318 -15
- package/src/database/drivers/mssql/index.js +1 -1
- package/src/database/drivers/mysql/index.js +4 -2
- package/src/database/drivers/pgsql/index.js +4 -2
- package/src/database/drivers/sqlite/base.js +13 -129
- package/src/database/drivers/sqlite/index.js +41 -62
- package/src/database/migrator.js +120 -50
- package/src/database/record/attachments/store.js +84 -1
- package/src/environment-handlers/base.js +13 -0
- package/src/environment-handlers/node.js +11 -0
- package/src/http-server/server-lock.js +21 -115
- package/src/testing/test-runner.js +152 -0
- package/src/testing/test-tenant-database-pool.js +438 -0
- package/src/testing/velocious-attempt-executor.js +44 -0
|
@@ -293,6 +293,31 @@ function formatElapsedMs(elapsedMs) {
|
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
export default class VelociousDatabaseDriversBase {
|
|
296
|
+
/**
|
|
297
|
+
* Process-wide state for the **process-local advisory-lock pre-filter
|
|
298
|
+
* tier** shared by every driver (SQLite, MySQL/MariaDB, PostgreSQL,
|
|
299
|
+
* MSSQL). It serializes concurrent acquisitions of the same lock name
|
|
300
|
+
* inside this process through a FIFO waiter queue before any caller
|
|
301
|
+
* contacts the driver-native tier, so a contended same-process waiter
|
|
302
|
+
* never parks a pooled database connection in a driver lock state
|
|
303
|
+
* (MariaDB `User lock`, `sp_getapplock` session hold, ...).
|
|
304
|
+
*
|
|
305
|
+
* This tier is a pre-filter, **never the authority**: a grant here only
|
|
306
|
+
* means "you are next in this process." Cross-process/cross-host
|
|
307
|
+
* exclusion is confirmed by the driver-native tier (or, once the
|
|
308
|
+
* STC-210 backend chain lands, the configured cross-host backend)
|
|
309
|
+
* before the acquisition succeeds.
|
|
310
|
+
*
|
|
311
|
+
* `ownersByName` maps each held lock name to the driver instance that
|
|
312
|
+
* acquired it so `releaseAdvisoryLock` can reject releases from drivers
|
|
313
|
+
* that do not own the lock.
|
|
314
|
+
* @type {{ownersByName: Map<string, VelociousDatabaseDriversBase>, waitersByName: Map<string, Array<() => void>>}}
|
|
315
|
+
*/
|
|
316
|
+
static _advisoryLockState = {
|
|
317
|
+
ownersByName: new Map(),
|
|
318
|
+
waitersByName: new Map()
|
|
319
|
+
}
|
|
320
|
+
|
|
296
321
|
/**
|
|
297
322
|
* Id seq.
|
|
298
323
|
* @type {number | undefined} */
|
|
@@ -2940,18 +2965,66 @@ export default class VelociousDatabaseDriversBase {
|
|
|
2940
2965
|
* Advisory locks are connection-scoped and do not interact with row or
|
|
2941
2966
|
* table locks; they are purely cooperative between callers that use the
|
|
2942
2967
|
* same name and let you serialize functionality without blocking readers
|
|
2943
|
-
* or writers that do not participate in the same lock.
|
|
2968
|
+
* or writers that do not participate in the same named lock.
|
|
2969
|
+
*
|
|
2970
|
+
* Acquisition runs as a two-tier chain:
|
|
2971
|
+
* 1. The **process-local pre-filter tier** (this class' FIFO waiter
|
|
2972
|
+
* queue) serializes same-process callers for the lock name without
|
|
2973
|
+
* issuing any driver-native lock query and without parking a
|
|
2974
|
+
* connection in a driver lock state. A queued caller never reaches
|
|
2975
|
+
* tier 2 until the in-process holder releases.
|
|
2976
|
+
* 2. The **driver-native tier** (`_acquireAdvisoryLock`: MariaDB
|
|
2977
|
+
* `GET_LOCK`, PostgreSQL `pg_advisory_lock`, SQL Server
|
|
2978
|
+
* `sp_getapplock`, SQLite filesystem lock on Node) confirms the
|
|
2979
|
+
* cross-process/cross-host exclusion.
|
|
2980
|
+
*
|
|
2981
|
+
* The process-local tier is a pre-filter, **never the authority**: a
|
|
2982
|
+
* lock name that other processes or hosts can request is only granted
|
|
2983
|
+
* after the driver-native tier (or, once the STC-210 backend chain
|
|
2984
|
+
* lands, the configured cross-host backend) confirms it. If the native
|
|
2985
|
+
* tier denies or times out, the in-process hold is rolled back.
|
|
2944
2986
|
* @param {string} name - Lock name.
|
|
2945
|
-
* @param {{timeoutMs?: number | null}} [args] - Optional timeout in milliseconds; `null` blocks forever, and an omitted value uses the database's `advisoryLockAcquireTimeoutMs` or the framework default.
|
|
2987
|
+
* @param {{timeoutMs?: number | null}} [args] - Optional timeout in milliseconds; `null` blocks forever, and an omitted value uses the database's `advisoryLockAcquireTimeoutMs` or the framework default. The bound covers the whole two-tier acquisition.
|
|
2946
2988
|
* @returns {Promise<boolean>} - Resolves to true when the lock has been acquired, false if the timeout elapsed.
|
|
2947
2989
|
*/
|
|
2948
2990
|
async acquireAdvisoryLock(name, args = {}) {
|
|
2949
2991
|
const timeoutMs = this.advisoryLockAcquireTimeoutMs(args)
|
|
2992
|
+
const deadlineMs = typeof timeoutMs === "number" && timeoutMs >= 0 ? Date.now() + timeoutMs : null
|
|
2993
|
+
const localAcquisition = await this._acquireProcessLocalAdvisoryLock(name, {timeoutMs})
|
|
2994
|
+
|
|
2995
|
+
if (!localAcquisition.acquired) return false
|
|
2996
|
+
|
|
2997
|
+
if (!this.supportsDriverNativeAdvisoryLock()) {
|
|
2998
|
+
// No driver-native tier exists for this driver (plain SQLite): the
|
|
2999
|
+
// process-local tier is the whole implementation, and callers must
|
|
3000
|
+
// not read it as a cross-process guarantee.
|
|
3001
|
+
this._trackAdvisoryLock(name)
|
|
3002
|
+
|
|
3003
|
+
return true
|
|
3004
|
+
}
|
|
3005
|
+
|
|
3006
|
+
// Only a caller that actually queued in the process-local tier spends
|
|
3007
|
+
// part of the acquire budget; an uncontended acquisition passes the
|
|
3008
|
+
// full bound on to the driver-native tier.
|
|
3009
|
+
const nativeTimeoutMs = localAcquisition.waited && deadlineMs !== null
|
|
3010
|
+
? Math.max(0, deadlineMs - Date.now())
|
|
3011
|
+
: timeoutMs
|
|
3012
|
+
|
|
2950
3013
|
const startedAt = Date.now()
|
|
2951
|
-
|
|
3014
|
+
|
|
3015
|
+
/** @type {boolean} */
|
|
3016
|
+
let nativeAcquired
|
|
3017
|
+
|
|
3018
|
+
try {
|
|
3019
|
+
nativeAcquired = await this._acquireAdvisoryLock(name, {timeoutMs: nativeTimeoutMs})
|
|
3020
|
+
} catch (error) {
|
|
3021
|
+
this._releaseProcessLocalAdvisoryLock(name)
|
|
3022
|
+
throw error
|
|
3023
|
+
}
|
|
3024
|
+
|
|
2952
3025
|
const durationMs = Date.now() - startedAt
|
|
2953
3026
|
|
|
2954
|
-
if (
|
|
3027
|
+
if (nativeAcquired) {
|
|
2955
3028
|
this._trackAdvisoryLock(name)
|
|
2956
3029
|
|
|
2957
3030
|
const slowAcquireMs = advisoryLockTelemetryThresholds(this.getArgs()).slowAcquireMs
|
|
@@ -2959,13 +3032,17 @@ export default class VelociousDatabaseDriversBase {
|
|
|
2959
3032
|
if (slowAcquireMs !== null && durationMs > slowAcquireMs) {
|
|
2960
3033
|
await this._reportAdvisoryLockTelemetry({event: "advisory-lock-slow-acquire", lockName: name, durationMs, outcome: "acquired", timeoutMs})
|
|
2961
3034
|
}
|
|
2962
|
-
|
|
2963
|
-
|
|
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})
|
|
3035
|
+
|
|
3036
|
+
return true
|
|
2966
3037
|
}
|
|
2967
3038
|
|
|
2968
|
-
|
|
3039
|
+
this._releaseProcessLocalAdvisoryLock(name)
|
|
3040
|
+
|
|
3041
|
+
// The wait exceeded its bound: attribute the lock to the holder that
|
|
3042
|
+
// blocked it, captured at this moment on this tier.
|
|
3043
|
+
await this._reportAdvisoryLockTelemetry({event: "advisory-lock-acquire-timeout", lockName: name, durationMs, outcome: "timeout", timeoutMs, captureHolder: true})
|
|
3044
|
+
|
|
3045
|
+
return false
|
|
2969
3046
|
}
|
|
2970
3047
|
|
|
2971
3048
|
/**
|
|
@@ -2988,8 +3065,146 @@ export default class VelociousDatabaseDriversBase {
|
|
|
2988
3065
|
return DEFAULT_ADVISORY_LOCK_ACQUIRE_TIMEOUT_MS
|
|
2989
3066
|
}
|
|
2990
3067
|
|
|
3068
|
+
/**
|
|
3069
|
+
* Whether this driver has a driver-native advisory-lock tier behind the
|
|
3070
|
+
* process-local pre-filter. MariaDB (`GET_LOCK`), PostgreSQL
|
|
3071
|
+
* (`pg_advisory_lock`) and SQL Server (`sp_getapplock`) do; SQLite has
|
|
3072
|
+
* no native primitive (the Node SQLite driver substitutes its
|
|
3073
|
+
* filesystem lock directory, the web/native builds have nothing), so
|
|
3074
|
+
* for plain SQLite the process-local tier IS the whole implementation
|
|
3075
|
+
* and callers must not read it as a cross-process guarantee.
|
|
3076
|
+
* @returns {boolean} - Whether the driver-native tier exists.
|
|
3077
|
+
*/
|
|
3078
|
+
supportsDriverNativeAdvisoryLock() {
|
|
3079
|
+
return true
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
/**
|
|
3083
|
+
* Whether the driver's native tier is re-entrant within the same
|
|
3084
|
+
* session. `GET_LOCK`, `pg_advisory_lock` and `sp_getapplock` grant the
|
|
3085
|
+
* same session repeatedly, so the process-local tier must let a
|
|
3086
|
+
* re-acquisition by its own owner pass through to the native tier.
|
|
3087
|
+
* SQLite's emulation (including the Node filesystem lock) is not
|
|
3088
|
+
* re-entrant, so the tier keeps treating a re-acquisition as contention
|
|
3089
|
+
* exactly as the SQLite emulation always has.
|
|
3090
|
+
* @returns {boolean} - Whether same-instance re-acquisition is honored.
|
|
3091
|
+
*/
|
|
3092
|
+
supportsReentrantAdvisoryLock() {
|
|
3093
|
+
return true
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
/**
|
|
3097
|
+
* Process-local pre-filter tier: FIFO waiter queue for one lock name
|
|
3098
|
+
* inside this process. This is the promoted SQLite emulation from
|
|
3099
|
+
* STC-211 — it now runs in front of **every** driver's native tier so a
|
|
3100
|
+
* same-process waiter never issues a driver-native lock query and never
|
|
3101
|
+
* parks a pooled connection in a driver lock state while queued.
|
|
3102
|
+
*
|
|
3103
|
+
* A grant here means only "you are the next caller in this process."
|
|
3104
|
+
* It is never the authority for cross-process/cross-host exclusion; the
|
|
3105
|
+
* caller must still pass the driver-native tier before the lock counts
|
|
3106
|
+
* as held. A re-entrant acquisition by the owning driver instance is
|
|
3107
|
+
* granted immediately, mirroring the session-scoped re-entrancy of
|
|
3108
|
+
* `GET_LOCK` / `pg_advisory_lock` / `sp_getapplock`.
|
|
3109
|
+
* @param {string} name - Lock name.
|
|
3110
|
+
* @param {{timeoutMs?: number | null}} [args] - Optional timeout in milliseconds; `null`, `undefined`, or negative blocks forever.
|
|
3111
|
+
* @returns {Promise<{acquired: boolean, waited: boolean}>} - Whether the tier granted this caller, and whether it had to queue for it.
|
|
3112
|
+
*/
|
|
3113
|
+
async _acquireProcessLocalAdvisoryLock(name, {timeoutMs} = {}) {
|
|
3114
|
+
const state = VelociousDatabaseDriversBase._advisoryLockState
|
|
3115
|
+
let waited = false
|
|
3116
|
+
|
|
3117
|
+
while (state.ownersByName.has(name)) {
|
|
3118
|
+
if (state.ownersByName.get(name) === this && this.supportsReentrantAdvisoryLock()) break
|
|
3119
|
+
|
|
3120
|
+
waited = true
|
|
3121
|
+
|
|
3122
|
+
let remainingMs = null
|
|
3123
|
+
|
|
3124
|
+
if (typeof timeoutMs === "number" && timeoutMs >= 0) {
|
|
3125
|
+
remainingMs = timeoutMs
|
|
3126
|
+
|
|
3127
|
+
if (remainingMs <= 0) return {acquired: false, waited}
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
await new Promise((resolve) => {
|
|
3131
|
+
const waiters = state.waitersByName.get(name) || []
|
|
3132
|
+
/**
|
|
3133
|
+
* Timeout handle.
|
|
3134
|
+
* @type {ReturnType<typeof setTimeout> | null} */
|
|
3135
|
+
let timeoutHandle = null
|
|
3136
|
+
/**
|
|
3137
|
+
* Remove and resolve.
|
|
3138
|
+
* @type {(() => void) | null} */
|
|
3139
|
+
let removeAndResolve = null
|
|
3140
|
+
|
|
3141
|
+
removeAndResolve = () => {
|
|
3142
|
+
if (timeoutHandle) clearTimeout(timeoutHandle)
|
|
3143
|
+
|
|
3144
|
+
const current = state.waitersByName.get(name) || []
|
|
3145
|
+
const index = current.indexOf(/** @type {() => void} */ (removeAndResolve))
|
|
3146
|
+
|
|
3147
|
+
if (index >= 0) current.splice(index, 1)
|
|
3148
|
+
if (current.length === 0) state.waitersByName.delete(name)
|
|
3149
|
+
|
|
3150
|
+
resolve(undefined)
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
waiters.push(removeAndResolve)
|
|
3154
|
+
state.waitersByName.set(name, waiters)
|
|
3155
|
+
|
|
3156
|
+
if (remainingMs !== null) {
|
|
3157
|
+
timeoutHandle = setTimeout(() => {
|
|
3158
|
+
if (removeAndResolve) removeAndResolve()
|
|
3159
|
+
}, remainingMs)
|
|
3160
|
+
}
|
|
3161
|
+
})
|
|
3162
|
+
|
|
3163
|
+
if (typeof timeoutMs === "number" && timeoutMs >= 0 && state.ownersByName.has(name) && !(state.ownersByName.get(name) === this && this.supportsReentrantAdvisoryLock())) {
|
|
3164
|
+
return {acquired: false, waited}
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
state.ownersByName.set(name, this)
|
|
3169
|
+
|
|
3170
|
+
return {acquired: true, waited}
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
/**
|
|
3174
|
+
* Hands the process-local tier for one lock name to the next FIFO waiter,
|
|
3175
|
+
* but only when **this** driver instance currently owns it. A release by
|
|
3176
|
+
* a non-owner is a no-op, matching the "you can only release locks you
|
|
3177
|
+
* own" contract of `RELEASE_LOCK` / `pg_advisory_unlock`.
|
|
3178
|
+
* @param {string} name - Lock name.
|
|
3179
|
+
* @returns {boolean} - Whether this instance owned the tier and released it.
|
|
3180
|
+
*/
|
|
3181
|
+
_releaseProcessLocalAdvisoryLock(name) {
|
|
3182
|
+
const state = VelociousDatabaseDriversBase._advisoryLockState
|
|
3183
|
+
const owner = state.ownersByName.get(name)
|
|
3184
|
+
|
|
3185
|
+
if (owner !== this) return false
|
|
3186
|
+
|
|
3187
|
+
state.ownersByName.delete(name)
|
|
3188
|
+
|
|
3189
|
+
const waiters = state.waitersByName.get(name)
|
|
3190
|
+
|
|
3191
|
+
if (waiters && waiters.length > 0) {
|
|
3192
|
+
const nextWaiter = waiters.shift()
|
|
3193
|
+
|
|
3194
|
+
if (waiters.length === 0) state.waitersByName.delete(name)
|
|
3195
|
+
if (nextWaiter) nextWaiter()
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
return true
|
|
3199
|
+
}
|
|
3200
|
+
|
|
2991
3201
|
/**
|
|
2992
3202
|
* Driver-specific blocking advisory-lock acquisition hook.
|
|
3203
|
+
* This is the **driver-native tier** of the acquisition chain: MariaDB
|
|
3204
|
+
* `GET_LOCK`, PostgreSQL `pg_advisory_lock`, SQL Server `sp_getapplock`,
|
|
3205
|
+
* the Node SQLite filesystem lock. It runs only after the process-local
|
|
3206
|
+
* pre-filter tier has granted this caller its turn, and its result is
|
|
3207
|
+
* what actually confirms cross-process/cross-host exclusion.
|
|
2993
3208
|
* @abstract
|
|
2994
3209
|
* @param {string} name - Lock name.
|
|
2995
3210
|
* @param {{timeoutMs?: number | null}} [_args] - Lock timeout options.
|
|
@@ -3001,15 +3216,55 @@ export default class VelociousDatabaseDriversBase {
|
|
|
3001
3216
|
|
|
3002
3217
|
/**
|
|
3003
3218
|
* Attempts to acquire a named advisory lock without blocking.
|
|
3219
|
+
*
|
|
3220
|
+
* Same two-tier chain as `acquireAdvisoryLock`: the process-local tier
|
|
3221
|
+
* short-circuits contention between callers in this process (a queued
|
|
3222
|
+
* name never reaches the driver-native hook), and the driver-native tier
|
|
3223
|
+
* confirms cross-process/cross-host exclusion. A free in-process tier is
|
|
3224
|
+
* NOT a grant — the native tier still has to agree.
|
|
3004
3225
|
* @param {string} name - Lock name.
|
|
3005
3226
|
* @returns {Promise<boolean>} - Resolves to true if the lock was acquired, false if it was already held.
|
|
3006
3227
|
*/
|
|
3007
3228
|
async tryAcquireAdvisoryLock(name) {
|
|
3008
|
-
const
|
|
3229
|
+
const owner = VelociousDatabaseDriversBase._advisoryLockState.ownersByName.get(name)
|
|
3230
|
+
|
|
3231
|
+
if (owner !== undefined && owner !== this) return false
|
|
3232
|
+
|
|
3233
|
+
// Same-instance re-acquisition mirrors the re-entrant semantics of the
|
|
3234
|
+
// session-scoped driver-native locks; a non-re-entrant driver (SQLite)
|
|
3235
|
+
// keeps treating it as contention, exactly as the emulation always did.
|
|
3236
|
+
const reentrant = owner === this
|
|
3009
3237
|
|
|
3010
|
-
if (
|
|
3238
|
+
if (reentrant && !this.supportsReentrantAdvisoryLock()) return false
|
|
3011
3239
|
|
|
3012
|
-
|
|
3240
|
+
if (!reentrant) VelociousDatabaseDriversBase._advisoryLockState.ownersByName.set(name, this)
|
|
3241
|
+
|
|
3242
|
+
if (!this.supportsDriverNativeAdvisoryLock()) {
|
|
3243
|
+
this._trackAdvisoryLock(name)
|
|
3244
|
+
|
|
3245
|
+
return true
|
|
3246
|
+
}
|
|
3247
|
+
|
|
3248
|
+
/** @type {boolean} */
|
|
3249
|
+
let acquired
|
|
3250
|
+
|
|
3251
|
+
try {
|
|
3252
|
+
acquired = await this._tryAcquireAdvisoryLock(name)
|
|
3253
|
+
} catch (error) {
|
|
3254
|
+
if (!reentrant) this._releaseProcessLocalAdvisoryLock(name)
|
|
3255
|
+
|
|
3256
|
+
throw error
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
if (!acquired) {
|
|
3260
|
+
if (!reentrant) this._releaseProcessLocalAdvisoryLock(name)
|
|
3261
|
+
|
|
3262
|
+
return false
|
|
3263
|
+
}
|
|
3264
|
+
|
|
3265
|
+
this._trackAdvisoryLock(name)
|
|
3266
|
+
|
|
3267
|
+
return true
|
|
3013
3268
|
}
|
|
3014
3269
|
|
|
3015
3270
|
/**
|
|
@@ -3024,10 +3279,36 @@ export default class VelociousDatabaseDriversBase {
|
|
|
3024
3279
|
|
|
3025
3280
|
/**
|
|
3026
3281
|
* Releases a named advisory lock previously acquired on this connection.
|
|
3282
|
+
* The driver-native release runs first; the process-local tier is handed
|
|
3283
|
+
* to the next waiter only once this connection's counted registry no
|
|
3284
|
+
* longer holds the name, so re-entrant acquisitions release one level at
|
|
3285
|
+
* a time exactly like the session-scoped driver locks.
|
|
3027
3286
|
* @param {string} name - Lock name.
|
|
3028
3287
|
* @returns {Promise<boolean>} - Resolves to true if the lock was held by this session and has now been released.
|
|
3029
3288
|
*/
|
|
3030
3289
|
async releaseAdvisoryLock(name) {
|
|
3290
|
+
const owner = VelociousDatabaseDriversBase._advisoryLockState.ownersByName.get(name)
|
|
3291
|
+
|
|
3292
|
+
// Another driver instance in this process owns the tier for this name,
|
|
3293
|
+
// so this connection can never be the native holder either — refuse
|
|
3294
|
+
// before issuing a release statement that could disturb the owner.
|
|
3295
|
+
if (owner !== undefined && owner !== this) return false
|
|
3296
|
+
|
|
3297
|
+
if (!this.supportsDriverNativeAdvisoryLock()) {
|
|
3298
|
+
// Plain SQLite: the process-local tier IS the lock, so the first
|
|
3299
|
+
// release by the owner releases it fully, exactly as the SQLite
|
|
3300
|
+
// emulation always has.
|
|
3301
|
+
const released = this._releaseProcessLocalAdvisoryLock(name)
|
|
3302
|
+
|
|
3303
|
+
if (released) {
|
|
3304
|
+
this._untrackAdvisoryLock(name)
|
|
3305
|
+
} else {
|
|
3306
|
+
this._heldAdvisoryLocks.delete(name)
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
return released
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3031
3312
|
const released = await this._releaseAdvisoryLock(name)
|
|
3032
3313
|
|
|
3033
3314
|
if (released) {
|
|
@@ -3049,6 +3330,10 @@ export default class VelociousDatabaseDriversBase {
|
|
|
3049
3330
|
await this._reportAdvisoryLockTelemetry({event: "advisory-lock-slow-hold", lockName: name, durationMs: holdDurationMs, outcome: null, timeoutMs: null})
|
|
3050
3331
|
}
|
|
3051
3332
|
}
|
|
3333
|
+
|
|
3334
|
+
// Last connection-level release: hand the process-local tier to the
|
|
3335
|
+
// next queued caller in this process.
|
|
3336
|
+
this._releaseProcessLocalAdvisoryLock(name)
|
|
3052
3337
|
}
|
|
3053
3338
|
|
|
3054
3339
|
return released
|
|
@@ -3117,12 +3402,30 @@ export default class VelociousDatabaseDriversBase {
|
|
|
3117
3402
|
* Checks whether a named advisory lock is currently held by any session.
|
|
3118
3403
|
* Intended as an introspection helper; callers who need to act on the
|
|
3119
3404
|
* result should prefer `tryAcquireAdvisoryLock` to avoid a TOCTOU race.
|
|
3405
|
+
*
|
|
3406
|
+
* The process-local tier answers first (a same-process holder is a
|
|
3407
|
+
* holder, full stop); otherwise the question goes to the driver-native
|
|
3408
|
+
* tier's introspection (`IS_USED_LOCK` / `pg_locks` / `APPLOCK_TEST`).
|
|
3409
|
+
* @param {string} name - Lock name.
|
|
3410
|
+
* @returns {Promise<boolean>} - Resolves to true if the lock is held by any session.
|
|
3411
|
+
*/
|
|
3412
|
+
async isAdvisoryLockHeld(name) {
|
|
3413
|
+
if (VelociousDatabaseDriversBase._advisoryLockState.ownersByName.has(name)) return true
|
|
3414
|
+
|
|
3415
|
+
if (!this.supportsDriverNativeAdvisoryLock()) return false
|
|
3416
|
+
|
|
3417
|
+
return await this._isAdvisoryLockHeldNative(name)
|
|
3418
|
+
}
|
|
3419
|
+
|
|
3420
|
+
/**
|
|
3421
|
+
* Driver-native advisory-lock introspection hook behind
|
|
3422
|
+
* `isAdvisoryLockHeld`.
|
|
3120
3423
|
* @abstract
|
|
3121
3424
|
* @param {string} name - Lock name.
|
|
3122
|
-
* @returns {Promise<boolean>} - Resolves to true if the
|
|
3425
|
+
* @returns {Promise<boolean>} - Resolves to true if the native tier reports the lock held.
|
|
3123
3426
|
*/
|
|
3124
|
-
|
|
3125
|
-
throw new Error(`'
|
|
3427
|
+
_isAdvisoryLockHeldNative(name) { // eslint-disable-line no-unused-vars
|
|
3428
|
+
throw new Error(`'_isAdvisoryLockHeldNative' not implemented for ${this.constructor.name}`)
|
|
3126
3429
|
}
|
|
3127
3430
|
|
|
3128
3431
|
/**
|
|
@@ -939,7 +939,7 @@ export default class VelociousDatabaseDriversMssql extends Base{
|
|
|
939
939
|
* @param {string} name - Lock name.
|
|
940
940
|
* @returns {Promise<boolean>} - True if any session currently holds the lock.
|
|
941
941
|
*/
|
|
942
|
-
async
|
|
942
|
+
async _isAdvisoryLockHeldNative(name) {
|
|
943
943
|
const rows = await this.query(
|
|
944
944
|
`SELECT ` +
|
|
945
945
|
`APPLOCK_MODE('public', ${this.quote(name)}, 'Session') AS velocious_advisory_self_mode, ` +
|
|
@@ -882,6 +882,7 @@ export default class VelociousDatabaseDriversMysql extends Base{
|
|
|
882
882
|
return Number(result) === 1
|
|
883
883
|
}
|
|
884
884
|
|
|
885
|
+
/**
|
|
885
886
|
/**
|
|
886
887
|
* Attributes the current holder of the named lock to its physical session
|
|
887
888
|
* connection id via `IS_USED_LOCK(name)`, captured at the telemetry event
|
|
@@ -897,11 +898,12 @@ export default class VelociousDatabaseDriversMysql extends Base{
|
|
|
897
898
|
}
|
|
898
899
|
|
|
899
900
|
/**
|
|
900
|
-
*
|
|
901
|
+
* Driver-native view for the base driver's `isAdvisoryLockHeld`, which
|
|
902
|
+
* answers from the process-local tier first.
|
|
901
903
|
* @param {string} name - Lock name.
|
|
902
904
|
* @returns {Promise<boolean>} - True if any session currently holds the lock.
|
|
903
905
|
*/
|
|
904
|
-
async
|
|
906
|
+
async _isAdvisoryLockHeldNative(name) {
|
|
905
907
|
const rows = await this.query(`SELECT IS_USED_LOCK(${this.quote(name)}) AS velocious_advisory_lock_holder`)
|
|
906
908
|
const holder = rows?.[0]?.velocious_advisory_lock_holder
|
|
907
909
|
|
|
@@ -505,6 +505,7 @@ export default class VelociousDatabaseDriversPgsql extends Base{
|
|
|
505
505
|
return result === true || result === "t" || result === 1 || result === "1"
|
|
506
506
|
}
|
|
507
507
|
|
|
508
|
+
/**
|
|
508
509
|
/**
|
|
509
510
|
* Attributes the current holder of the named advisory lock to its backend
|
|
510
511
|
* process id from `pg_locks`, captured at the telemetry event moment
|
|
@@ -523,11 +524,12 @@ export default class VelociousDatabaseDriversPgsql extends Base{
|
|
|
523
524
|
}
|
|
524
525
|
|
|
525
526
|
/**
|
|
526
|
-
*
|
|
527
|
+
* Driver-native view for the base driver's `isAdvisoryLockHeld`, which
|
|
528
|
+
* answers from the process-local tier first.
|
|
527
529
|
* @param {string} name - Lock name.
|
|
528
530
|
* @returns {Promise<boolean>} - True if any session currently holds the lock.
|
|
529
531
|
*/
|
|
530
|
-
async
|
|
532
|
+
async _isAdvisoryLockHeldNative(name) {
|
|
531
533
|
const key = this.advisoryLockKey(name)
|
|
532
534
|
const rows = await this.query(
|
|
533
535
|
`SELECT EXISTS (SELECT 1 FROM pg_locks WHERE locktype = 'advisory' AND ((classid::bigint << 32) | (objid::bigint & 4294967295)) = ${key}) AS velocious_advisory_lock_held`
|
|
@@ -19,21 +19,6 @@ import Upsert from "./sql/upsert.js"
|
|
|
19
19
|
import Update from "./sql/update.js"
|
|
20
20
|
|
|
21
21
|
export default class VelociousDatabaseDriversSqliteBase extends Base {
|
|
22
|
-
/**
|
|
23
|
-
* Process-wide state for the SQLite advisory lock emulation. Shared across
|
|
24
|
-
* every SQLite driver instance (native, web, sql.js) because there is no
|
|
25
|
-
* concept of "connection" to distinguish them at the SQLite level.
|
|
26
|
-
*
|
|
27
|
-
* `ownersByName` maps each held lock name to the driver instance that
|
|
28
|
-
* acquired it so `releaseAdvisoryLock` can reject releases from drivers
|
|
29
|
-
* that do not own the lock.
|
|
30
|
-
* @type {{ownersByName: Map<string, VelociousDatabaseDriversSqliteBase>, waitersByName: Map<string, Array<() => void>>}}
|
|
31
|
-
*/
|
|
32
|
-
static _advisoryLockState = {
|
|
33
|
-
ownersByName: new Map(),
|
|
34
|
-
waitersByName: new Map()
|
|
35
|
-
}
|
|
36
|
-
|
|
37
22
|
/**
|
|
38
23
|
* Version major.
|
|
39
24
|
* @type {number | undefined} */
|
|
@@ -404,125 +389,24 @@ export default class VelociousDatabaseDriversSqliteBase extends Base {
|
|
|
404
389
|
}
|
|
405
390
|
|
|
406
391
|
/**
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
* for cross-process mutual exclusion.
|
|
413
|
-
*
|
|
414
|
-
* The owning driver instance is recorded so that `releaseAdvisoryLock`
|
|
415
|
-
* can refuse to release a lock that was acquired by someone else.
|
|
416
|
-
* @param {string} name - Lock name.
|
|
417
|
-
* @param {{timeoutMs?: number | null}} [args] - Optional timeout in milliseconds; `null`, `undefined`, or negative blocks forever.
|
|
418
|
-
* @returns {Promise<boolean>} - True if the lock was acquired, false if the timeout elapsed.
|
|
419
|
-
*/
|
|
420
|
-
async _acquireAdvisoryLock(name, {timeoutMs} = {}) {
|
|
421
|
-
const state = VelociousDatabaseDriversSqliteBase._advisoryLockState
|
|
422
|
-
|
|
423
|
-
while (state.ownersByName.has(name)) {
|
|
424
|
-
let remainingMs = null
|
|
425
|
-
|
|
426
|
-
if (typeof timeoutMs === "number" && timeoutMs >= 0) {
|
|
427
|
-
remainingMs = timeoutMs
|
|
428
|
-
|
|
429
|
-
if (remainingMs <= 0) return false
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
await new Promise((resolve) => {
|
|
433
|
-
const waiters = state.waitersByName.get(name) || []
|
|
434
|
-
/**
|
|
435
|
-
* Timeout handle.
|
|
436
|
-
* @type {ReturnType<typeof setTimeout> | null} */
|
|
437
|
-
let timeoutHandle = null
|
|
438
|
-
/**
|
|
439
|
-
* Remove and resolve.
|
|
440
|
-
* @type {(() => void) | null} */
|
|
441
|
-
let removeAndResolve = null
|
|
442
|
-
|
|
443
|
-
removeAndResolve = () => {
|
|
444
|
-
if (timeoutHandle) clearTimeout(timeoutHandle)
|
|
445
|
-
|
|
446
|
-
const current = state.waitersByName.get(name) || []
|
|
447
|
-
const index = current.indexOf(/** @type {() => void} */ (removeAndResolve))
|
|
448
|
-
|
|
449
|
-
if (index >= 0) current.splice(index, 1)
|
|
450
|
-
if (current.length === 0) state.waitersByName.delete(name)
|
|
451
|
-
|
|
452
|
-
resolve(undefined)
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
waiters.push(removeAndResolve)
|
|
456
|
-
state.waitersByName.set(name, waiters)
|
|
457
|
-
|
|
458
|
-
if (remainingMs !== null) {
|
|
459
|
-
timeoutHandle = setTimeout(() => {
|
|
460
|
-
if (removeAndResolve) removeAndResolve()
|
|
461
|
-
}, remainingMs)
|
|
462
|
-
}
|
|
463
|
-
})
|
|
464
|
-
|
|
465
|
-
if (typeof timeoutMs === "number" && timeoutMs >= 0 && state.ownersByName.has(name)) {
|
|
466
|
-
return false
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
state.ownersByName.set(name, this)
|
|
471
|
-
|
|
472
|
-
return true
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
* Runs try acquire advisory lock.
|
|
477
|
-
* @param {string} name - Lock name.
|
|
478
|
-
* @returns {Promise<boolean>} - True if the lock was acquired, false if it was already held.
|
|
392
|
+
* SQLite has no native advisory-lock primitive, so the shared
|
|
393
|
+
* process-local pre-filter tier in the base driver is the whole
|
|
394
|
+
* implementation for the web/sql.js and Expo native builds. The Node
|
|
395
|
+
* driver layers its filesystem lock on top as the driver-native tier.
|
|
396
|
+
* @returns {boolean} - Always false for plain SQLite.
|
|
479
397
|
*/
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
if (state.ownersByName.has(name)) return false
|
|
484
|
-
|
|
485
|
-
state.ownersByName.set(name, this)
|
|
486
|
-
|
|
487
|
-
return true
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Releases the lock only if **this** driver instance owns it. Calling
|
|
492
|
-
* release for a lock owned by another driver instance is a no-op that
|
|
493
|
-
* returns `false`, matching the "you can only release locks you own"
|
|
494
|
-
* contract of MySQL's `RELEASE_LOCK` and PostgreSQL's
|
|
495
|
-
* `pg_advisory_unlock`.
|
|
496
|
-
* @param {string} name - Lock name.
|
|
497
|
-
* @returns {Promise<boolean>} - True if the lock was held by this driver and has now been released.
|
|
498
|
-
*/
|
|
499
|
-
async _releaseAdvisoryLock(name) {
|
|
500
|
-
const state = VelociousDatabaseDriversSqliteBase._advisoryLockState
|
|
501
|
-
const owner = state.ownersByName.get(name)
|
|
502
|
-
|
|
503
|
-
if (owner !== this) return false
|
|
504
|
-
|
|
505
|
-
state.ownersByName.delete(name)
|
|
506
|
-
|
|
507
|
-
const waiters = state.waitersByName.get(name)
|
|
508
|
-
|
|
509
|
-
if (waiters && waiters.length > 0) {
|
|
510
|
-
const nextWaiter = waiters.shift()
|
|
511
|
-
|
|
512
|
-
if (waiters.length === 0) state.waitersByName.delete(name)
|
|
513
|
-
if (nextWaiter) nextWaiter()
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
return true
|
|
398
|
+
supportsDriverNativeAdvisoryLock() {
|
|
399
|
+
return false
|
|
517
400
|
}
|
|
518
401
|
|
|
519
402
|
/**
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
*
|
|
403
|
+
* The SQLite emulation is not re-entrant: a re-acquisition by the same
|
|
404
|
+
* driver instance is contention, not a grant, exactly as the in-process
|
|
405
|
+
* queue always behaved.
|
|
406
|
+
* @returns {boolean} - Always false for SQLite.
|
|
523
407
|
*/
|
|
524
|
-
|
|
525
|
-
return
|
|
408
|
+
supportsReentrantAdvisoryLock() {
|
|
409
|
+
return false
|
|
526
410
|
}
|
|
527
411
|
|
|
528
412
|
/**
|