@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 +0 -4
- package/lib/management-api.js +22 -15
- package/lib/runtime.js +98 -80
- package/lib/worker/symbols.js +0 -1
- package/package.json +15 -15
- package/schema.json +2 -3
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'
|
package/lib/management-api.js
CHANGED
|
@@ -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
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
socket.send(serializedMetrics + '\n')
|
|
268
|
-
}
|
|
274
|
+
// Poll every second
|
|
275
|
+
const pollingInterval = setInterval(pollAndSendMetrics, 1000)
|
|
269
276
|
|
|
270
|
-
|
|
277
|
+
// Send initial metrics immediately
|
|
278
|
+
await pollAndSendMetrics()
|
|
271
279
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
280
|
+
const cleanup = () => {
|
|
281
|
+
clearInterval(pollingInterval)
|
|
282
|
+
}
|
|
275
283
|
|
|
276
|
-
socket.on('
|
|
277
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
287
|
-
|
|
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
|
-
|
|
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
|
|
708
|
-
|
|
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
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
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
|
-
#
|
|
1650
|
-
//
|
|
1651
|
-
worker
|
|
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
|
-
|
|
1654
|
-
if
|
|
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
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
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
|
-
|
|
1678
|
-
|
|
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
|
package/lib/worker/symbols.js
CHANGED
|
@@ -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.
|
|
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/
|
|
39
|
-
"@platformatic/
|
|
40
|
-
"@platformatic/gateway": "3.
|
|
41
|
-
"@platformatic/
|
|
42
|
-
"@platformatic/
|
|
43
|
-
"@platformatic/sql-
|
|
44
|
-
"@platformatic/
|
|
45
|
-
"@platformatic/
|
|
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.
|
|
75
|
-
"@platformatic/
|
|
76
|
-
"@platformatic/
|
|
77
|
-
"@platformatic/
|
|
78
|
-
"@platformatic/metrics": "3.
|
|
79
|
-
"@platformatic/telemetry": "3.
|
|
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.
|
|
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": [
|