@platformatic/runtime 3.52.2 → 3.52.3

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/lib/runtime.js CHANGED
@@ -61,6 +61,7 @@ import {
61
61
  kHealthCheckTimer,
62
62
  kId,
63
63
  kInterceptorReadyPromise,
64
+ kIsSubprocessHost,
64
65
  kITC,
65
66
  kLastHealthCheckELU,
66
67
  kStderrMarker,
@@ -73,6 +74,9 @@ import {
73
74
 
74
75
  const kWorkerFile = join(import.meta.dirname, 'worker/main.js')
75
76
  const kInspectorOptions = Symbol('plt.runtime.worker.inspectorOptions')
77
+ const kHeapCheckCounter = Symbol('plt.runtime.worker.heapCheckCounter')
78
+ const kLastHeapStats = Symbol('plt.runtime.worker.lastHeapStats')
79
+ const kHealthITCTimeoutMs = 5000
76
80
 
77
81
  const MAX_LISTENERS_COUNT = 100
78
82
 
@@ -1515,12 +1519,57 @@ export class Runtime extends EventEmitter {
1515
1519
  return status
1516
1520
  }
1517
1521
 
1518
- async getWorkerHealth (worker, options = {}) {
1519
- // Use ITC to get health from the worker. This correctly forwards to child
1520
- // processes when the service uses startWithCommand (subprocess mode),
1521
- // instead of reading the coordinator thread's metrics.
1522
- const { currentELU, heapUsed, heapTotal } = await sendViaITC(worker, 'getHealth')
1522
+ getWorkerHealth (worker, options = {}) {
1523
+ // For subprocess workers we must round-trip through ITC to reach the child;
1524
+ // for pure worker-thread workers we can read ELU/heap directly from the
1525
+ // worker handle, which is served by Node's C++ layer and does not depend
1526
+ // on the worker's event loop being responsive. Going through ITC for
1527
+ // thread workers means a CPU-bound or stuck worker can freeze the whole
1528
+ // health-collection loop, which in turn blocks the management API.
1529
+ if (worker[kIsSubprocessHost]) {
1530
+ return this.#getSubprocessWorkerHealth(worker, options)
1531
+ }
1523
1532
 
1533
+ const currentELU = worker.performance.eventLoopUtilization()
1534
+ const previousELU = options.previousELU
1535
+
1536
+ let elu = currentELU
1537
+ if (previousELU) {
1538
+ elu = worker.performance.eventLoopUtilization(elu, previousELU)
1539
+ }
1540
+
1541
+ if (!features.node.worker.getHeapStatistics) {
1542
+ return { elu: elu.utilization, currentELU }
1543
+ }
1544
+
1545
+ // Only refresh heap statistics every 60 health checks (once per minute).
1546
+ // This keeps the common path fully synchronous — no promise allocation.
1547
+ const counter = (worker[kHeapCheckCounter] ?? 0) + 1
1548
+ worker[kHeapCheckCounter] = counter >= 60 ? 0 : counter
1549
+
1550
+ if (counter >= 60 || !worker[kLastHeapStats]) {
1551
+ return worker.getHeapStatistics().then(({ used_heap_size: heapUsed, total_heap_size: heapTotal }) => {
1552
+ worker[kLastHeapStats] = { heapUsed, heapTotal }
1553
+ return { elu: elu.utilization, heapUsed, heapTotal, currentELU }
1554
+ })
1555
+ }
1556
+
1557
+ const { heapUsed, heapTotal } = worker[kLastHeapStats]
1558
+ return { elu: elu.utilization, heapUsed, heapTotal, currentELU }
1559
+ }
1560
+
1561
+ async #getSubprocessWorkerHealth (worker, options) {
1562
+ // Bound the ITC call so a hung child cannot freeze the health loop.
1563
+ // On timeout we fall back to last-known ELU with a null heap reading so
1564
+ // that the loop keeps rescheduling and signals remain observable.
1565
+ const result = await executeWithTimeout(sendViaITC(worker, 'getHealth'), kHealthITCTimeoutMs, kTimeout)
1566
+
1567
+ if (result === kTimeout) {
1568
+ const previousELU = options.previousELU ?? worker.performance.eventLoopUtilization()
1569
+ return { elu: 1, heapUsed: null, heapTotal: null, currentELU: previousELU }
1570
+ }
1571
+
1572
+ const { currentELU, heapUsed, heapTotal } = result
1524
1573
  const previousELU = options.previousELU
1525
1574
  let elu = currentELU
1526
1575
  if (previousELU) {
@@ -1853,6 +1902,14 @@ export class Runtime extends EventEmitter {
1853
1902
  this.logger.trace({ event, payload, id: workerId, application: applicationId, worker: index }, 'Runtime event')
1854
1903
  })
1855
1904
 
1905
+ // The worker notifies us when its capability has spawned a child process
1906
+ // (e.g. Next.js in dev mode). From that point on health metrics must come
1907
+ // from the child via ITC; for thread-only workers we keep reading the
1908
+ // handle directly in getWorkerHealth().
1909
+ worker[kITC].on('subprocess:started', () => {
1910
+ worker[kIsSubprocessHost] = true
1911
+ })
1912
+
1856
1913
  worker[kITC].on('request:restart', async () => {
1857
1914
  // Do not restart applications that are not fully started yet or when the runtime is still starting.
1858
1915
  // The gateway sends request:restart when it receives application:added events,
@@ -12,6 +12,7 @@ export const kInterceptors = Symbol.for('plt.runtime.worker.interceptors')
12
12
  export const kLastHealthCheckELU = Symbol.for('plt.runtime.worker.lastHealthCheckELU')
13
13
  export const kLastWorkerScalerELU = Symbol.for('plt.runtime.worker.lastWorkerScalerELU')
14
14
  export const kInterceptorReadyPromise = Symbol.for('plt.runtime.worker.interceptorReadyPromise')
15
+ export const kIsSubprocessHost = Symbol.for('plt.runtime.worker.isSubprocessHost')
15
16
 
16
17
  // This string marker should be safe to use since it belongs to Unicode private area
17
18
  export const kStderrMarker = '\ue002'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "3.52.2",
3
+ "version": "3.52.3",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -36,14 +36,14 @@
36
36
  "typescript": "^5.5.4",
37
37
  "undici-oidc-interceptor": "^0.5.0",
38
38
  "why-is-node-running": "^2.2.2",
39
- "@platformatic/composer": "3.52.2",
40
- "@platformatic/gateway": "3.52.2",
41
- "@platformatic/db": "3.52.2",
42
- "@platformatic/node": "3.52.2",
43
- "@platformatic/service": "3.52.2",
44
- "@platformatic/sql-graphql": "3.52.2",
45
- "@platformatic/sql-mapper": "3.52.2",
46
- "@platformatic/wattpm-pprof-capture": "3.52.2"
39
+ "@platformatic/composer": "3.52.3",
40
+ "@platformatic/db": "3.52.3",
41
+ "@platformatic/gateway": "3.52.3",
42
+ "@platformatic/node": "3.52.3",
43
+ "@platformatic/service": "3.52.3",
44
+ "@platformatic/sql-mapper": "3.52.3",
45
+ "@platformatic/sql-graphql": "3.52.3",
46
+ "@platformatic/wattpm-pprof-capture": "3.52.3"
47
47
  },
48
48
  "dependencies": {
49
49
  "@fastify/accepts": "^5.0.0",
@@ -73,12 +73,12 @@
73
73
  "undici": "^7.0.0",
74
74
  "undici-thread-interceptor": "^1.3.1",
75
75
  "ws": "^8.16.0",
76
- "@platformatic/basic": "3.52.2",
77
- "@platformatic/generators": "3.52.2",
78
- "@platformatic/foundation": "3.52.2",
79
- "@platformatic/metrics": "3.52.2",
80
- "@platformatic/itc": "3.52.2",
81
- "@platformatic/telemetry": "3.52.2"
76
+ "@platformatic/basic": "3.52.3",
77
+ "@platformatic/foundation": "3.52.3",
78
+ "@platformatic/generators": "3.52.3",
79
+ "@platformatic/metrics": "3.52.3",
80
+ "@platformatic/telemetry": "3.52.3",
81
+ "@platformatic/itc": "3.52.3"
82
82
  },
83
83
  "engines": {
84
84
  "node": ">=22.19.0"
package/schema.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.52.2.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.52.3.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "title": "Platformatic Runtime Config",
5
5
  "type": "object",