velocious 1.0.682 → 1.0.684

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.
Files changed (38) hide show
  1. package/README.md +1 -0
  2. package/build/background-jobs/pooled-runner-child.js +102 -0
  3. package/build/background-jobs/types.js +21 -0
  4. package/build/background-jobs/worker.js +111 -1
  5. package/build/http-server/client/index.js +15 -0
  6. package/build/http-server/client/request.js +44 -0
  7. package/build/http-server/client/websocket-request.js +45 -0
  8. package/build/http-server/worker-handler/in-process.js +7 -1
  9. package/build/http-server/worker-handler/worker-thread.js +5 -0
  10. package/build/src/background-jobs/pooled-runner-child.js +95 -1
  11. package/build/src/background-jobs/types.d.ts +71 -0
  12. package/build/src/background-jobs/types.d.ts.map +1 -1
  13. package/build/src/background-jobs/types.js +22 -1
  14. package/build/src/background-jobs/worker.d.ts +40 -1
  15. package/build/src/background-jobs/worker.d.ts.map +1 -1
  16. package/build/src/background-jobs/worker.js +109 -2
  17. package/build/src/http-server/client/index.d.ts +9 -0
  18. package/build/src/http-server/client/index.d.ts.map +1 -1
  19. package/build/src/http-server/client/index.js +16 -1
  20. package/build/src/http-server/client/request.d.ts +27 -0
  21. package/build/src/http-server/client/request.d.ts.map +1 -1
  22. package/build/src/http-server/client/request.js +42 -1
  23. package/build/src/http-server/client/websocket-request.d.ts +28 -0
  24. package/build/src/http-server/client/websocket-request.d.ts.map +1 -1
  25. package/build/src/http-server/client/websocket-request.js +43 -1
  26. package/build/src/http-server/worker-handler/in-process.d.ts.map +1 -1
  27. package/build/src/http-server/worker-handler/in-process.js +8 -2
  28. package/build/src/http-server/worker-handler/worker-thread.d.ts.map +1 -1
  29. package/build/src/http-server/worker-handler/worker-thread.js +6 -1
  30. package/package.json +1 -1
  31. package/src/background-jobs/pooled-runner-child.js +102 -0
  32. package/src/background-jobs/types.js +21 -0
  33. package/src/background-jobs/worker.js +111 -1
  34. package/src/http-server/client/index.js +15 -0
  35. package/src/http-server/client/request.js +44 -0
  36. package/src/http-server/client/websocket-request.js +45 -0
  37. package/src/http-server/worker-handler/in-process.js +7 -1
  38. package/src/http-server/worker-handler/worker-thread.js +5 -0
@@ -73,6 +73,27 @@
73
73
  * @property {number | null} shutdownRequestedAtMs - Parent request timestamp when supplied over IPC.
74
74
  * @property {import("node:child_process").ChildProcess["signalCode"]} signal - Requested or observed signal when available.
75
75
  */
