velocious 1.0.603 → 1.0.604
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 +23 -1
- package/build/configuration.js +19 -0
- package/build/database/drivers/base.js +105 -7
- package/build/database/drivers/mysql/index.js +21 -6
- package/build/database/pool/async-tracked-multi-connection.js +136 -28
- package/build/database/pool/base.js +1 -1
- package/build/environment-handlers/base.js +16 -0
- package/build/environment-handlers/node/cli/commands/test.js +220 -84
- package/build/environment-handlers/node.js +40 -1
- package/build/src/configuration.d.ts +9 -0
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +17 -1
- package/build/src/database/drivers/base.d.ts +40 -1
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +97 -8
- package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mysql/index.js +25 -9
- package/build/src/database/pool/async-tracked-multi-connection.d.ts +42 -2
- package/build/src/database/pool/async-tracked-multi-connection.d.ts.map +1 -1
- package/build/src/database/pool/async-tracked-multi-connection.js +119 -30
- package/build/src/database/pool/base.d.ts +11 -1
- package/build/src/database/pool/base.d.ts.map +1 -1
- package/build/src/database/pool/base.js +2 -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 +15 -1
- package/build/src/environment-handlers/node/cli/commands/test.d.ts +22 -0
- package/build/src/environment-handlers/node/cli/commands/test.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/test.js +200 -82
- package/build/src/environment-handlers/node.d.ts +14 -0
- package/build/src/environment-handlers/node.d.ts.map +1 -1
- package/build/src/environment-handlers/node.js +38 -2
- package/build/src/testing/test-filter-parser.d.ts +15 -0
- package/build/src/testing/test-filter-parser.d.ts.map +1 -1
- package/build/src/testing/test-filter-parser.js +57 -6
- package/build/src/testing/test-profile-activity.d.ts +7 -0
- package/build/src/testing/test-profile-activity.d.ts.map +1 -0
- package/build/src/testing/test-profile-activity.js +13 -0
- package/build/src/testing/test-profile-context.d.ts +15 -0
- package/build/src/testing/test-profile-context.d.ts.map +1 -0
- package/build/src/testing/test-profile-context.js +25 -0
- package/build/src/testing/test-profile-output.d.ts +30 -0
- package/build/src/testing/test-profile-output.d.ts.map +1 -0
- package/build/src/testing/test-profile-output.js +204 -0
- package/build/src/testing/test-profiler.d.ts +668 -0
- package/build/src/testing/test-profiler.d.ts.map +1 -0
- package/build/src/testing/test-profiler.js +847 -0
- package/build/src/testing/test-runner.d.ts +85 -2
- package/build/src/testing/test-runner.d.ts.map +1 -1
- package/build/src/testing/test-runner.js +182 -33
- package/build/testing/test-filter-parser.js +57 -5
- package/build/testing/test-profile-activity.js +14 -0
- package/build/testing/test-profile-context.js +28 -0
- package/build/testing/test-profile-output.js +240 -0
- package/build/testing/test-profiler.js +917 -0
- package/build/testing/test-runner.js +196 -33
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/configuration.js +19 -0
- package/src/database/drivers/base.js +105 -7
- package/src/database/drivers/mysql/index.js +21 -6
- package/src/database/pool/async-tracked-multi-connection.js +136 -28
- package/src/database/pool/base.js +1 -1
- package/src/environment-handlers/base.js +16 -0
- package/src/environment-handlers/node/cli/commands/test.js +220 -84
- package/src/environment-handlers/node.js +40 -1
- package/src/testing/test-filter-parser.js +57 -5
- package/src/testing/test-profile-activity.js +14 -0
- package/src/testing/test-profile-context.js +28 -0
- package/src/testing/test-profile-output.js +240 -0
- package/src/testing/test-profiler.js +917 -0
- package/src/testing/test-runner.js +196 -33
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* Gap-less positional lists with automatic reordering via `actsAsList`, including models with numeric, string, or UUID primary keys (see [docs/acts-as-list.md](docs/acts-as-list.md))
|
|
27
27
|
* Rails-style nested-attribute writes on frontend-model `save()` (see [docs/nested-attributes.md](docs/nested-attributes.md))
|
|
28
28
|
* Async-aware test-data factories with inherited traits, graph-first native association autosave, metadata-aware override precedence, callbacks, sequences, linting, and a process-global reload-retention budget that bounds cache-busted re-import memory (see [docs/factories.md](docs/factories.md))
|
|
29
|
+
* Opt-in Benchmark-style test profiling with privacy-safe rich JSON and directly reusable duration-aware shard manifests (see [docs/test-profiling.md](docs/test-profiling.md))
|
|
29
30
|
* Per-row association counts via `.withCount(...)`, including cohort-safe intersected filters, safe batching of structurally identical aggregates, and automatic IN-list chunking for large parent sets, on frontend and backend queries (see [docs/with-count.md](docs/with-count.md))
|
|
30
31
|
* Consumer-defined per-row SQL aggregates/computations via `.queryData(...)`, with compatible projections sharing a roundtrip while preserving declared alias-overwrite order and automatic IN-list chunking for large parent sets, on frontend and backend queries (see [docs/query-data.md](docs/query-data.md))
|
|
31
32
|
* Per-record ability checks via `.abilities(...)` on frontend queries + `record.can(action)` (see [docs/abilities.md](docs/abilities.md))
|
|
@@ -227,6 +228,22 @@ Slowest 10 tests:
|
|
|
227
228
|
|
|
228
229
|
The report is skipped for single-test runs. See [docs/testing-guidelines.md](docs/testing-guidelines.md).
|
|
229
230
|
|
|
231
|
+
Add `--profile` for a compact Benchmark-style phase and pool summary. Use
|
|
232
|
+
`--profile-json <path>` for versioned, privacy-safe detail or
|
|
233
|
+
`--timing-manifest-output <path>` to generate a sorted per-file duration map for
|
|
234
|
+
the existing `--timing-manifest` shard input; either output flag implies
|
|
235
|
+
profiling.
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
npx velocious test --profile-json tmp/test-profile.json \
|
|
239
|
+
--timing-manifest-output tmp/test-timings.json
|
|
240
|
+
npx velocious test --groups=4 --group-number=1 \
|
|
241
|
+
--timing-manifest tmp/test-timings.json
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
See [test profiling](docs/test-profiling.md) for lifecycle accounting, custom
|
|
245
|
+
activity spans, schema, and privacy guarantees.
|
|
246
|
+
|
|
230
247
|
Prefer waiting for a real signal or condition over sleeping a fixed duration. `waitForEvent(emitter, eventName, {timeoutMs, filter})` resolves the instant a matching event fires (a background job finishing, a model update, a websocket message) and rejects on timeout; for polling an arbitrary condition, use awaitery's `waitFor`.
|
|
231
248
|
|
|
232
249
|
```js
|
|
@@ -1769,7 +1786,12 @@ database: {
|
|
|
1769
1786
|
|
|
1770
1787
|
`pool.max` caps live async-tracked connections for that pool and defaults to `10` when omitted. When the cap is reached, new checkouts wait until a matching checked-in connection can be handed over or capacity is freed. Set `pool.max` to `null` only when a process is deliberately allowed to open an unbounded number of database connections. The built-in debug endpoint reports each in-use connection's `checkedOutForMs`, each idle connection's `idleForMs`, and queued `pendingCheckouts[].waitingForMs` so production diagnostics can distinguish long-held checkouts from pool-capacity waits.
|
|
1771
1788
|
|
|
1772
|
-
Debug snapshots also expose cumulative checkout-wait and
|
|
1789
|
+
Debug snapshots also expose cumulative connection-creation, checkout-wait and
|
|
1790
|
+
timeout, idle-reap, and peak-live-connection telemetry. Opt-in test profiles can
|
|
1791
|
+
attribute safe aggregate deltas to their current spans. The [MySQL idle-reaping
|
|
1792
|
+
benchmark and methodology](docs/mysql-pool-idle-reaping-research.md) compare the
|
|
1793
|
+
5-second default with 60 seconds and disabled reaping under a fixed cap; absent
|
|
1794
|
+
representative measured evidence, retain the 5-second default.
|
|
1773
1795
|
|
|
1774
1796
|
# Websockets
|
|
1775
1797
|
|
package/build/configuration.js
CHANGED
|
@@ -35,6 +35,7 @@ import {frontendModelApiManifest, frontendModelResourceClassFromDefinition, fron
|
|
|
35
35
|
import {currentOfflineGrantSigningKey, normalizeOfflineGrantSigningKey} from "./sync/offline-grant.js"
|
|
36
36
|
import PluginRoutes from "./routes/plugin-routes.js"
|
|
37
37
|
import restArgsError from "./utils/rest-args-error.js"
|
|
38
|
+
import { validateTestActivityName } from "./testing/test-profile-activity.js"
|
|
38
39
|
import {validateTimeZone} from "./time-zone.js"
|
|
39
40
|
import {withTrackedStack} from "./utils/with-tracked-stack.js"
|
|
40
41
|
import VelociousPackage from "./packages/velocious-package.js"
|
|
@@ -3150,6 +3151,24 @@ export default class VelociousConfiguration {
|
|
|
3150
3151
|
return await this.getEnvironmentHandler().runWithRequestTiming(requestTiming, callback)
|
|
3151
3152
|
}
|
|
3152
3153
|
|
|
3154
|
+
/**
|
|
3155
|
+
* Profiles an application-defined test activity when an opt-in test profile
|
|
3156
|
+
* context is active. The callback always runs, including outside profiling.
|
|
3157
|
+
* @template T
|
|
3158
|
+
* @param {string} name - Low-cardinality activity identifier.
|
|
3159
|
+
* @param {() => (T | Promise<T>)} callback - Activity callback.
|
|
3160
|
+
* @returns {Promise<T>} - Callback result.
|
|
3161
|
+
*/
|
|
3162
|
+
async profileTestActivity(name, callback) {
|
|
3163
|
+
const validatedName = validateTestActivityName(name)
|
|
3164
|
+
|
|
3165
|
+
const context = this.getEnvironmentHandler().getCurrentTestProfileContext()
|
|
3166
|
+
|
|
3167
|
+
if (!context) return await callback()
|
|
3168
|
+
|
|
3169
|
+
return await context.profiler.profileActivity(context, validatedName, callback)
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3153
3172
|
/**
|
|
3154
3173
|
* Runs run with timezone.
|
|
3155
3174
|
* @param {string} timeZone - IANA timezone identifier.
|
|
@@ -91,6 +91,14 @@
|
|
|
91
91
|
* @property {boolean} willRetry - Whether another outer transaction attempt will run.
|
|
92
92
|
*/
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* TestProfileQueryAttempt type.
|
|
96
|
+
* @typedef {object} TestProfileQueryAttempt
|
|
97
|
+
* @property {import("../../testing/test-profiler.js").TestProfileAsyncContext} context - Captured async attribution.
|
|
98
|
+
* @property {{sqlFingerprint: string, sqlOperation: string}} diagnostic - Redacted statement diagnostic.
|
|
99
|
+
* @property {number} startedAtMs - Physical attempt start time.
|
|
100
|
+
*/
|
|
101
|
+
|
|
94
102
|
/**
|
|
95
103
|
* ActiveQueryDebugSnapshot type.
|
|
96
104
|
* @typedef {object} ActiveQueryDebugSnapshot
|
|
@@ -164,6 +172,7 @@ import TableForeignKey from "../table-data/table-foreign-key.js"
|
|
|
164
172
|
import wait from "awaitery/build/wait.js"
|
|
165
173
|
import { optionalPositiveInteger } from "typanic"
|
|
166
174
|
import { coordinateSharedTransactionConnection } from "../../testing/shared-transaction-connection-coordinator.js"
|
|
175
|
+
import { currentTestProfileContext } from "../../testing/test-profile-context.js"
|
|
167
176
|
import sha256Hex from "../../utils/sha256-hex.js"
|
|
168
177
|
|
|
169
178
|
/** Maximum characters inspected when building the debug SQL preview. */
|
|
@@ -1740,7 +1749,9 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1740
1749
|
return
|
|
1741
1750
|
}
|
|
1742
1751
|
|
|
1743
|
-
await this.
|
|
1752
|
+
await this._runProfiledTransactionAction("start", async () => {
|
|
1753
|
+
await this._startTransactionAction(options)
|
|
1754
|
+
})
|
|
1744
1755
|
this._transactionsCount++
|
|
1745
1756
|
|
|
1746
1757
|
if (this._transactionsCount === 1) {
|
|
@@ -1772,7 +1783,9 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1772
1783
|
*/
|
|
1773
1784
|
async commitTransaction(options = {}) {
|
|
1774
1785
|
await this._transactionsActionsMutex.sync(async () => {
|
|
1775
|
-
await this.
|
|
1786
|
+
await this._runProfiledTransactionAction("commit", async () => {
|
|
1787
|
+
await this._commitTransactionAction(options)
|
|
1788
|
+
})
|
|
1776
1789
|
this._transactionsCount--
|
|
1777
1790
|
this._resolveCompletedTransaction()
|
|
1778
1791
|
})
|
|
@@ -1797,6 +1810,68 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1797
1810
|
await this.query("COMMIT", options)
|
|
1798
1811
|
}
|
|
1799
1812
|
|
|
1813
|
+
/**
|
|
1814
|
+
* Times a physical transaction action only when test profiling is active.
|
|
1815
|
+
* @template T
|
|
1816
|
+
* @param {"start" | "commit" | "rollback"} action - Transaction action.
|
|
1817
|
+
* @param {() => Promise<T>} callback - Physical action callback.
|
|
1818
|
+
* @returns {Promise<T>} - Callback result.
|
|
1819
|
+
*/
|
|
1820
|
+
async _runProfiledTransactionAction(action, callback) {
|
|
1821
|
+
const profileContext = currentTestProfileContext(this.configuration)
|
|
1822
|
+
|
|
1823
|
+
if (!profileContext) return await callback()
|
|
1824
|
+
|
|
1825
|
+
const startedAtMs = nowMs()
|
|
1826
|
+
let failed = true
|
|
1827
|
+
|
|
1828
|
+
try {
|
|
1829
|
+
const result = await callback()
|
|
1830
|
+
|
|
1831
|
+
failed = false
|
|
1832
|
+
return result
|
|
1833
|
+
} finally {
|
|
1834
|
+
profileContext.profiler.recordDatabaseTransaction(profileContext, {
|
|
1835
|
+
action,
|
|
1836
|
+
durationMs: nowMs() - startedAtMs,
|
|
1837
|
+
failed
|
|
1838
|
+
})
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
/**
|
|
1843
|
+
* Starts an optional physical-query profile attempt without retaining SQL.
|
|
1844
|
+
* @param {string} sql - Original SQL used only to derive its redacted diagnostic.
|
|
1845
|
+
* @returns {TestProfileQueryAttempt | undefined} - Active profile handle.
|
|
1846
|
+
*/
|
|
1847
|
+
_startProfiledQueryAttempt(sql) {
|
|
1848
|
+
const context = currentTestProfileContext(this.configuration)
|
|
1849
|
+
|
|
1850
|
+
if (!context) return undefined
|
|
1851
|
+
|
|
1852
|
+
return {
|
|
1853
|
+
context,
|
|
1854
|
+
diagnostic: sqlDiagnostic(sql),
|
|
1855
|
+
startedAtMs: nowMs()
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
/**
|
|
1860
|
+
* Completes an optional physical-query profile attempt.
|
|
1861
|
+
* @param {TestProfileQueryAttempt | undefined} attempt - Profile handle.
|
|
1862
|
+
* @param {boolean} failed - Whether the physical driver call failed.
|
|
1863
|
+
* @returns {void}
|
|
1864
|
+
*/
|
|
1865
|
+
_finishProfiledQueryAttempt(attempt, failed) {
|
|
1866
|
+
if (!attempt) return
|
|
1867
|
+
|
|
1868
|
+
attempt.context.profiler.recordDatabaseQuery(attempt.context, {
|
|
1869
|
+
durationMs: nowMs() - attempt.startedAtMs,
|
|
1870
|
+
failed,
|
|
1871
|
+
...attempt.diagnostic
|
|
1872
|
+
})
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1800
1875
|
/**
|
|
1801
1876
|
* Runs every guard registered to the transaction frame.
|
|
1802
1877
|
* @param {TransactionCallbackFrame} callbackFrame - Frame whose guards are completing.
|
|
@@ -1916,7 +1991,17 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1916
1991
|
await this.beforeQuery(sql, options)
|
|
1917
1992
|
|
|
1918
1993
|
try {
|
|
1919
|
-
|
|
1994
|
+
const profileAttempt = this._startProfiledQueryAttempt(sql)
|
|
1995
|
+
let failed = true
|
|
1996
|
+
|
|
1997
|
+
try {
|
|
1998
|
+
const affectedRows = await this._affectedRowsActual(sql)
|
|
1999
|
+
|
|
2000
|
+
failed = false
|
|
2001
|
+
return affectedRows
|
|
2002
|
+
} finally {
|
|
2003
|
+
this._finishProfiledQueryAttempt(profileAttempt, failed)
|
|
2004
|
+
}
|
|
1920
2005
|
} finally {
|
|
1921
2006
|
await this.afterQuery(sql, options)
|
|
1922
2007
|
}
|
|
@@ -1945,7 +2030,7 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1945
2030
|
let result
|
|
1946
2031
|
|
|
1947
2032
|
try {
|
|
1948
|
-
const runQueryActualWithHooks = async () => await this._queryActualWithHooks(querySql, options)
|
|
2033
|
+
const runQueryActualWithHooks = async () => await this._queryActualWithHooks(querySql, options, originalSql)
|
|
1949
2034
|
|
|
1950
2035
|
if (requestTiming && tries === 1) {
|
|
1951
2036
|
result = await requestTiming.measureDbQuery(runQueryActualWithHooks)
|
|
@@ -1980,14 +2065,25 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1980
2065
|
* Runs query actual with before/after hooks.
|
|
1981
2066
|
* @param {string} sql - SQL string.
|
|
1982
2067
|
* @param {QueryOptions} options - Query options.
|
|
2068
|
+
* @param {string} originalSql - SQL before process-list comments.
|
|
1983
2069
|
* @returns {Promise<QueryResultType>} - Resolves with the query.
|
|
1984
2070
|
*/
|
|
1985
|
-
async _queryActualWithHooks(sql, options) {
|
|
2071
|
+
async _queryActualWithHooks(sql, options, originalSql) {
|
|
1986
2072
|
return await coordinateSharedTransactionConnection(this, async () => {
|
|
1987
2073
|
await this.beforeQuery(sql, options)
|
|
1988
2074
|
|
|
1989
2075
|
try {
|
|
1990
|
-
|
|
2076
|
+
const profileAttempt = this._startProfiledQueryAttempt(originalSql)
|
|
2077
|
+
let failed = true
|
|
2078
|
+
|
|
2079
|
+
try {
|
|
2080
|
+
const result = await this._queryActual(sql, options)
|
|
2081
|
+
|
|
2082
|
+
failed = false
|
|
2083
|
+
return result
|
|
2084
|
+
} finally {
|
|
2085
|
+
this._finishProfiledQueryAttempt(profileAttempt, failed)
|
|
2086
|
+
}
|
|
1991
2087
|
} finally {
|
|
1992
2088
|
await this.afterQuery(sql, options)
|
|
1993
2089
|
}
|
|
@@ -2403,7 +2499,9 @@ export default class VelociousDatabaseDriversBase {
|
|
|
2403
2499
|
async rollbackTransaction(options = {}) {
|
|
2404
2500
|
await this._transactionsActionsMutex.sync(async () => {
|
|
2405
2501
|
try {
|
|
2406
|
-
await this.
|
|
2502
|
+
await this._runProfiledTransactionAction("rollback", async () => {
|
|
2503
|
+
await this._rollbackTransactionAction(options)
|
|
2504
|
+
})
|
|
2407
2505
|
} finally {
|
|
2408
2506
|
this._transactionsCount--
|
|
2409
2507
|
this._resolveCompletedTransaction()
|
|
@@ -518,7 +518,15 @@ export default class VelociousDatabaseDriversMysql extends Base{
|
|
|
518
518
|
if (!this.pool) await this.connect()
|
|
519
519
|
if (!this.pool) throw new Error("MySQL pool failed to initialize")
|
|
520
520
|
|
|
521
|
-
|
|
521
|
+
const profileAttempt = this._startProfiledQueryAttempt(sql)
|
|
522
|
+
let failed = true
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
yield* streamQuery(this.pool, sql)
|
|
526
|
+
failed = false
|
|
527
|
+
} finally {
|
|
528
|
+
this._finishProfiledQueryAttempt(profileAttempt, failed)
|
|
529
|
+
}
|
|
522
530
|
}
|
|
523
531
|
|
|
524
532
|
/**
|
|
@@ -558,13 +566,20 @@ export default class VelociousDatabaseDriversMysql extends Base{
|
|
|
558
566
|
if (!this.pool) throw new Error("MySQL pool failed to initialize")
|
|
559
567
|
|
|
560
568
|
const pool = this.pool
|
|
569
|
+
const profileAttempt = this._startProfiledQueryAttempt(structureSql)
|
|
570
|
+
let failed = true
|
|
561
571
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
572
|
+
try {
|
|
573
|
+
await new Promise((resolve, reject) => {
|
|
574
|
+
pool.query(structureSql, (error) => {
|
|
575
|
+
if (error) reject(error)
|
|
576
|
+
else resolve(undefined)
|
|
577
|
+
})
|
|
566
578
|
})
|
|
567
|
-
|
|
579
|
+
failed = false
|
|
580
|
+
} finally {
|
|
581
|
+
this._finishProfiledQueryAttempt(profileAttempt, failed)
|
|
582
|
+
}
|
|
568
583
|
|
|
569
584
|
return true
|
|
570
585
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import {AsyncLocalStorage} from "async_hooks"
|
|
4
|
-
import BasePool, {POOL_CONFIGURATION_KEY} from "./base.js"
|
|
3
|
+
import { AsyncLocalStorage } from "async_hooks"
|
|
4
|
+
import BasePool, { POOL_CONFIGURATION_KEY } from "./base.js"
|
|
5
|
+
import { currentTestProfileContext } from "../../testing/test-profile-context.js"
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* PendingCheckout type.
|
|
@@ -15,6 +16,7 @@ import BasePool, {POOL_CONFIGURATION_KEY} from "./base.js"
|
|
|
15
16
|
* @property {number | null} timeoutAt - Timestamp when the checkout will time out, or null when disabled.
|
|
16
17
|
* @property {number | null} timeoutMillis - Milliseconds to wait before rejecting, or null when disabled.
|
|
17
18
|
* @property {ReturnType<typeof setTimeout> | undefined} timeoutTimer - Timer that rejects the pending checkout.
|
|
19
|
+
* @property {import("../../testing/test-profiler.js").TestProfileAsyncContext | undefined} [testProfileContext] - Async-safe profile attribution captured at enqueue.
|
|
18
20
|
*/
|
|
19
21
|
export const CLOSED_CONNECTION = Symbol("velociousClosedConnection")
|
|
20
22
|
const IDLE_CONNECTION_CHECKED_IN_AT = Symbol("velociousIdleConnectionCheckedInAt")
|
|
@@ -115,10 +117,20 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
115
117
|
|
|
116
118
|
/** Cumulative low-cardinality pool telemetry. */
|
|
117
119
|
telemetry = {
|
|
120
|
+
connectionCreationCount: 0,
|
|
121
|
+
connectionCreationFailureCount: 0,
|
|
122
|
+
connectionCreationMaxMs: 0,
|
|
123
|
+
connectionCreationTotalMs: 0,
|
|
124
|
+
checkoutTimeoutCount: 0,
|
|
118
125
|
checkoutWaitCount: 0,
|
|
119
126
|
checkoutWaitMaxMs: 0,
|
|
120
127
|
checkoutWaitTotalMs: 0,
|
|
121
|
-
|
|
128
|
+
idleReapCount: 0,
|
|
129
|
+
idleReapDisposalCount: 0,
|
|
130
|
+
idleReapFailureCount: 0,
|
|
131
|
+
idleReapMaxMs: 0,
|
|
132
|
+
idleReapTotalMs: 0,
|
|
133
|
+
peakLiveConnections: 0
|
|
122
134
|
}
|
|
123
135
|
|
|
124
136
|
idSeq = 0
|
|
@@ -139,6 +151,58 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
139
151
|
this._withoutCurrentConnectionContext = withoutCurrentConnectionContext
|
|
140
152
|
}
|
|
141
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Returns the pool telemetry clock.
|
|
156
|
+
* @returns {number} - Current time in milliseconds.
|
|
157
|
+
*/
|
|
158
|
+
nowMs() { return Date.now() }
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Records a pool metric in the active async-safe test profile context.
|
|
162
|
+
* @param {import("../../testing/test-profiler.js").TestProfileAsyncContext | undefined} context - Captured profile context.
|
|
163
|
+
* @param {"connectionCreation" | "checkoutWait" | "checkoutTimeout" | "idleReap" | "idleReapDisposal" | "peakLiveConnections"} metric - Metric name.
|
|
164
|
+
* @param {{durationMs?: number, failed?: boolean, value?: number}} [values] - Aggregate values.
|
|
165
|
+
* @returns {void}
|
|
166
|
+
*/
|
|
167
|
+
recordTestProfilePoolMetric(context, metric, values = {}) {
|
|
168
|
+
if (!context) return
|
|
169
|
+
|
|
170
|
+
context.profiler.recordPoolMetric(context, this.identifier, metric, values)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Spawns and times a physical connection without retaining its configuration.
|
|
175
|
+
* @param {import("../../configuration-types.js").DatabaseConfigurationType} config - Resolved database configuration.
|
|
176
|
+
* @returns {Promise<import("../drivers/base.js").default>} - Connected driver.
|
|
177
|
+
*/
|
|
178
|
+
async spawnConnectionWithConfiguration(config) {
|
|
179
|
+
const startedAt = this.nowMs()
|
|
180
|
+
const profileContext = currentTestProfileContext(this.configuration)
|
|
181
|
+
let failed = true
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const connection = await super.spawnConnectionWithConfiguration(config)
|
|
185
|
+
|
|
186
|
+
failed = false
|
|
187
|
+
const liveConnectionCount = this.liveConnectionCount() - this.connectionsBeingSpawned + 1
|
|
188
|
+
|
|
189
|
+
if (liveConnectionCount > this.telemetry.peakLiveConnections) {
|
|
190
|
+
this.telemetry.peakLiveConnections = liveConnectionCount
|
|
191
|
+
this.recordTestProfilePoolMetric(profileContext, "peakLiveConnections", {value: liveConnectionCount})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return connection
|
|
195
|
+
} finally {
|
|
196
|
+
const durationMs = Math.max(0, this.nowMs() - startedAt)
|
|
197
|
+
|
|
198
|
+
this.telemetry.connectionCreationCount++
|
|
199
|
+
if (failed) this.telemetry.connectionCreationFailureCount++
|
|
200
|
+
this.telemetry.connectionCreationTotalMs += durationMs
|
|
201
|
+
this.telemetry.connectionCreationMaxMs = Math.max(this.telemetry.connectionCreationMaxMs, durationMs)
|
|
202
|
+
this.recordTestProfilePoolMetric(profileContext, "connectionCreation", {durationMs, failed})
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
142
206
|
/**
|
|
143
207
|
* Runs checkin.
|
|
144
208
|
* @param {import("../drivers/base.js").default} connection - Database connection instance.
|
|
@@ -258,7 +322,11 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
258
322
|
// The post-reap configuration is fresh for the current caller, and its reuse key is
|
|
259
323
|
// derived from this exact captured object so the connection cannot open one tenant while
|
|
260
324
|
// being stamped for another. The queued path retains the same captured pair.
|
|
261
|
-
connection = await this.spawnConnectionForCheckout(
|
|
325
|
+
connection = await this.spawnConnectionForCheckout(
|
|
326
|
+
databaseConfig,
|
|
327
|
+
reuseKey,
|
|
328
|
+
currentTestProfileContext(this.configuration)
|
|
329
|
+
)
|
|
262
330
|
|
|
263
331
|
return await this.activateConnection(connection, options)
|
|
264
332
|
}
|
|
@@ -291,7 +359,11 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
291
359
|
if (connection) return await this.activateConnection(connection, options)
|
|
292
360
|
|
|
293
361
|
if (this.canSpawnConnection(databaseConfig)) {
|
|
294
|
-
connection = await this.spawnConnectionForCheckout(
|
|
362
|
+
connection = await this.spawnConnectionForCheckout(
|
|
363
|
+
databaseConfig,
|
|
364
|
+
reuseKey,
|
|
365
|
+
currentTestProfileContext(this.configuration)
|
|
366
|
+
)
|
|
295
367
|
|
|
296
368
|
return await this.activateConnection(connection, options)
|
|
297
369
|
}
|
|
@@ -436,13 +508,17 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
436
508
|
* Runs spawn connection for checkout.
|
|
437
509
|
* @param {import("../../configuration-types.js").DatabaseConfigurationType} databaseConfig - Resolved database config for the checkout.
|
|
438
510
|
* @param {string} reuseKey - Database configuration reuse key for the checkout.
|
|
511
|
+
* @param {import("../../testing/test-profiler.js").TestProfileAsyncContext | undefined} profileContext - Profile context captured when checkout began.
|
|
439
512
|
* @returns {Promise<import("../drivers/base.js").default>} - Spawned connection.
|
|
440
513
|
*/
|
|
441
|
-
async spawnConnectionForCheckout(databaseConfig, reuseKey) {
|
|
514
|
+
async spawnConnectionForCheckout(databaseConfig, reuseKey, profileContext) {
|
|
442
515
|
this.connectionsBeingSpawned++
|
|
443
516
|
|
|
444
517
|
try {
|
|
445
|
-
const
|
|
518
|
+
const environmentHandler = this.configuration.getEnvironmentHandler()
|
|
519
|
+
const connection = await environmentHandler.runWithTestProfileContext(profileContext, async () => {
|
|
520
|
+
return await this.spawnConnectionWithConfiguration(databaseConfig)
|
|
521
|
+
})
|
|
446
522
|
|
|
447
523
|
this.stampConnectionForConfigurationReuseKey(connection, reuseKey)
|
|
448
524
|
|
|
@@ -473,7 +549,8 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
473
549
|
reuseKey,
|
|
474
550
|
timeoutAt: timeoutMillis === null ? null : enqueuedAt + timeoutMillis,
|
|
475
551
|
timeoutMillis,
|
|
476
|
-
timeoutTimer: undefined
|
|
552
|
+
timeoutTimer: undefined,
|
|
553
|
+
testProfileContext: currentTestProfileContext(this.configuration)
|
|
477
554
|
}
|
|
478
555
|
|
|
479
556
|
checkout.timeoutTimer = this.startPendingCheckoutTimeout(checkout)
|
|
@@ -573,11 +650,12 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
573
650
|
* @returns {void}
|
|
574
651
|
*/
|
|
575
652
|
recordCheckoutWait(checkout) {
|
|
576
|
-
const waitedForMs = Math.max(0,
|
|
653
|
+
const waitedForMs = Math.max(0, this.nowMs() - checkout.enqueuedAt)
|
|
577
654
|
|
|
578
655
|
this.telemetry.checkoutWaitCount++
|
|
579
656
|
this.telemetry.checkoutWaitTotalMs += waitedForMs
|
|
580
657
|
this.telemetry.checkoutWaitMaxMs = Math.max(this.telemetry.checkoutWaitMaxMs, waitedForMs)
|
|
658
|
+
this.recordTestProfilePoolMetric(checkout.testProfileContext, "checkoutWait", {durationMs: waitedForMs})
|
|
581
659
|
}
|
|
582
660
|
|
|
583
661
|
/**
|
|
@@ -606,6 +684,8 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
606
684
|
if (index === -1) return
|
|
607
685
|
|
|
608
686
|
this.removePendingCheckoutAt(index)
|
|
687
|
+
this.telemetry.checkoutTimeoutCount++
|
|
688
|
+
this.recordTestProfilePoolMetric(checkout.testProfileContext, "checkoutTimeout")
|
|
609
689
|
checkout.reject(this.pendingCheckoutTimeoutError(checkout))
|
|
610
690
|
}
|
|
611
691
|
|
|
@@ -739,16 +819,24 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
739
819
|
* @returns {Promise<void>} - Resolves when the checkout has been handled.
|
|
740
820
|
*/
|
|
741
821
|
async spawnAndResolvePendingCheckout(checkout) {
|
|
742
|
-
|
|
822
|
+
const environmentHandler = this.configuration.getEnvironmentHandler()
|
|
743
823
|
|
|
744
|
-
|
|
745
|
-
connection
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
824
|
+
return await environmentHandler.runWithTestProfileContext(checkout.testProfileContext, async () => {
|
|
825
|
+
let connection
|
|
826
|
+
|
|
827
|
+
try {
|
|
828
|
+
connection = await this.spawnConnectionForCheckout(
|
|
829
|
+
checkout.databaseConfig,
|
|
830
|
+
checkout.reuseKey,
|
|
831
|
+
checkout.testProfileContext
|
|
832
|
+
)
|
|
833
|
+
} catch (error) {
|
|
834
|
+
checkout.reject(error instanceof Error ? error : new Error("Failed to spawn database connection.", {cause: error}))
|
|
835
|
+
return
|
|
836
|
+
}
|
|
750
837
|
|
|
751
|
-
|
|
838
|
+
await this.resolvePendingCheckout(checkout, connection)
|
|
839
|
+
})
|
|
752
840
|
}
|
|
753
841
|
|
|
754
842
|
/**
|
|
@@ -758,11 +846,15 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
758
846
|
* @returns {Promise<void>} - Resolves when the checkout has been handled.
|
|
759
847
|
*/
|
|
760
848
|
async resolvePendingCheckout(checkout, connection) {
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
849
|
+
const environmentHandler = this.configuration.getEnvironmentHandler()
|
|
850
|
+
|
|
851
|
+
return await environmentHandler.runWithTestProfileContext(checkout.testProfileContext, async () => {
|
|
852
|
+
try {
|
|
853
|
+
checkout.resolve(await this.activateConnection(connection, checkout.options))
|
|
854
|
+
} catch (error) {
|
|
855
|
+
checkout.reject(error instanceof Error ? error : new Error("Failed to activate database connection.", {cause: error}))
|
|
856
|
+
}
|
|
857
|
+
})
|
|
766
858
|
}
|
|
767
859
|
|
|
768
860
|
/**
|
|
@@ -1308,24 +1400,40 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
1308
1400
|
const idleTimeoutMillis = this.idleTimeoutMillis()
|
|
1309
1401
|
|
|
1310
1402
|
if (idleTimeoutMillis === null) return
|
|
1403
|
+
const startedAt = this.nowMs()
|
|
1404
|
+
const profileContext = currentTestProfileContext(this.configuration)
|
|
1405
|
+
let failed = true
|
|
1406
|
+
|
|
1407
|
+
try {
|
|
1408
|
+
const {expiredConnections, keptConnections} = this.classifyIdleConnectionsForReaping({idleTimeoutMillis, now: this.nowMs()})
|
|
1311
1409
|
|
|
1312
|
-
|
|
1410
|
+
this.connections = keptConnections
|
|
1411
|
+
await this.closeExpiredIdleConnections(expiredConnections, profileContext)
|
|
1412
|
+
await this.awaitInflightConnectionCloses()
|
|
1413
|
+
if (this.connections.length > 0) this.scheduleIdleConnectionReaper()
|
|
1414
|
+
failed = false
|
|
1415
|
+
} finally {
|
|
1416
|
+
const durationMs = Math.max(0, this.nowMs() - startedAt)
|
|
1313
1417
|
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1418
|
+
this.telemetry.idleReapCount++
|
|
1419
|
+
if (failed) this.telemetry.idleReapFailureCount++
|
|
1420
|
+
this.telemetry.idleReapTotalMs += durationMs
|
|
1421
|
+
this.telemetry.idleReapMaxMs = Math.max(this.telemetry.idleReapMaxMs, durationMs)
|
|
1422
|
+
this.recordTestProfilePoolMetric(profileContext, "idleReap", {durationMs, failed})
|
|
1423
|
+
}
|
|
1318
1424
|
}
|
|
1319
1425
|
|
|
1320
1426
|
/**
|
|
1321
1427
|
* Runs close expired idle connections.
|
|
1322
1428
|
* @param {import("../drivers/base.js").default[]} expiredConnections - Connections to close.
|
|
1429
|
+
* @param {import("../../testing/test-profiler.js").TestProfileAsyncContext | undefined} [profileContext] - Reaper profile context.
|
|
1323
1430
|
* @returns {Promise<void>} - Resolves when closed.
|
|
1324
1431
|
*/
|
|
1325
|
-
async closeExpiredIdleConnections(expiredConnections) {
|
|
1432
|
+
async closeExpiredIdleConnections(expiredConnections, profileContext) {
|
|
1326
1433
|
for (const connection of expiredConnections) {
|
|
1327
1434
|
await this.closeConnection(connection)
|
|
1328
1435
|
this.telemetry.idleReapDisposalCount++
|
|
1436
|
+
this.recordTestProfilePoolMetric(profileContext, "idleReapDisposal")
|
|
1329
1437
|
}
|
|
1330
1438
|
}
|
|
1331
1439
|
|
|
@@ -45,7 +45,7 @@ import sha256Hex from "../../utils/sha256-hex.js"
|
|
|
45
45
|
* @property {Array<DatabasePoolPendingCheckoutDebugSnapshot>} [pendingCheckouts] - Waiting checkout snapshots.
|
|
46
46
|
* @property {number} pendingCheckoutCount - Number of queued checkout requests.
|
|
47
47
|
* @property {string} poolClass - Pool class name.
|
|
48
|
-
* @property {{checkoutWaitCount: number, checkoutWaitMaxMs: number, checkoutWaitTotalMs: number, idleReapDisposalCount: number}} [telemetry] - Cumulative pool lifecycle telemetry.
|
|
48
|
+
* @property {{connectionCreationCount: number, connectionCreationFailureCount: number, connectionCreationMaxMs: number, connectionCreationTotalMs: number, checkoutTimeoutCount: number, checkoutWaitCount: number, checkoutWaitMaxMs: number, checkoutWaitTotalMs: number, idleReapCount: number, idleReapDisposalCount: number, idleReapFailureCount: number, idleReapMaxMs: number, idleReapTotalMs: number, peakLiveConnections: number}} [telemetry] - Cumulative pool lifecycle telemetry.
|
|
49
49
|
*/
|
|
50
50
|
export const POOL_CONFIGURATION_KEY = Symbol("velociousPoolConfigurationKey")
|
|
51
51
|
|
|
@@ -54,6 +54,22 @@ export default class VelociousEnvironmentHandlerBase {
|
|
|
54
54
|
*/
|
|
55
55
|
runWithSharedTransactionCoordinatorOwner(_connection, _owner, callback) { return callback() }
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Runs work with test-profile attribution. Runtimes without async-context
|
|
59
|
+
* storage execute the callback without installing ambient attribution.
|
|
60
|
+
* @template T
|
|
61
|
+
* @param {import("../testing/test-profiler.js").TestProfileAsyncContext | undefined} _context - Captured profile context, or an explicit absence of attribution.
|
|
62
|
+
* @param {() => T} callback - Profiled work.
|
|
63
|
+
* @returns {T} - Callback result.
|
|
64
|
+
*/
|
|
65
|
+
runWithTestProfileContext(_context, callback) { return callback() }
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Gets the current test-profile attribution context.
|
|
69
|
+
* @returns {import("../testing/test-profiler.js").TestProfileAsyncContext | undefined} - Active context.
|
|
70
|
+
*/
|
|
71
|
+
getCurrentTestProfileContext() { return undefined }
|
|
72
|
+
|
|
57
73
|
/**
|
|
58
74
|
* Mutable ambient tenant used by runtimes without async-context storage.
|
|
59
75
|
* @type {ReturnType<typeof JSON.parse> | undefined}
|