velocious 1.0.632 → 1.0.634
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 +6 -3
- package/build/background-jobs/lifecycle-client.js +3 -3
- package/build/background-jobs/local-dispatcher.js +29 -18
- package/build/configuration.js +38 -1
- package/build/database/drivers/base.js +122 -105
- package/build/database/drivers/sqlite/index.web.js +19 -11
- package/build/database/migrator.js +8 -5
- package/build/database/pool/async-tracked-multi-connection.js +73 -24
- package/build/database/pool/base.js +5 -0
- package/build/database/pool/single-multi-use.js +43 -11
- package/build/database/record/attachments/store.js +35 -26
- package/build/environment-handlers/base.js +122 -9
- package/build/environment-handlers/node.js +15 -9
- package/build/http-server/websocket-events-host.js +10 -8
- package/build/src/background-jobs/lifecycle-client.d.ts +1 -0
- package/build/src/background-jobs/lifecycle-client.d.ts.map +1 -1
- package/build/src/background-jobs/lifecycle-client.js +4 -4
- package/build/src/background-jobs/local-dispatcher.d.ts.map +1 -1
- package/build/src/background-jobs/local-dispatcher.js +31 -20
- package/build/src/configuration.d.ts +19 -0
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +36 -2
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +118 -100
- package/build/src/database/drivers/sqlite/index.web.d.ts +2 -0
- package/build/src/database/drivers/sqlite/index.web.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/index.web.js +17 -11
- package/build/src/database/migrator.d.ts.map +1 -1
- package/build/src/database/migrator.js +9 -6
- package/build/src/database/pool/async-tracked-multi-connection.d.ts +15 -0
- package/build/src/database/pool/async-tracked-multi-connection.d.ts.map +1 -1
- package/build/src/database/pool/async-tracked-multi-connection.js +71 -21
- package/build/src/database/pool/base.d.ts +2 -0
- package/build/src/database/pool/base.d.ts.map +1 -1
- package/build/src/database/pool/base.js +5 -1
- package/build/src/database/pool/single-multi-use.d.ts.map +1 -1
- package/build/src/database/pool/single-multi-use.js +47 -13
- package/build/src/database/record/attachments/store.d.ts +6 -0
- package/build/src/database/record/attachments/store.d.ts.map +1 -1
- package/build/src/database/record/attachments/store.js +31 -23
- package/build/src/environment-handlers/base.d.ts +79 -8
- package/build/src/environment-handlers/base.d.ts.map +1 -1
- package/build/src/environment-handlers/base.js +106 -10
- package/build/src/environment-handlers/node.d.ts +3 -6
- package/build/src/environment-handlers/node.d.ts.map +1 -1
- package/build/src/environment-handlers/node.js +13 -10
- package/build/src/http-server/websocket-events-host.d.ts.map +1 -1
- package/build/src/http-server/websocket-events-host.js +11 -8
- package/build/src/testing/sqljs-test-database.d.ts +41 -0
- package/build/src/testing/sqljs-test-database.d.ts.map +1 -0
- package/build/src/testing/sqljs-test-database.js +72 -0
- package/build/src/testing/test-runner.d.ts +79 -6
- package/build/src/testing/test-runner.d.ts.map +1 -1
- package/build/src/testing/test-runner.js +498 -139
- package/build/src/testing/test.js +2 -2
- package/build/testing/sqljs-test-database.js +73 -0
- package/build/testing/test-runner.js +529 -141
- package/build/testing/test.js +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/scripts/test-browser.js +16 -29
- package/src/background-jobs/lifecycle-client.js +3 -3
- package/src/background-jobs/local-dispatcher.js +29 -18
- package/src/configuration.js +38 -1
- package/src/database/drivers/base.js +122 -105
- package/src/database/drivers/sqlite/index.web.js +19 -11
- package/src/database/migrator.js +8 -5
- package/src/database/pool/async-tracked-multi-connection.js +73 -24
- package/src/database/pool/base.js +5 -0
- package/src/database/pool/single-multi-use.js +43 -11
- package/src/database/record/attachments/store.js +35 -26
- package/src/environment-handlers/base.js +122 -9
- package/src/environment-handlers/node.js +15 -9
- package/src/http-server/websocket-events-host.js +10 -8
- package/src/testing/sqljs-test-database.js +73 -0
- package/src/testing/test-runner.js +529 -141
- package/src/testing/test.js +1 -1
package/README.md
CHANGED
|
@@ -682,8 +682,9 @@ Task.hasOneAttachment("descriptionFile")
|
|
|
682
682
|
Task.hasOneAttachment("archivedPdf", {driver: "s3"})
|
|
683
683
|
```
|
|
684
684
|
|
|
685
|
-
|
|
686
|
-
|
|
685
|
+
`db:migrate` provisions the framework-owned attachment table before runtime
|
|
686
|
+
attachment work begins. See [Backend record attachments](docs/attachments.md)
|
|
687
|
+
for the complete input, storage-driver, lifecycle, and path-security contracts.
|
|
687
688
|
|
|
688
689
|
You can also pass a driver class or instance directly on the attachment:
|
|
689
690
|
|
|
@@ -2179,6 +2180,8 @@ If you are developing on Velocious, you can run the tests with:
|
|
|
2179
2180
|
|
|
2180
2181
|
Tests default to a 60-second timeout. Override per test with `{timeoutSeconds: 5}` or set a suite-wide default via `configureTests({defaultTimeoutSeconds: 30})`.
|
|
2181
2182
|
|
|
2183
|
+
Database-backed tests default `testArgs.databaseCleaning` to transaction rollback. The configured testing hook uses this metadata to cover `beforeEach`, the test body, and `afterEach` hooks on one pinned connection. Use `{databaseCleaning: {transaction: false, truncate: true}}` only for behavior that requires physical root transactions, independent commits, DDL that auto-commits or cannot run inside the wrapper transaction, lock contention, or genuine concurrency. Transaction-disabled non-request tests use ordinary independently owned checkouts instead of a runner-pinned connection. Tests that own their pool lifecycle or use only private databases can disable configured cleaning with `{databaseCleaning: {transaction: false, truncate: false}}`. See [database cleanup guidance](docs/testing-guidelines.md#preferred-strategy).
|
|
2184
|
+
|
|
2182
2185
|
Truncation-based test cleanup batches eligible tables into one request on PostgreSQL,
|
|
2183
2186
|
SQL Server, and SQLite while preserving each driver's existing identity behavior,
|
|
2184
2187
|
foreign-key restoration, stale-schema retry, and SQL.js persistence guarantees.
|
|
@@ -2461,7 +2464,7 @@ npx velocious background-jobs:retire --generation release-20260828.1 --socket /s
|
|
|
2461
2464
|
```
|
|
2462
2465
|
|
|
2463
2466
|
Each lifecycle command sends one request with no retry and has a hard 10000ms
|
|
2464
|
-
deadline; `--timeout-ms` accepts 1 through
|
|
2467
|
+
deadline; `--timeout-ms` accepts 1 through 60000ms. Generation-aware workers,
|
|
2465
2468
|
clients, and reporters require their hello acknowledgement before readiness or
|
|
2466
2469
|
mutation and bound it to 4000ms by default.
|
|
2467
2470
|
|
|
@@ -6,7 +6,7 @@ import timeout from "awaitery/build/timeout.js"
|
|
|
6
6
|
import JsonSocket from "./json-socket.js"
|
|
7
7
|
|
|
8
8
|
const DEFAULT_REQUEST_TIMEOUT_MS = 10000
|
|
9
|
-
const
|
|
9
|
+
export const MAX_LIFECYCLE_REQUEST_TIMEOUT_MS = 60000
|
|
10
10
|
|
|
11
11
|
/** One-request acknowledged lifecycle client. */
|
|
12
12
|
export default class BackgroundJobsLifecycleClient {
|
|
@@ -26,8 +26,8 @@ export default class BackgroundJobsLifecycleClient {
|
|
|
26
26
|
})
|
|
27
27
|
this.generationId = generationConfig.generationId
|
|
28
28
|
this.socketPath = generationConfig.lifecycleSocketPath
|
|
29
|
-
if (!Number.isInteger(requestTimeoutMs) || requestTimeoutMs < 1 || requestTimeoutMs >
|
|
30
|
-
throw new TypeError(`requestTimeoutMs must be an integer between 1 and ${
|
|
29
|
+
if (!Number.isInteger(requestTimeoutMs) || requestTimeoutMs < 1 || requestTimeoutMs > MAX_LIFECYCLE_REQUEST_TIMEOUT_MS) {
|
|
30
|
+
throw new TypeError(`requestTimeoutMs must be an integer between 1 and ${MAX_LIFECYCLE_REQUEST_TIMEOUT_MS}`)
|
|
31
31
|
}
|
|
32
32
|
this.requestTimeoutMs = requestTimeoutMs
|
|
33
33
|
if (!this.generationId) throw new Error("Background jobs lifecycle client requires generationId")
|
|
@@ -102,25 +102,36 @@ export default class LocalBackgroundJobsDispatcher {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
this._wakeQueued = true
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
105
|
+
/** @type {Promise<void>} */
|
|
106
|
+
let drainPromise
|
|
107
|
+
|
|
108
|
+
drainPromise = this.configuration.withoutCurrentTestDatabaseAccessScope(() => {
|
|
109
|
+
return this.configuration.withoutCurrentConnectionContexts(() => {
|
|
110
|
+
return Promise
|
|
111
|
+
.resolve()
|
|
112
|
+
.then(async () => {
|
|
113
|
+
this._wakeQueued = false
|
|
114
|
+
await this._drain()
|
|
115
|
+
})
|
|
116
|
+
.catch((error) => {
|
|
117
|
+
try {
|
|
118
|
+
this._reportFrameworkError({error, stage: "local-background-jobs-drain"})
|
|
119
|
+
} finally {
|
|
120
|
+
this._armRecoveryTimer()
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
.finally(() => {
|
|
124
|
+
if (this._drainPromise === drainPromise) this._drainPromise = null
|
|
125
|
+
|
|
126
|
+
if (this._redrain && this._accepting) {
|
|
127
|
+
this._redrain = false
|
|
128
|
+
this.wake()
|
|
129
|
+
} else {
|
|
130
|
+
this._resolveIdleWaiters()
|
|
131
|
+
}
|
|
132
|
+
})
|
|
123
133
|
})
|
|
134
|
+
})
|
|
124
135
|
|
|
125
136
|
this._drainPromise = drainPromise
|
|
126
137
|
}
|
package/build/configuration.js
CHANGED
|
@@ -3526,6 +3526,32 @@ export default class VelociousConfiguration {
|
|
|
3526
3526
|
return payload
|
|
3527
3527
|
}
|
|
3528
3528
|
|
|
3529
|
+
/**
|
|
3530
|
+
* Runs one test attempt in a revocable database-access context.
|
|
3531
|
+
* @template T
|
|
3532
|
+
* @param {{revoked: boolean}} scope - Attempt-owned access scope.
|
|
3533
|
+
* @param {() => T | Promise<T>} callback - Attempt work.
|
|
3534
|
+
* @returns {T | Promise<T>} - Callback result.
|
|
3535
|
+
*/
|
|
3536
|
+
runWithTestDatabaseAccessScope(scope, callback) {
|
|
3537
|
+
return this.getEnvironmentHandler().runWithTestDatabaseAccessScope(scope, callback)
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
/**
|
|
3541
|
+
* Runs persistent framework work without inheriting a test attempt's revocable database-access scope.
|
|
3542
|
+
* @template T
|
|
3543
|
+
* @param {() => T | Promise<T>} callback - Persistent work to run.
|
|
3544
|
+
* @returns {Promise<T>} - Callback result.
|
|
3545
|
+
*/
|
|
3546
|
+
async withoutCurrentTestDatabaseAccessScope(callback) {
|
|
3547
|
+
return await this.getEnvironmentHandler().runWithCapturedTestDatabaseAccessScope(undefined, callback)
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
/** Throws when a timed-out test attempt tries to start more database work. */
|
|
3551
|
+
assertDatabaseAccessAllowed() {
|
|
3552
|
+
this.getEnvironmentHandler().assertTestDatabaseAccessAllowed()
|
|
3553
|
+
}
|
|
3554
|
+
|
|
3529
3555
|
/**
|
|
3530
3556
|
* Runs with connections.
|
|
3531
3557
|
* @template T
|
|
@@ -3534,6 +3560,7 @@ export default class VelociousConfiguration {
|
|
|
3534
3560
|
* @returns {Promise<T>} - Resolves with the callback result.
|
|
3535
3561
|
*/
|
|
3536
3562
|
async withConnections(optionsOrCallback, callback) {
|
|
3563
|
+
this.assertDatabaseAccessAllowed()
|
|
3537
3564
|
const {
|
|
3538
3565
|
callback: actualWithConnectionsCallback,
|
|
3539
3566
|
databaseIdentifiers,
|
|
@@ -3564,6 +3591,7 @@ export default class VelociousConfiguration {
|
|
|
3564
3591
|
* @returns {Promise<T>} - Resolves with the callback result.
|
|
3565
3592
|
*/
|
|
3566
3593
|
async withTransaction({databaseIdentifier, name = "Configuration.withTransaction", ...restArgs}, callback) {
|
|
3594
|
+
this.assertDatabaseAccessAllowed()
|
|
3567
3595
|
restArgsError(restArgs)
|
|
3568
3596
|
|
|
3569
3597
|
if (!databaseIdentifier) throw new Error("Configuration.withTransaction requires a databaseIdentifier")
|
|
@@ -3577,6 +3605,7 @@ export default class VelociousConfiguration {
|
|
|
3577
3605
|
const pool = this.getDatabasePool(databaseIdentifier)
|
|
3578
3606
|
|
|
3579
3607
|
return await pool.withOperationConnection({name}, async (connection, owner) => {
|
|
3608
|
+
this.assertDatabaseAccessAllowed()
|
|
3580
3609
|
const operation = new DatabaseOperation({
|
|
3581
3610
|
configuration: this,
|
|
3582
3611
|
databaseConfiguration,
|
|
@@ -3588,7 +3617,10 @@ export default class VelociousConfiguration {
|
|
|
3588
3617
|
})
|
|
3589
3618
|
|
|
3590
3619
|
try {
|
|
3591
|
-
return await operation.transaction(async () =>
|
|
3620
|
+
return await operation.transaction(async () => {
|
|
3621
|
+
this.assertDatabaseAccessAllowed()
|
|
3622
|
+
return await callback(operation)
|
|
3623
|
+
})
|
|
3592
3624
|
} finally {
|
|
3593
3625
|
operation.complete()
|
|
3594
3626
|
}
|
|
@@ -3605,6 +3637,7 @@ export default class VelociousConfiguration {
|
|
|
3605
3637
|
* @returns {Promise<T>} - Callback result.
|
|
3606
3638
|
*/
|
|
3607
3639
|
async withDatabaseOperation({databaseConfiguration, databaseIdentifier, name = "Configuration.withDatabaseOperation", schemaGeneration, tenant, ...restArgs}, callback) {
|
|
3640
|
+
this.assertDatabaseAccessAllowed()
|
|
3608
3641
|
restArgsError(restArgs)
|
|
3609
3642
|
|
|
3610
3643
|
if (!databaseIdentifier) throw new Error("Configuration.withDatabaseOperation requires a databaseIdentifier")
|
|
@@ -3615,6 +3648,7 @@ export default class VelociousConfiguration {
|
|
|
3615
3648
|
const configurationReuseKey = pool.getConfigurationReuseKey(databaseConfiguration)
|
|
3616
3649
|
|
|
3617
3650
|
return await pool.withCapturedOperationConnection({databaseConfiguration, name}, async (connection, owner) => {
|
|
3651
|
+
this.assertDatabaseAccessAllowed()
|
|
3618
3652
|
const operation = new DatabaseOperation({
|
|
3619
3653
|
configuration: this,
|
|
3620
3654
|
databaseConfiguration,
|
|
@@ -3644,6 +3678,7 @@ export default class VelociousConfiguration {
|
|
|
3644
3678
|
async withDatabaseIdentifierConnections({callback, dbs, identifiers, name, stackLabel}) {
|
|
3645
3679
|
const stack = Error().stack
|
|
3646
3680
|
const actualCallback = async () => {
|
|
3681
|
+
this.assertDatabaseAccessAllowed()
|
|
3647
3682
|
return await withTrackedStack(stack || stackLabel, async () => {
|
|
3648
3683
|
return await callback(dbs)
|
|
3649
3684
|
})
|
|
@@ -3677,6 +3712,7 @@ export default class VelociousConfiguration {
|
|
|
3677
3712
|
* @returns {Record<string, import("./database/drivers/base.js").default>} A map of database connections with identifier as key
|
|
3678
3713
|
*/
|
|
3679
3714
|
getCurrentConnections(databaseIdentifiers = this.getDatabaseIdentifiers()) {
|
|
3715
|
+
this.assertDatabaseAccessAllowed()
|
|
3680
3716
|
/**
|
|
3681
3717
|
* Dbs.
|
|
3682
3718
|
* @type {{[key: string]: import("./database/drivers/base.js").default}} */
|
|
@@ -3766,6 +3802,7 @@ export default class VelociousConfiguration {
|
|
|
3766
3802
|
* @returns {Promise<T>} - Resolves with the callback result.
|
|
3767
3803
|
*/
|
|
3768
3804
|
async ensureConnections(optionsOrCallback, callback) {
|
|
3805
|
+
this.assertDatabaseAccessAllowed()
|
|
3769
3806
|
const {
|
|
3770
3807
|
callback: actualWithConnectionsCallback,
|
|
3771
3808
|
databaseIdentifiers,
|
|
@@ -1376,63 +1376,65 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1376
1376
|
async transaction(callback, options = {}) {
|
|
1377
1377
|
await this._waitForOperationLease(options.operationOwner)
|
|
1378
1378
|
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
const args = this.getArgs()
|
|
1384
|
-
const maxAttempts = optionalPositiveInteger(args.deadlockMaxRetries, "deadlockMaxRetries") ?? 8
|
|
1385
|
-
const configuredBaseWaitMs = optionalPositiveInteger(args.deadlockBaseWaitMs, "deadlockBaseWaitMs")
|
|
1386
|
-
const deadlockMaxWaitMs = optionalPositiveInteger(args.deadlockMaxWaitMs, "deadlockMaxWaitMs") ?? 1000
|
|
1387
|
-
let attempt = 0
|
|
1379
|
+
return await coordinateSharedTransactionConnection(this, async () => {
|
|
1380
|
+
if (this._transactionsCount > 0) {
|
|
1381
|
+
return await this._runTransactionAttempt(callback, options)
|
|
1382
|
+
}
|
|
1388
1383
|
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
const
|
|
1384
|
+
const args = this.getArgs()
|
|
1385
|
+
const maxAttempts = optionalPositiveInteger(args.deadlockMaxRetries, "deadlockMaxRetries") ?? 8
|
|
1386
|
+
const configuredBaseWaitMs = optionalPositiveInteger(args.deadlockBaseWaitMs, "deadlockBaseWaitMs")
|
|
1387
|
+
const deadlockMaxWaitMs = optionalPositiveInteger(args.deadlockMaxWaitMs, "deadlockMaxWaitMs") ?? 1000
|
|
1388
|
+
let attempt = 0
|
|
1392
1389
|
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
if (error instanceof VelociousDatabaseAfterCommitCallbackError) throw error.callbackError
|
|
1397
|
-
if (!(error instanceof Error)) throw error
|
|
1390
|
+
while (true) {
|
|
1391
|
+
attempt++
|
|
1392
|
+
const attemptStartedAtMs = this._nowMs()
|
|
1398
1393
|
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1394
|
+
try {
|
|
1395
|
+
return await this._runTransactionAttempt(callback, options)
|
|
1396
|
+
} catch (error) {
|
|
1397
|
+
if (error instanceof VelociousDatabaseAfterCommitCallbackError) throw error.callbackError
|
|
1398
|
+
if (!(error instanceof Error)) throw error
|
|
1399
|
+
|
|
1400
|
+
const retryInfo = this.retryableDatabaseError(error)
|
|
1401
|
+
const willRetry = Boolean(retryInfo.deadlock && attempt < maxAttempts && this._transactionsCount == 0)
|
|
1402
|
+
|
|
1403
|
+
if (willRetry) {
|
|
1404
|
+
this._reportDeadlockRetryDiagnostic({
|
|
1405
|
+
attempt,
|
|
1406
|
+
contentionKind: retryInfo.contentionKind || "deadlock",
|
|
1407
|
+
error,
|
|
1408
|
+
maxAttempts,
|
|
1409
|
+
transactionAttemptDurationMs: Math.max(0, this._nowMs() - attemptStartedAtMs),
|
|
1410
|
+
willRetry
|
|
1411
|
+
})
|
|
1412
|
+
|
|
1413
|
+
// An explicitly-configured base wins so the tuning knob is effective even on drivers
|
|
1414
|
+
// whose classifier supplies its own `waitMs` (MySQL/MariaDB return a fixed 50ms for
|
|
1415
|
+
// deadlocks); otherwise honor that classifier hint, then fall back to 50ms.
|
|
1416
|
+
const baseWaitMs = configuredBaseWaitMs ?? (typeof retryInfo.waitMs == "number" && retryInfo.waitMs > 0 ? retryInfo.waitMs : 50)
|
|
1417
|
+
|
|
1418
|
+
// Full-jitter exponential backoff: wait a uniform-random duration in
|
|
1419
|
+
// [0, min(base * 2^(attempt-1), cap)]. The doubling ceiling spreads retries out as
|
|
1420
|
+
// contention persists, and the jitter de-correlates transactions that deadlocked in
|
|
1421
|
+
// lockstep so they stop re-colliding on the same wait (the linear `base * attempt`
|
|
1422
|
+
// this replaces had every victim retry after an identical delay). `attempt` is
|
|
1423
|
+
// 1-based here, so 2^(attempt-1) is 1, 2, 4, ... The cap keeps the tail sub-second.
|
|
1424
|
+
const ceilingWaitMs = Math.min(baseWaitMs * (2 ** (attempt - 1)), deadlockMaxWaitMs)
|
|
1425
|
+
const jitteredWaitMs = Math.floor(Math.random() * (ceilingWaitMs + 1))
|
|
1426
|
+
|
|
1427
|
+
const loggedContentionKind = retryInfo.contentionKind || "transaction contention"
|
|
1428
|
+
|
|
1429
|
+
this.logger.warn(`Retrying transaction after ${loggedContentionKind} (attempt ${attempt}/${maxAttempts})`)
|
|
1430
|
+
await this._waitMs(jitteredWaitMs)
|
|
1431
|
+
continue
|
|
1432
|
+
}
|
|
1411
1433
|
|
|
1412
|
-
|
|
1413
|
-
// whose classifier supplies its own `waitMs` (MySQL/MariaDB return a fixed 50ms for
|
|
1414
|
-
// deadlocks); otherwise honor that classifier hint, then fall back to 50ms.
|
|
1415
|
-
const baseWaitMs = configuredBaseWaitMs ?? (typeof retryInfo.waitMs == "number" && retryInfo.waitMs > 0 ? retryInfo.waitMs : 50)
|
|
1416
|
-
|
|
1417
|
-
// Full-jitter exponential backoff: wait a uniform-random duration in
|
|
1418
|
-
// [0, min(base * 2^(attempt-1), cap)]. The doubling ceiling spreads retries out as
|
|
1419
|
-
// contention persists, and the jitter de-correlates transactions that deadlocked in
|
|
1420
|
-
// lockstep so they stop re-colliding on the same wait (the linear `base * attempt`
|
|
1421
|
-
// this replaces had every victim retry after an identical delay). `attempt` is
|
|
1422
|
-
// 1-based here, so 2^(attempt-1) is 1, 2, 4, ... The cap keeps the tail sub-second.
|
|
1423
|
-
const ceilingWaitMs = Math.min(baseWaitMs * (2 ** (attempt - 1)), deadlockMaxWaitMs)
|
|
1424
|
-
const jitteredWaitMs = Math.floor(Math.random() * (ceilingWaitMs + 1))
|
|
1425
|
-
|
|
1426
|
-
const loggedContentionKind = retryInfo.contentionKind || "transaction contention"
|
|
1427
|
-
|
|
1428
|
-
this.logger.warn(`Retrying transaction after ${loggedContentionKind} (attempt ${attempt}/${maxAttempts})`)
|
|
1429
|
-
await this._waitMs(jitteredWaitMs)
|
|
1430
|
-
continue
|
|
1434
|
+
throw error
|
|
1431
1435
|
}
|
|
1432
|
-
|
|
1433
|
-
throw error
|
|
1434
1436
|
}
|
|
1435
|
-
}
|
|
1437
|
+
}, options.operationOwner)
|
|
1436
1438
|
}
|
|
1437
1439
|
|
|
1438
1440
|
/**
|
|
@@ -1769,34 +1771,36 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1769
1771
|
* @returns {Promise<void>} - Resolves when complete.
|
|
1770
1772
|
*/
|
|
1771
1773
|
async startTransaction(options = {}) {
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
await this._transactionsActionsMutex.sync(async () => {
|
|
1777
|
-
const operationLease = this._operationLease
|
|
1774
|
+
await coordinateSharedTransactionConnection(this, async () => {
|
|
1775
|
+
while (true) {
|
|
1776
|
+
/** @type {import("../operation-lease.js").default | undefined} */
|
|
1777
|
+
let blockingOperationLease
|
|
1778
1778
|
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
return
|
|
1782
|
-
}
|
|
1779
|
+
await this._transactionsActionsMutex.sync(async () => {
|
|
1780
|
+
const operationLease = this._operationLease
|
|
1783
1781
|
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1782
|
+
if (operationLease && options.operationOwner !== operationLease.owner) {
|
|
1783
|
+
blockingOperationLease = operationLease
|
|
1784
|
+
return
|
|
1785
|
+
}
|
|
1788
1786
|
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
this._resolveTransactionCompletion = resolve
|
|
1787
|
+
await this._runProfiledTransactionAction("start", async () => {
|
|
1788
|
+
await this._startTransactionAction(options)
|
|
1792
1789
|
})
|
|
1793
|
-
|
|
1794
|
-
})
|
|
1790
|
+
this._transactionsCount++
|
|
1795
1791
|
|
|
1796
|
-
|
|
1792
|
+
if (this._transactionsCount === 1) {
|
|
1793
|
+
this._transactionCompletionPromise = new Promise((resolve) => {
|
|
1794
|
+
this._resolveTransactionCompletion = resolve
|
|
1795
|
+
})
|
|
1796
|
+
}
|
|
1797
|
+
})
|
|
1797
1798
|
|
|
1798
|
-
|
|
1799
|
-
|
|
1799
|
+
if (!blockingOperationLease) return
|
|
1800
|
+
|
|
1801
|
+
await blockingOperationLease.wait(options.operationOwner)
|
|
1802
|
+
}
|
|
1803
|
+
}, options.operationOwner)
|
|
1800
1804
|
}
|
|
1801
1805
|
|
|
1802
1806
|
/**
|
|
@@ -1814,13 +1818,15 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1814
1818
|
* @returns {Promise<void>} - Resolves when complete.
|
|
1815
1819
|
*/
|
|
1816
1820
|
async commitTransaction(options = {}) {
|
|
1817
|
-
await this
|
|
1818
|
-
await this.
|
|
1819
|
-
await this.
|
|
1821
|
+
await coordinateSharedTransactionConnection(this, async () => {
|
|
1822
|
+
await this._transactionsActionsMutex.sync(async () => {
|
|
1823
|
+
await this._runProfiledTransactionAction("commit", async () => {
|
|
1824
|
+
await this._commitTransactionAction(options)
|
|
1825
|
+
})
|
|
1826
|
+
this._transactionsCount--
|
|
1827
|
+
this._resolveCompletedTransaction()
|
|
1820
1828
|
})
|
|
1821
|
-
|
|
1822
|
-
this._resolveCompletedTransaction()
|
|
1823
|
-
})
|
|
1829
|
+
}, options.operationOwner)
|
|
1824
1830
|
}
|
|
1825
1831
|
|
|
1826
1832
|
/** Resolves the current outer transaction completion when it has finished. */
|
|
@@ -2560,23 +2566,28 @@ export default class VelociousDatabaseDriversBase {
|
|
|
2560
2566
|
* @returns {Promise<void>} - Resolves when complete.
|
|
2561
2567
|
*/
|
|
2562
2568
|
async rollbackTransaction(options = {}) {
|
|
2563
|
-
await this
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
await this.
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2569
|
+
await coordinateSharedTransactionConnection(this, async () => {
|
|
2570
|
+
await this._transactionsActionsMutex.sync(async () => {
|
|
2571
|
+
try {
|
|
2572
|
+
await this._runProfiledTransactionAction("rollback", async () => {
|
|
2573
|
+
await this._rollbackTransactionAction(options)
|
|
2574
|
+
})
|
|
2575
|
+
} finally {
|
|
2576
|
+
// Driver recovery may need to clear a stale physical transaction when
|
|
2577
|
+
// no logical transaction is active. Never let that cleanup underflow
|
|
2578
|
+
// the logical depth and turn the next root transaction into a savepoint.
|
|
2579
|
+
if (this._transactionsCount > 0) this._transactionsCount--
|
|
2580
|
+
this._resolveCompletedTransaction()
|
|
2581
|
+
|
|
2582
|
+
// A rolled-back transaction may have reverted DDL (e.g. a CREATE TABLE
|
|
2583
|
+
// run lazily inside the transaction), so any cached schema metadata is
|
|
2584
|
+
// now stale and must be invalidated. Without this, a later tableExists()
|
|
2585
|
+
// check can report a table that the rollback already removed, so callers
|
|
2586
|
+
// skip recreating it and then fail with "no such table".
|
|
2587
|
+
this.clearSchemaCache()
|
|
2588
|
+
}
|
|
2589
|
+
})
|
|
2590
|
+
}, options.operationOwner)
|
|
2580
2591
|
}
|
|
2581
2592
|
|
|
2582
2593
|
/**
|
|
@@ -2603,9 +2614,11 @@ export default class VelociousDatabaseDriversBase {
|
|
|
2603
2614
|
* @returns {Promise<void>} - Resolves when complete.
|
|
2604
2615
|
*/
|
|
2605
2616
|
async startSavePoint(savePointName, options = {}) {
|
|
2606
|
-
await this
|
|
2607
|
-
await this.
|
|
2608
|
-
|
|
2617
|
+
await coordinateSharedTransactionConnection(this, async () => {
|
|
2618
|
+
await this._transactionsActionsMutex.sync(async () => {
|
|
2619
|
+
await this._startSavePointAction(savePointName, options)
|
|
2620
|
+
})
|
|
2621
|
+
}, options.operationOwner)
|
|
2609
2622
|
}
|
|
2610
2623
|
|
|
2611
2624
|
/**
|
|
@@ -2649,9 +2662,11 @@ export default class VelociousDatabaseDriversBase {
|
|
|
2649
2662
|
* @returns {Promise<void>} - Resolves when complete.
|
|
2650
2663
|
*/
|
|
2651
2664
|
async releaseSavePoint(savePointName, options = {}) {
|
|
2652
|
-
await this
|
|
2653
|
-
await this.
|
|
2654
|
-
|
|
2665
|
+
await coordinateSharedTransactionConnection(this, async () => {
|
|
2666
|
+
await this._transactionsActionsMutex.sync(async () => {
|
|
2667
|
+
await this._releaseSavePointAction(savePointName, options)
|
|
2668
|
+
})
|
|
2669
|
+
}, options.operationOwner)
|
|
2655
2670
|
}
|
|
2656
2671
|
|
|
2657
2672
|
/**
|
|
@@ -2683,9 +2698,11 @@ export default class VelociousDatabaseDriversBase {
|
|
|
2683
2698
|
* @returns {Promise<void>} - Resolves when complete.
|
|
2684
2699
|
*/
|
|
2685
2700
|
async rollbackSavePoint(savePointName, options = {}) {
|
|
2686
|
-
await this
|
|
2687
|
-
await this.
|
|
2688
|
-
|
|
2701
|
+
await coordinateSharedTransactionConnection(this, async () => {
|
|
2702
|
+
await this._transactionsActionsMutex.sync(async () => {
|
|
2703
|
+
await this._rollbackSavePointAction(savePointName, options)
|
|
2704
|
+
})
|
|
2705
|
+
}, options.operationOwner)
|
|
2689
2706
|
}
|
|
2690
2707
|
|
|
2691
2708
|
/**
|
|
@@ -17,6 +17,9 @@ export default class VelociousDatabaseDriversSqliteWeb extends Base {
|
|
|
17
17
|
* @type {ConnectionSqlJs | undefined} */
|
|
18
18
|
_connection = undefined
|
|
19
19
|
|
|
20
|
+
/** @type {SqliteWebConnection | undefined} */
|
|
21
|
+
_externalConnection = undefined
|
|
22
|
+
|
|
20
23
|
/**
|
|
21
24
|
* Runs sql js locate file.
|
|
22
25
|
* @returns {(file: string) => string} - locateFile callback for sql.js.
|
|
@@ -34,18 +37,21 @@ export default class VelociousDatabaseDriversSqliteWeb extends Base {
|
|
|
34
37
|
async connect() {
|
|
35
38
|
this.args = this.getArgs()
|
|
36
39
|
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const persistence = await createSqliteWebPersistence({databaseName: this.databaseName()})
|
|
43
|
-
const SQL = await initSqlJs({locateFile: this.sqlJsLocateFile()})
|
|
44
|
-
const databaseContent = await persistence.load()
|
|
45
|
-
const connectionSqlJs = new ConnectionSqlJs(this, new SQL.Database(databaseContent), persistence)
|
|
40
|
+
if (this.args.getConnection) {
|
|
41
|
+
this._externalConnection = this.args.getConnection()
|
|
42
|
+
return
|
|
43
|
+
}
|
|
46
44
|
|
|
47
|
-
|
|
45
|
+
if (this.args.reset) {
|
|
46
|
+
await deleteSqliteWebPersistences({databaseName: this.databaseName()})
|
|
48
47
|
}
|
|
48
|
+
|
|
49
|
+
const persistence = await createSqliteWebPersistence({databaseName: this.databaseName()})
|
|
50
|
+
const SQL = await initSqlJs({locateFile: this.sqlJsLocateFile()})
|
|
51
|
+
const databaseContent = await persistence.load()
|
|
52
|
+
const connectionSqlJs = new ConnectionSqlJs(this, new SQL.Database(databaseContent), persistence)
|
|
53
|
+
|
|
54
|
+
this._connection = connectionSqlJs
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
async _close() {
|
|
@@ -152,7 +158,9 @@ export default class VelociousDatabaseDriversSqliteWeb extends Base {
|
|
|
152
158
|
*/
|
|
153
159
|
getConnection() {
|
|
154
160
|
if (this.args?.getConnection) {
|
|
155
|
-
|
|
161
|
+
if (!this._externalConnection) throw new Error("SQLite web external connection has not been initialized")
|
|
162
|
+
|
|
163
|
+
return this._externalConnection
|
|
156
164
|
} else {
|
|
157
165
|
if (!this._connection) throw new Error("SQLite web connection has not been initialized")
|
|
158
166
|
return this._connection
|
|
@@ -265,15 +265,18 @@ export default class VelociousDatabaseMigrator {
|
|
|
265
265
|
const environmentHandler = this.configuration.getEnvironmentHandler()
|
|
266
266
|
const dbs = await this.configuration.getCurrentConnections()
|
|
267
267
|
const filteredDbs = Object.fromEntries(
|
|
268
|
-
Object.entries(dbs).filter(([dbIdentifier]) =>
|
|
268
|
+
Object.entries(dbs).filter(([dbIdentifier]) => {
|
|
269
|
+
if (!this.handlesDatabaseIdentifier(dbIdentifier)) return false
|
|
270
|
+
|
|
271
|
+
return Boolean(this.configuration.getDatabaseIdentifier(dbIdentifier).migrations)
|
|
272
|
+
})
|
|
269
273
|
)
|
|
270
274
|
|
|
271
275
|
if (!environmentHandler || Object.keys(filteredDbs).length == 0) return
|
|
272
276
|
|
|
273
|
-
// Ensure
|
|
274
|
-
//
|
|
275
|
-
// framework
|
|
276
|
-
// schema:load of the dumped SQL) produces a complete DB in every environment.
|
|
277
|
+
// Ensure Velocious' own framework schema before the structure dump. The dump is
|
|
278
|
+
// gated to enabled environments, but migration-enabled databases must include
|
|
279
|
+
// framework tables so `db:migrate` and schema:load produce a complete database.
|
|
277
280
|
await environmentHandler.ensureFrameworkSchema({dbs: filteredDbs})
|
|
278
281
|
await environmentHandler.afterMigrations({dbs: filteredDbs})
|
|
279
282
|
}
|