76
+ /**
77
+ * Bounded pooled-child memory observation sent over the child IPC channel.
78
+ * The pooled child's stdio is ignored by the worker fork, so this observation
79
+ * (sent periodically while jobs run, plus on demand) is how a memory problem
80
+ * in a running child names itself. `heapStatistics` and `memoryUsage`
81
+ * distinguish V8-heap growth from external/array-buffer (native resource)
82
+ * growth; `activeJobIds` ties the sample to the work that was in flight.
83
+ * @typedef {object} PooledChildMemoryObservation
84
+ * @property {string[]} activeJobIds - In-flight job ids, bounded.
85
+ * @property {number} activeJobIdsTruncatedCount - In-flight job ids omitted by the bound.
86
+ * @property {string} childInstanceId - Stable identity reported by the child.
87
+ * @property {number} childPid - Child process id.
88
+ * @property {number} childUptimeMs - Child process uptime in ms.
89
+ * @property {ReturnType<typeof import("node:v8").getHeapStatistics>} heapStatistics - V8 heap-stat breakdown at the sample.
90
+ * @property {number} jobCount - In-flight job count.
91
+ * @property {ReturnType<typeof import("node:process").memoryUsage>} memoryUsage - Process memory breakdown at the sample.
92
+ * @property {number} observedAtMs - Epoch ms the child sampled.
93
+ * @property {number} rssBytes - Resident set size in bytes at the sample.
94
+ * @property {"pooled-child-memory"} type - Discriminator.
95
+ * @property {number} uptimeMs - Process uptime in ms.
96
+ */
76
97
  /**
77
98
  * @typedef {object} LocalBackgroundJobsClock
78
99
  * @property {() => number} now - Current epoch milliseconds.
@@ -41,6 +41,7 @@ import { POOLED_RUNNER_INFLIGHT_JOB_ID_LIMIT, boundedPooledRunnerInflightJobIds,
41
41
  * @property {boolean} retiring - Whether this child is draining before retirement.
42
42
  * @property {boolean} [started] - Whether the child completed its startup handshake.
43
43
  * @property {boolean} [settling] - Whether failure handling already owns this child.
44
+ * @property {import("./types.js").PooledChildMemoryObservation} [lastMemoryObservation] - Latest memory observation received from this child.
44
45
  * @property {number} [ipcDisconnectedAtMs] - Parent observation of IPC disconnect.
45
46
  * @property {import("./types.js").PooledChildShutdownObservation} [shutdownObservation] - Child observation sent before teardown.
46
47
  * @property {import("./types.js").PooledChildShutdownReason} [shutdownReason] - Exact parent-requested shutdown reason.
@@ -132,6 +133,38 @@ function isChildShutdownObservationMessage(message) {
132
133
  && isPooledChildShutdownSignal(record.signal)
133
134
  }
134
135
 
136
+ /**
137
+ * Checks whether an IPC value is a pooled child's bounded memory observation.
138
+ * The pooled child's stdio is ignored by the worker fork, so this observation
139
+ * (periodic while jobs run, plus on demand) is how a memory problem in a
140
+ * running child names itself. The worker logs it (its stderr reaches the prod
141
+ * log) and forwards it to the optional `onPooledRunnerMemoryObservation` hook.
142
+ * @param {ReturnType<typeof JSON.parse>} message - IPC message.
143
+ * @returns {message is import("./types.js").PooledChildMemoryObservation} - Whether this is a valid memory observation.
144
+ */
145
+ function isPooledChildMemoryObservationMessage(message) {
146
+ if (!message || typeof message !== "object") return false
147
+ const record = /** @type {Record<string, ReturnType<typeof JSON.parse>>} */ (message)
148
+ const heapStatistics = /** @type {Record<string, ReturnType<typeof JSON.parse>> | undefined} */ (record.heapStatistics)
149
+ const memoryUsage = /** @type {Record<string, ReturnType<typeof JSON.parse>> | undefined} */ (record.memoryUsage)
150
+
151
+ return record.type === "pooled-child-memory"
152
+ && typeof record.childInstanceId === "string"
153
+ && Number.isInteger(record.childPid)
154
+ && typeof record.rssBytes === "number"
155
+ && Number.isFinite(record.rssBytes)
156
+ && Number.isInteger(record.jobCount)
157
+ && record.jobCount >= 0
158
+ && Array.isArray(record.activeJobIds)
159
+ && record.activeJobIds.every((jobId) => typeof jobId === "string")
160
+ && Number.isInteger(record.activeJobIdsTruncatedCount)
161
+ && record.activeJobIdsTruncatedCount >= 0
162
+ && typeof record.observedAtMs === "number"
163
+ && Number.isFinite(record.observedAtMs)
164
+ && heapStatistics !== undefined && typeof heapStatistics === "object"
165
+ && memoryUsage !== undefined && typeof memoryUsage === "object"
166
+ }
167
+
135
168
  /**
136
169
  * Normalizes a candidate pooled-runner resource limit.
137
170
  * @param {number | undefined} value - Candidate positive number.
@@ -166,8 +199,9 @@ export default class BackgroundJobsWorker {
166
199
  * @param {() => void | Promise<void>} [args.onStopped] - Lifecycle hook invoked after the worker finishes stopping.
167
200
  * @param {() => void} [args.onGenerationAccepted] - Explicit generation-acceptance observation hook.
168
201
  * @param {() => void} [args.onRetireMessage] - Explicit retire-message observation hook.
202
+ * @param {(observation: import("./types.js").PooledChildMemoryObservation) => void | Promise<void>} [args.onPooledRunnerMemoryObservation] - Explicit pooled-child memory observation hook. Every validated observation (periodic while a child has in-flight jobs, plus on demand) is forwarded here, in addition to the worker's compact stderr log line, so an application can route memory diagnostics (e.g. to a bug reporter) without parsing logs.
169
203
  */
