@platformatic/runtime 3.17.0 → 3.18.0

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/errors.js CHANGED
@@ -128,10 +128,6 @@ export const MissingPprofCapture = createError(
128
128
  'Please install @platformatic/wattpm-pprof-capture'
129
129
  )
130
130
 
131
- export const GetHeapStatisticUnavailable = createError(
132
- `${ERROR_PREFIX}_GET_HEAP_STATISTIC_UNAVAILABLE`,
133
- 'The getHeapStatistics method is not available in your Node version'
134
- )
135
131
  export const FailedToSendHealthSignalsError = createError(
136
132
  `${ERROR_PREFIX}_FAILED_TO_SEND_HEALTH_SIGNALS`,
137
133
  'Cannot send health signals from application "%s": %s'
@@ -238,6 +238,7 @@ export async function managementApiPlugin (app, opts) {
238
238
  return metrics
239
239
  })
240
240
 
241
+ // TODO: Remove in next major version - deprecated endpoint
241
242
  app.get('/metrics/live', { websocket: true }, async socket => {
242
243
  const config = await runtime.getRuntimeConfig()
243
244
 
@@ -256,26 +257,32 @@ export async function managementApiPlugin (app, opts) {
256
257
  return
257
258
  }
258
259
 
259
- const cachedMetrics = runtime.getCachedMetrics()
260
- if (cachedMetrics.length > 0) {
261
- const serializedMetrics = cachedMetrics.map(metric => JSON.stringify(metric)).join('\n')
262
- socket.send(serializedMetrics + '\n')
260
+ const pollAndSendMetrics = async () => {
261
+ try {
262
+ const metrics = await runtime.getFormattedMetrics()
263
+ if (metrics) {
264
+ const serializedMetrics = JSON.stringify(metrics)
265
+ socket.send(serializedMetrics + '\n')
266
+ }
267
+ } catch (error) {
268
+ // If there's an error, stop polling and close the connection
269
+ clearInterval(pollingInterval)
270
+ socket.close()
271
+ }
263
272
  }
264
273
 
265
- const eventHandler = metrics => {
266
- const serializedMetrics = JSON.stringify(metrics)
267
- socket.send(serializedMetrics + '\n')
268
- }
274
+ // Poll every second
275
+ const pollingInterval = setInterval(pollAndSendMetrics, 1000)
269
276
 
270
- runtime.on('metrics', eventHandler)
277
+ // Send initial metrics immediately
278
+ await pollAndSendMetrics()
271
279
 
272
- socket.on('error', () => {
273
- runtime.off('metrics', eventHandler)
274
- })
280
+ const cleanup = () => {
281
+ clearInterval(pollingInterval)
282
+ }
275
283
 
276
- socket.on('close', () => {
277
- runtime.off('metrics', eventHandler)
278
- })
284
+ socket.on('error', cleanup)
285
+ socket.on('close', cleanup)
279
286
  })
280
287
 
281
288
  app.get('/logs/live', { websocket: true }, async socket => {
package/lib/runtime.js CHANGED
@@ -31,13 +31,11 @@ import {
31
31
  ApplicationNotStartedError,
32
32
  ApplicationStartTimeoutError,
33
33
  CannotRemoveEntrypointError,
34
- GetHeapStatisticUnavailable,
35
34
  InvalidArgumentError,
36
35
  MessagingError,
37
36
  MissingEntrypointError,
38
37
  MissingPprofCapture,
39
38
  RuntimeAbortedError,
40
- RuntimeExitedError,
41
39
  WorkerNotFoundError
42
40
  } from './errors.js'
43
41
  import { abstractLogger, createLogger } from './logger.js'
@@ -55,7 +53,6 @@ import {
55
53
  kConfig,
56
54
  kFullId,
57
55
  kHealthCheckTimer,
58
- kHealthMetricsTimer,
59
56
  kId,
60
57
  kITC,
61
58
  kLastHealthCheckELU,
@@ -71,8 +68,6 @@ const kWorkerFile = join(import.meta.dirname, 'worker/main.js')
71
68
  const kInspectorOptions = Symbol('plt.runtime.worker.inspectorOptions')
72
69
 
73
70
  const MAX_LISTENERS_COUNT = 100
74
- const MAX_METRICS_QUEUE_LENGTH = 5 * 60 // 5 minutes in seconds
75
- const COLLECT_METRICS_TIMEOUT = 1000
76
71
 
77
72
  const MAX_CONCURRENCY = 5
78
73
  const MAX_BOOTSTRAP_ATTEMPTS = 5
@@ -98,8 +93,7 @@ export class Runtime extends EventEmitter {
98
93
  #entrypointId
99
94
  #url
100
95
 
101
- #metrics
102
- #metricsTimeout
96
+ #healthMetricsTimer
103
97
 
104
98
  #meshInterceptor
105
99
  #dispatcher
@@ -283,9 +277,8 @@ export class Runtime extends EventEmitter {
283
277
 
284
278
  this.#updateStatus('started')
285
279
 
286
- if (this.#config.metrics?.enabled !== false && typeof this.#metrics === 'undefined') {
287
- this.startCollectingMetrics()
288
- }
280
+ // Start the global health metrics timer for all workers if needed
281
+ this.#startHealthMetricsCollectionIfNeeded()
289
282
 
290
283
  await this.#dynamicWorkersScaler?.start()
291
284
  this.#showUrl()
@@ -339,7 +332,7 @@ export class Runtime extends EventEmitter {
339
332
  }
340
333
 
341
334
  async close (silent = false) {
342
- clearInterval(this.#metricsTimeout)
335
+ clearTimeout(this.#healthMetricsTimer)
343
336
 
344
337
  await this.stop(silent)
345
338
  this.#updateStatus('closing')
@@ -703,29 +696,15 @@ export class Runtime extends EventEmitter {
703
696
  }
704
697
  }
705
698
 
699
+ // TODO: Remove in next major version
706
700
  startCollectingMetrics () {
707
- this.#metrics = []
708
- this.#metricsTimeout = setInterval(async () => {
709
- if (this.#status !== 'started') {
710
- return
711
- }
712
-
713
- let metrics = null
714
- try {
715
- metrics = await this.getFormattedMetrics()
716
- } catch (error) {
717
- if (!(error instanceof RuntimeExitedError)) {
718
- this.logger.error({ err: ensureLoggableError(error) }, 'Error collecting metrics')
719
- }
720
- return
721
- }
701
+ this.logger.warn('startCollectingMetrics() is deprecated and no longer collects metrics. Metrics are now polled on-demand by the management API.')
702
+ }
722
703
 
723
- this.emitAndNotify('metrics', metrics)
724
- this.#metrics.push(metrics)
725
- if (this.#metrics.length > MAX_METRICS_QUEUE_LENGTH) {
726
- this.#metrics.shift()
727
- }
728
- }, COLLECT_METRICS_TIMEOUT).unref()
704
+ // TODO: Remove in next major version
705
+ getCachedMetrics () {
706
+ this.logger.warn('getCachedMetrics() is deprecated and returns an empty array. Metrics are no longer cached.')
707
+ return []
729
708
  }
730
709
 
731
710
  invalidateHttpCache (options = {}) {
@@ -1036,10 +1015,6 @@ export class Runtime extends EventEmitter {
1036
1015
  return { metrics }
1037
1016
  }
1038
1017
 
1039
- getCachedMetrics () {
1040
- return this.#metrics
1041
- }
1042
-
1043
1018
  async getFormattedMetrics () {
1044
1019
  try {
1045
1020
  const { metrics } = await this.getMetrics()
@@ -1280,10 +1255,6 @@ export class Runtime extends EventEmitter {
1280
1255
  }
1281
1256
 
1282
1257
  async getWorkerHealth (worker, options = {}) {
1283
- if (!features.node.worker.getHeapStatistics) {
1284
- throw new GetHeapStatisticUnavailable()
1285
- }
1286
-
1287
1258
  const currentELU = worker.performance.eventLoopUtilization()
1288
1259
  const previousELU = options.previousELU
1289
1260
 
@@ -1292,6 +1263,10 @@ export class Runtime extends EventEmitter {
1292
1263
  elu = worker.performance.eventLoopUtilization(elu, previousELU)
1293
1264
  }
1294
1265
 
1266
+ if (!features.node.worker.getHeapStatistics) {
1267
+ return { elu: elu.utilization, currentELU }
1268
+ }
1269
+
1295
1270
  const { used_heap_size: heapUsed, total_heap_size: heapTotal } = await worker.getHeapStatistics()
1296
1271
  return { elu: elu.utilization, heapUsed, heapTotal, currentELU }
1297
1272
  }
@@ -1460,17 +1435,26 @@ export class Runtime extends EventEmitter {
1460
1435
  workerEnv.NODE_OPTIONS = `${originalNodeOptions} ${applicationConfig.nodeOptions}`.trim()
1461
1436
  }
1462
1437
 
1463
- const maxHeapTotal =
1464
- typeof health.maxHeapTotal === 'string' ? parseMemorySize(health.maxHeapTotal) : health.maxHeapTotal
1465
- const maxYoungGeneration =
1466
- typeof health.maxYoungGeneration === 'string'
1467
- ? parseMemorySize(health.maxYoungGeneration)
1468
- : health.maxYoungGeneration
1438
+ let resourceLimits
1469
1439
 
1470
- const maxOldGenerationSizeMb = Math.floor(
1471
- (maxYoungGeneration > 0 ? maxHeapTotal - maxYoungGeneration : maxHeapTotal) / (1024 * 1024)
1472
- )
1473
- const maxYoungGenerationSizeMb = maxYoungGeneration ? Math.floor(maxYoungGeneration / (1024 * 1024)) : undefined
1440
+ {
1441
+ const maxHeapTotal =
1442
+ typeof health.maxHeapTotal === 'string' ? parseMemorySize(health.maxHeapTotal) : health.maxHeapTotal
1443
+ const maxYoungGeneration =
1444
+ typeof health.maxYoungGeneration === 'string'
1445
+ ? parseMemorySize(health.maxYoungGeneration)
1446
+ : health.maxYoungGeneration
1447
+
1448
+ const maxOldGenerationSizeMb = maxHeapTotal ? Math.floor((maxYoungGeneration > 0 ? maxHeapTotal - maxYoungGeneration : maxHeapTotal) / (1024 * 1024)) : undefined
1449
+ const maxYoungGenerationSizeMb = maxYoungGeneration ? Math.floor(maxYoungGeneration / (1024 * 1024)) : undefined
1450
+
1451
+ if (maxOldGenerationSizeMb || maxYoungGenerationSizeMb) {
1452
+ resourceLimits = {
1453
+ maxOldGenerationSizeMb,
1454
+ maxYoungGenerationSizeMb
1455
+ }
1456
+ }
1457
+ }
1474
1458
 
1475
1459
  const worker = new Worker(kWorkerFile, {
1476
1460
  workerData: {
@@ -1494,10 +1478,7 @@ export class Runtime extends EventEmitter {
1494
1478
  argv: applicationConfig.arguments,
1495
1479
  execArgv,
1496
1480
  env: workerEnv,
1497
- resourceLimits: {
1498
- maxOldGenerationSizeMb,
1499
- maxYoungGenerationSizeMb
1500
- },
1481
+ resourceLimits,
1501
1482
  stdout: true,
1502
1483
  stderr: true
1503
1484
  })
@@ -1646,36 +1627,75 @@ export class Runtime extends EventEmitter {
1646
1627
  return worker
1647
1628
  }
1648
1629
 
1649
- #setupHealthMetrics (id, index, worker, errorLabel) {
1650
- // Clear the timeout when exiting
1651
- worker.on('exit', () => clearTimeout(worker[kHealthMetricsTimer]))
1630
+ #startHealthMetricsCollectionIfNeeded () {
1631
+ // Need health metrics if dynamic workers scaler exists (for vertical scaling)
1632
+ // or if any worker has health checks enabled
1633
+ let needsHealthMetrics = !!this.#dynamicWorkersScaler
1652
1634
 
1653
- worker[kHealthMetricsTimer] = setTimeout(async () => {
1654
- if (worker[kWorkerStatus] !== 'started') return
1635
+ if (!needsHealthMetrics) {
1636
+ // Check if any worker has health checks enabled
1637
+ for (const worker of this.#workers.values()) {
1638
+ const healthConfig = worker[kConfig]?.health
1639
+ if (healthConfig?.enabled && this.#config.restartOnError > 0) {
1640
+ needsHealthMetrics = true
1641
+ break
1642
+ }
1643
+ }
1644
+ }
1655
1645
 
1656
- let health = null
1657
- try {
1658
- health = await this.getWorkerHealth(worker, {
1659
- previousELU: worker[kLastHealthCheckELU]
1660
- })
1661
- } catch (err) {
1662
- this.logger.error({ err }, `Failed to get health for ${errorLabel}.`)
1663
- } finally {
1664
- worker[kLastHealthCheckELU] = health?.currentELU ?? null
1646
+ if (needsHealthMetrics) {
1647
+ this.#startHealthMetricsCollection()
1648
+ }
1649
+ }
1650
+
1651
+ #startHealthMetricsCollection () {
1652
+ const collectHealthMetrics = async () => {
1653
+ if (this.#status !== 'started') {
1654
+ return
1665
1655
  }
1666
1656
 
1667
- const healthSignals = worker[kWorkerHealthSignals]?.getAll() ?? []
1657
+ // Iterate through all workers and collect health metrics
1658
+ for (const worker of this.#workers.values()) {
1659
+ if (worker[kWorkerStatus] !== 'started') {
1660
+ continue
1661
+ }
1668
1662
 
1669
- this.emitAndNotify('application:worker:health:metrics', {
1670
- id: worker[kId],
1671
- application: id,
1672
- worker: index,
1673
- currentHealth: health,
1674
- healthSignals
1675
- })
1663
+ const id = worker[kApplicationId]
1664
+ const index = worker[kWorkerId]
1665
+ const errorLabel = this.#workerExtendedLabel(id, index, worker[kConfig].workers)
1676
1666
 
1677
- worker[kHealthMetricsTimer].refresh()
1678
- }, 1000).unref()
1667
+ let health = null
1668
+ try {
1669
+ health = await this.getWorkerHealth(worker, {
1670
+ previousELU: worker[kLastHealthCheckELU]
1671
+ })
1672
+ } catch (err) {
1673
+ this.logger.error({ err }, `Failed to get health for ${errorLabel}.`)
1674
+ } finally {
1675
+ worker[kLastHealthCheckELU] = health?.currentELU ?? null
1676
+ }
1677
+
1678
+ const healthSignals = worker[kWorkerHealthSignals]?.getAll() ?? []
1679
+
1680
+ // We use emit instead of emitAndNotify to avoid sending a postMessages
1681
+ // to each workers even if they are not interested in health metrics.
1682
+ // No one of the known capabilities use this event yet.
1683
+ this.emit('application:worker:health:metrics', {
1684
+ id: worker[kId],
1685
+ application: id,
1686
+ worker: index,
1687
+ currentHealth: health,
1688
+ healthSignals
1689
+ })
1690
+ }
1691
+
1692
+ // Reschedule the next check. We are not using .refresh() because it's more
1693
+ // expensive (weird).
1694
+ this.#healthMetricsTimer = setTimeout(collectHealthMetrics, 1000).unref()
1695
+ }
1696
+
1697
+ // Start the collection
1698
+ this.#healthMetricsTimer = setTimeout(collectHealthMetrics, 1000).unref()
1679
1699
  }
1680
1700
 
1681
1701
  #setupHealthCheck (config, applicationConfig, workersCount, id, index, worker, errorLabel) {
@@ -1842,8 +1862,6 @@ export class Runtime extends EventEmitter {
1842
1862
  this.logger.info(`Started the ${label}...`)
1843
1863
  }
1844
1864
 
1845
- this.#setupHealthMetrics(id, index, worker, label)
1846
-
1847
1865
  const { enabled, gracePeriod } = worker[kConfig].health
1848
1866
  if (enabled && config.restartOnError > 0) {
1849
1867
  // if gracePeriod is 0, it will be set to 1 to start health checks immediately
@@ -5,7 +5,6 @@ export const kApplicationId = Symbol.for('plt.runtime.application.id')
5
5
  export const kWorkerId = Symbol.for('plt.runtime.worker.id')
6
6
  export const kITC = Symbol.for('plt.runtime.itc')
7
7
  export const kHealthCheckTimer = Symbol.for('plt.runtime.worker.healthCheckTimer')
8
- export const kHealthMetricsTimer = Symbol.for('plt.runtime.worker.healthMetricsTimer')
9
8
  export const kWorkerStatus = Symbol('plt.runtime.worker.status')
10
9
  export const kWorkerHealthSignals = Symbol.for('plt.runtime.worker.healthSignals')
11
10
  export const kWorkerStartTime = Symbol.for('plt.runtime.worker.startTime')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "3.17.0",
3
+ "version": "3.18.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -35,14 +35,14 @@
35
35
  "typescript": "^5.5.4",
36
36
  "undici-oidc-interceptor": "^0.5.0",
37
37
  "why-is-node-running": "^2.2.2",
38
- "@platformatic/db": "3.17.0",
39
- "@platformatic/composer": "3.17.0",
40
- "@platformatic/gateway": "3.17.0",
41
- "@platformatic/node": "3.17.0",
42
- "@platformatic/service": "3.17.0",
43
- "@platformatic/sql-graphql": "3.17.0",
44
- "@platformatic/sql-mapper": "3.17.0",
45
- "@platformatic/wattpm-pprof-capture": "3.17.0"
38
+ "@platformatic/composer": "3.18.0",
39
+ "@platformatic/node": "3.18.0",
40
+ "@platformatic/gateway": "3.18.0",
41
+ "@platformatic/db": "3.18.0",
42
+ "@platformatic/sql-graphql": "3.18.0",
43
+ "@platformatic/sql-mapper": "3.18.0",
44
+ "@platformatic/wattpm-pprof-capture": "3.18.0",
45
+ "@platformatic/service": "3.18.0"
46
46
  },
47
47
  "dependencies": {
48
48
  "@fastify/accepts": "^5.0.0",
@@ -71,12 +71,12 @@
71
71
  "undici": "^7.0.0",
72
72
  "undici-thread-interceptor": "^0.15.0",
73
73
  "ws": "^8.16.0",
74
- "@platformatic/basic": "3.17.0",
75
- "@platformatic/itc": "3.17.0",
76
- "@platformatic/generators": "3.17.0",
77
- "@platformatic/foundation": "3.17.0",
78
- "@platformatic/metrics": "3.17.0",
79
- "@platformatic/telemetry": "3.17.0"
74
+ "@platformatic/basic": "3.18.0",
75
+ "@platformatic/generators": "3.18.0",
76
+ "@platformatic/foundation": "3.18.0",
77
+ "@platformatic/itc": "3.18.0",
78
+ "@platformatic/metrics": "3.18.0",
79
+ "@platformatic/telemetry": "3.18.0"
80
80
  },
81
81
  "engines": {
82
82
  "node": ">=22.19.0"
package/schema.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.17.0.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.18.0.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "title": "Platformatic Runtime Config",
5
5
  "type": "object",
@@ -1626,8 +1626,7 @@
1626
1626
  {
1627
1627
  "type": "string"
1628
1628
  }
1629
- ],
1630
- "default": 4294967296
1629
+ ]
1631
1630
  },
1632
1631
  "maxYoungGeneration": {
1633
1632
  "anyOf": [