170
- constructor({configuration, host, port, generationId, workerInstanceId, maxConcurrentForkedJobs, maxConcurrentInlineJobs, pooledRunnerCount, pooledRunnerConcurrency, pooledRunnerMaxJobs, pooledRunnerMaxRssBytes, pooledRunnerMaxLifetimeMs, forkedChildSigkillGraceMs, heartbeatIntervalMs, generationHandshakeTimeoutMs = DEFAULT_GENERATION_HANDSHAKE_TIMEOUT_MS, reconnectDelayMs = 1000, jobTimeoutMs, closeDatabaseConnectionsOnStop = true, onStopped, onGenerationAccepted, onRetireMessage} = {}) {
204
+ constructor({configuration, host, port, generationId, workerInstanceId, maxConcurrentForkedJobs, maxConcurrentInlineJobs, pooledRunnerCount, pooledRunnerConcurrency, pooledRunnerMaxJobs, pooledRunnerMaxRssBytes, pooledRunnerMaxLifetimeMs, forkedChildSigkillGraceMs, heartbeatIntervalMs, generationHandshakeTimeoutMs = DEFAULT_GENERATION_HANDSHAKE_TIMEOUT_MS, reconnectDelayMs = 1000, jobTimeoutMs, closeDatabaseConnectionsOnStop = true, onStopped, onGenerationAccepted, onRetireMessage, onPooledRunnerMemoryObservation} = {}) {
171
205
  /**
172
206
  * Narrows the runtime value to the documented type.
173
207
  * @type {Promise<import("../configuration.js").default>} */
@@ -186,6 +220,7 @@ export default class BackgroundJobsWorker {
186
220
  this.onStopped = onStopped
187
221
  this.onGenerationAccepted = onGenerationAccepted
188
222
  this.onRetireMessage = onRetireMessage
223
+ this.onPooledRunnerMemoryObservation = onPooledRunnerMemoryObservation
189
224
  /**
190
225
  * Constructor override for the inline-job concurrency cap. When unset
191
226
  * the cap is read from `configuration.getBackgroundJobsConfig()` in
@@ -1393,6 +1428,10 @@ export default class BackgroundJobsWorker {
1393
1428
  this._reportChildAccepted(message)
1394
1429
  return
1395
1430
  }
1431
+ if (isPooledChildMemoryObservationMessage(message)) {
1432
+ this._handlePooledChildMemoryObservation({child, message})
1433
+ return
1434
+ }
1396
1435
  if (record.type !== "job-outcome" || !state || state.settling || typeof record.jobId !== "string") return
1397
1436
  state.started = true
1398
1437
  const entry = state.inflight.get(record.jobId)
@@ -1456,6 +1495,77 @@ export default class BackgroundJobsWorker {
1456
1495
  })
1457
1496
  }
1458
1497
 
1498
+ /**
1499
+ * Handles a pooled child's bounded memory observation. The pooled child's
1500
+ * stdio is ignored by the worker fork, so this IPC observation is how a
1501
+ * memory problem in a running child names itself. The worker (a) records the
1502
+ * latest observation on the child's state for later correlation, (b) logs one
1503
+ * compact line to its own stderr (which reaches the prod log, unlike the
1504
+ * child's ignored stdio), and (c) forwards the full observation to the
1505
+ * optional `onPooledRunnerMemoryObservation` hook so an application can route
1506
+ * it (e.g. to a bug reporter) without parsing logs. The heap-stat breakdown
1507
+ * distinguishes V8-heap growth from external/array-buffer (native) growth. A
1508
+ * hook failure is swallowed — diagnostics must never take down the worker or
1509
+ * fail the jobs running on that child.
1510
+ * @param {object} args - Message details.
1511
+ * @param {import("node:child_process").ChildProcess} args.child - Pooled child.
1512
+ * @param {import("./types.js").PooledChildMemoryObservation} args.message - Validated memory observation.
1513
+ * @returns {void}
1514
+ */
1515
+ _handlePooledChildMemoryObservation({child, message}) {
1516
+ const state = this.pooledChildStates.get(child)
1517
+ if (state) state.lastMemoryObservation = message
1518
+
1519
+ const heap = message.heapStatistics
1520
+ console.error(
1521
+ JSON.stringify({
1522
+ event: "pooled-child-memory",
1523
+ childInstanceId: state?.childInstanceId ?? message.childInstanceId,
1524
+ childPid: message.childPid,
1525
+ childUptimeS: Math.round(message.childUptimeMs / 1000),
1526
+ rssMb: Math.round(message.rssBytes / (1024 * 1024)),
1527
+ heapUsedMb: Math.round(heap.used_heap_size / (1024 * 1024)),
1528
+ heapTotalMb: Math.round(heap.total_heap_size / (1024 * 1024)),
1529
+ heapLimitMb: Math.round(heap.heap_size_limit / (1024 * 1024)),
1530
+ externalMb: Math.round(message.memoryUsage.external / (1024 * 1024)),
1531
+ arrayBuffersMb: Math.round(message.memoryUsage.arrayBuffers / (1024 * 1024)),
1532
+ jobs: message.jobCount,
1533
+ activeJobIds: message.activeJobIds
1534
+ })
1535
+ )
1536
+
1537
+ if (this.onPooledRunnerMemoryObservation) {
1538
+ try {
1539
+ const result = this.onPooledRunnerMemoryObservation(message)
1540
+ if (result && typeof result.catch === "function") result.catch((error) => {
1541
+ console.error("Pooled runner memory observation hook failed:", error)
1542
+ })
1543
+ } catch (error) {
1544
+ console.error("Pooled runner memory observation hook failed:", error)
1545
+ }
1546
+ }
1547
+ }
1548
+
1549
+ /**
1550
+ * Requests an immediate memory observation from one pooled child. The child
1551
+ * replies over IPC with its current snapshot (the same shape as the periodic
1552
+ * sampler), which the worker records, logs, and forwards to the
1553
+ * `onPooledRunnerMemoryObservation` hook. Use this to pull a snapshot on
1554
+ * suspicion (e.g. after an OOM report) without waiting for the next periodic
1555
+ * sample. A no-op when the child is gone or its IPC channel is closed.
1556
+ * @param {import("node:child_process").ChildProcess} child - Pooled child to sample.
1557
+ * @returns {void}
1558
+ */
1559
+ requestPooledChildMemoryObservation(child) {
1560
+ if (!child.connected) return
1561
+
1562
+ try {
1563
+ child.send({type: "memory-observation-request"})
1564
+ } catch {
1565
+ // The IPC channel is already gone; the disconnect/exit handler owns teardown.
1566
+ }
1567
+ }
1568
+
1459
1569
  /**
1460
1570
  * Marks a pooled child for retirement and — when the pool is below its hard
1461
1571
  * cap — eagerly spawns a single replacement (1-for-1) so its capacity is
@@ -803,6 +803,21 @@ export default class VeoliciousHttpServerClient {
803
803
  }
804
804
  }
805
805
 
806
+ /**
807
+ * Marks every in-flight request as client-disconnected and runs its
808
+ * disconnect callbacks. The worker handler calls this when the underlying
809
+ * socket tears down, so a handler whose response is still buffered (e.g.
810
+ * waiting in an admission queue) can observe the client leaving without
811
+ * waiting for the response to be sent.
812
+ * @returns {void}
813
+ */
814
+ notifyClientDisconnect() {
815
+ for (const requestRunner of this.requestRunners) {
816
+ if (requestRunner.getState() !== "running") continue
817
+ requestRunner.getRequest().markClientDisconnected()
818
+ }
819
+ }
820
+
806
821
  /**
807
822
  * Runs the close callbacks of a finished or aborted stream exactly once.
808
823
  * @param {import("./response.js").default} response - Finished stream.
@@ -19,6 +19,17 @@ export default class VelociousHttpServerClientRequest {
19
19
  this.client = client
20
20
  this.configuration = configuration
21
21
  this.requestParser = new RequestParser({configuration})
22
+
23
+ /**
24
+ * Whether the owning client connection was torn down while this request
25
+ * was still running. Set once by the client on socket teardown and read
26
+ * by handlers that need to settle in-flight work (e.g. admission queue
27
+ * positions) without waiting for the response to be sent.
28
+ * @type {boolean} */
29
+ this.clientDisconnected = false
30
+
31
+ /** @type {Set<() => void>} */
32
+ this.clientDisconnectCallbacks = new Set()
22
33
  }
23
34
 
24
35
  baseURL() { return `${this.protocol()}://${this.hostWithPort()}` }
@@ -114,6 +125,39 @@ export default class VelociousHttpServerClientRequest {
114
125
  rawBody() { return this.getRequestBuffer().getRawBody() }
115
126
  socketRemoteAddress() { return this.client?.remoteAddress }
116
127
 
128
+ /**
129
+ * Marks this request as client-disconnected and runs every registered
130
+ * disconnect callback exactly once. The owning client invokes it when the
131
+ * socket tears down; handlers that register afterwards learn of the
132
+ * disconnect through the `clientDisconnected` field instead.
133
+ * @returns {void}
134
+ */
135
+ markClientDisconnected() {
136
+ if (this.clientDisconnected) return
137
+ this.clientDisconnected = true
138
+ for (const callback of this.clientDisconnectCallbacks) {
139
+ callback()
140
+ }
141
+ this.clientDisconnectCallbacks.clear()
142
+ }
143
+
144
+ /**
145
+ * Registers a callback that fires once when the client connection tears
146
+ * down while this request is still running. Buffered requests cannot rely
147
+ * on the streaming response's `onStreamClose` for that: no stream has
148
+ * been opened yet, so a queued request's queue position is otherwise
149
+ * stranded until its admission deadline.
150
+ * @param {() => void} callback - Disconnect callback.
151
+ * @returns {void}
152
+ */
153
+ onClientDisconnect(callback) {
154
+ if (this.clientDisconnected) {
155
+ callback()
156
+ return
157
+ }
158
+ this.clientDisconnectCallbacks.add(callback)
159
+ }
160
+
117
161
  getRequestBuffer() { return this.getRequestParser().getRequestBuffer() }
118
162
  getRequestParser() { return this.requestParser }
119
163
  }
@@ -50,6 +50,51 @@ export default class VelociousHttpServerClientWebsocketRequest {
50
50
  const queryParams = this._parseQueryParams()
51
51
 
52
52
  this.paramsObject = {...queryParams, ...this.paramsObject}
53
+
54
+ /**
55
+ * Whether the owning client connection was torn down while this request
56
+ * was still running. Websocket requests are session-owned and never
57
+ * enter the client's in-flight request list, so this stays false for
58
+ * them; the field exists so the request union shares one surface.
59
+ * @type {boolean} */
60
+ this.clientDisconnected = false
61
+
62
+ /** @type {Set<() => void>} */
63
+ this.clientDisconnectCallbacks = new Set()
64
+ }
65
+
66
+ /**
67
+ * Marks this request as client-disconnected and runs every registered
68
+ * disconnect callback exactly once. The owning client invokes it when the
69
+ * socket tears down; handlers that register afterwards learn of the
70
+ * disconnect through the `clientDisconnected` field instead. Websocket
71
+ * requests are session-owned and never enter the client's in-flight
72
+ * request list, so this is inert for them; the shared surface keeps the
73
+ * request union uniform.
74
+ * @returns {void}
75
+ */
76
+ markClientDisconnected() {
77
+ if (this.clientDisconnected) return
78
+ this.clientDisconnected = true
79
+ for (const callback of this.clientDisconnectCallbacks) {
80
+ callback()
81
+ }
82
+ this.clientDisconnectCallbacks.clear()
83
+ }
84
+
85
+ /**
86
+ * Registers a callback that fires once when the client connection tears
87
+ * down while this request is still running. See the HTTP request's
88
+ * {@linkcode markClientDisconnected} for the shared-surface note.
89
+ * @param {() => void} callback - Disconnect callback.
90
+ * @returns {void}
91
+ */
92
+ onClientDisconnect(callback) {
93
+ if (this.clientDisconnected) {
94
+ callback()
95
+ return
96
+ }
97
+ this.clientDisconnectCallbacks.add(callback)
53
98
  }
54
99
 
55
100
  baseURL() {
@@ -116,7 +116,13 @@ export default class VelociousHttpServerInProcessHandler {
116
116
  httpClient.abortPendingFileResponses(),
117
117
  httpClient.abortStreamResponses()
118
118
  ])
119
- .then(() => {})
119
+ .then(() => {
120
+ // Buffered responses have no stream to abort, so their in-flight
121
+ // handlers never hear about the socket teardown through the
122
+ // streaming path: notify the running requests directly so they can
123
+ // settle resources (e.g. admission queue positions) in-process.
124
+ httpClient.notifyClientDisconnect()
125
+ })
120
126
  .catch((error) => {
121
127
  this.logger.warn("Failed to abort responses after client close", error)
122
128
  })
@@ -220,6 +220,11 @@ export default class VelociousHttpServerWorkerHandlerWorkerThread {
220
220
  if (client) {
221
221
  settlements.push(client.abortPendingFileResponses())
222
222
  settlements.push(client.abortStreamResponses())
223
+ // Buffered responses have no stream to abort, so their in-flight
224
+ // handlers never hear about the socket teardown through the streaming
225
+ // path: notify the running requests directly so they can settle
226
+ // resources (e.g. admission queue positions) in-process.
227
+ client.notifyClientDisconnect()
223
228
  }
224
229
 
225
230
  for (const [transferId, transfer] of this.fileTransfers) {