@platformatic/runtime 2.69.0 → 2.70.1
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/config.d.ts +4 -3
- package/lib/errors.js +93 -22
- package/lib/runtime.js +433 -61
- package/lib/worker/interceptors.js +4 -2
- package/lib/worker/itc.js +21 -4
- package/lib/worker/messaging.js +186 -0
- package/lib/worker/round-robin-map.js +1 -1
- package/lib/worker/symbols.js +7 -1
- package/package.json +23 -23
- package/schema.json +66 -23
package/lib/runtime.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { ITC } = require('@platformatic/itc')
|
|
4
|
-
const { features, ensureLoggableError, executeWithTimeout, deepmerge } = require('@platformatic/utils')
|
|
4
|
+
const { features, ensureLoggableError, executeWithTimeout, deepmerge, parseMemorySize, kTimeout } = require('@platformatic/utils')
|
|
5
5
|
const { once, EventEmitter } = require('node:events')
|
|
6
6
|
const { createReadStream, watch, existsSync } = require('node:fs')
|
|
7
7
|
const { readdir, readFile, stat, access } = require('node:fs/promises')
|
|
@@ -33,7 +33,9 @@ const {
|
|
|
33
33
|
kHealthCheckTimer,
|
|
34
34
|
kConfig,
|
|
35
35
|
kWorkerStatus,
|
|
36
|
-
kStderrMarker
|
|
36
|
+
kStderrMarker,
|
|
37
|
+
kLastELU,
|
|
38
|
+
kWorkersBroadcast
|
|
37
39
|
} = require('./worker/symbols')
|
|
38
40
|
|
|
39
41
|
const fastify = require('fastify')
|
|
@@ -74,6 +76,8 @@ class Runtime extends EventEmitter {
|
|
|
74
76
|
#prometheusServer
|
|
75
77
|
#inspectorServer
|
|
76
78
|
#workers
|
|
79
|
+
#workersBroadcastChannel
|
|
80
|
+
#workerITCHandlers
|
|
77
81
|
#restartingWorkers
|
|
78
82
|
#sharedHttpCache
|
|
79
83
|
servicesConfigsPatches
|
|
@@ -106,6 +110,18 @@ class Runtime extends EventEmitter {
|
|
|
106
110
|
stderr: new SonicBoom({ fd: process.stderr.fd })
|
|
107
111
|
}
|
|
108
112
|
}
|
|
113
|
+
|
|
114
|
+
this.#workerITCHandlers = {
|
|
115
|
+
getServiceMeta: this.getServiceMeta.bind(this),
|
|
116
|
+
listServices: () => this.#servicesIds,
|
|
117
|
+
getServices: this.getServices.bind(this),
|
|
118
|
+
getWorkers: this.getWorkers.bind(this),
|
|
119
|
+
getWorkerMessagingChannel: this.#getWorkerMessagingChannel.bind(this),
|
|
120
|
+
getHttpCacheValue: this.#getHttpCacheValue.bind(this),
|
|
121
|
+
setHttpCacheValue: this.#setHttpCacheValue.bind(this),
|
|
122
|
+
deleteHttpCacheValue: this.#deleteHttpCacheValue.bind(this),
|
|
123
|
+
invalidateHttpCache: this.invalidateHttpCache.bind(this)
|
|
124
|
+
}
|
|
109
125
|
}
|
|
110
126
|
|
|
111
127
|
async init () {
|
|
@@ -130,12 +146,15 @@ class Runtime extends EventEmitter {
|
|
|
130
146
|
|
|
131
147
|
this.#isProduction = this.#configManager.args?.production ?? false
|
|
132
148
|
this.#servicesIds = config.services.map(service => service.id)
|
|
149
|
+
this.#createWorkersBroadcastChannel()
|
|
133
150
|
|
|
134
151
|
const workersConfig = []
|
|
135
152
|
for (const service of config.services) {
|
|
136
153
|
const count = service.workers ?? this.#configManager.current.workers
|
|
137
154
|
if (count > 1 && service.entrypoint && !features.node.reusePort) {
|
|
138
|
-
this.logger.warn(
|
|
155
|
+
this.logger.warn(
|
|
156
|
+
`"${service.id}" is set as the entrypoint, but reusePort is not available in your OS; setting workers to 1 instead of ${count}`
|
|
157
|
+
)
|
|
139
158
|
workersConfig.push({ id: service.id, workers: 1 })
|
|
140
159
|
} else {
|
|
141
160
|
workersConfig.push({ id: service.id, workers: count })
|
|
@@ -225,6 +244,7 @@ class Runtime extends EventEmitter {
|
|
|
225
244
|
throw new errors.MissingEntrypointError()
|
|
226
245
|
}
|
|
227
246
|
this.#updateStatus('starting')
|
|
247
|
+
this.#createWorkersBroadcastChannel()
|
|
228
248
|
|
|
229
249
|
// Important: do not use Promise.all here since it won't properly manage dependencies
|
|
230
250
|
try {
|
|
@@ -311,6 +331,7 @@ class Runtime extends EventEmitter {
|
|
|
311
331
|
}
|
|
312
332
|
|
|
313
333
|
await this.#meshInterceptor.close()
|
|
334
|
+
this.#workersBroadcastChannel?.close()
|
|
314
335
|
|
|
315
336
|
this.#updateStatus('stopped')
|
|
316
337
|
}
|
|
@@ -1148,10 +1169,13 @@ class Runtime extends EventEmitter {
|
|
|
1148
1169
|
workerEnv['NODE_OPTIONS'] = `${originalNodeOptions} ${serviceConfig.nodeOptions}`.trim()
|
|
1149
1170
|
}
|
|
1150
1171
|
|
|
1172
|
+
const maxHeapTotal = typeof health.maxHeapTotal === 'string' ? parseMemorySize(health.maxHeapTotal) : health.maxHeapTotal
|
|
1173
|
+
const maxYoungGeneration = typeof health.maxYoungGeneration === 'string' ? parseMemorySize(health.maxYoungGeneration) : health.maxYoungGeneration
|
|
1174
|
+
|
|
1151
1175
|
const maxOldGenerationSizeMb = Math.floor(
|
|
1152
|
-
(
|
|
1176
|
+
(maxYoungGeneration > 0 ? maxHeapTotal - maxYoungGeneration : maxHeapTotal) / (1024 * 1024)
|
|
1153
1177
|
)
|
|
1154
|
-
const maxYoungGenerationSizeMb =
|
|
1178
|
+
const maxYoungGenerationSizeMb = maxYoungGeneration ? Math.floor(maxYoungGeneration / (1024 * 1024)) : undefined
|
|
1155
1179
|
|
|
1156
1180
|
const worker = new Worker(kWorkerFile, {
|
|
1157
1181
|
workerData: {
|
|
@@ -1207,6 +1231,8 @@ class Runtime extends EventEmitter {
|
|
|
1207
1231
|
setImmediate(() => {
|
|
1208
1232
|
if (started && (!config.watch || code !== 0)) {
|
|
1209
1233
|
this.emit('service:worker:error', { ...eventPayload, code })
|
|
1234
|
+
this.#broadcastWorkers()
|
|
1235
|
+
|
|
1210
1236
|
this.logger.warn(`The ${errorLabel} unexpectedly exited with code ${code}.`)
|
|
1211
1237
|
}
|
|
1212
1238
|
|
|
@@ -1250,14 +1276,7 @@ class Runtime extends EventEmitter {
|
|
|
1250
1276
|
name: workerId + '-runtime',
|
|
1251
1277
|
port: worker,
|
|
1252
1278
|
handlers: {
|
|
1253
|
-
|
|
1254
|
-
listServices: () => this.#servicesIds,
|
|
1255
|
-
getServices: this.getServices.bind(this),
|
|
1256
|
-
getWorkers: this.getWorkers.bind(this),
|
|
1257
|
-
getHttpCacheValue: this.#getHttpCacheValue.bind(this),
|
|
1258
|
-
setHttpCacheValue: this.#setHttpCacheValue.bind(this),
|
|
1259
|
-
deleteHttpCacheValue: this.#deleteHttpCacheValue.bind(this),
|
|
1260
|
-
invalidateHttpCache: this.invalidateHttpCache.bind(this),
|
|
1279
|
+
...this.#workerITCHandlers,
|
|
1261
1280
|
setEventsForwarding (value) {
|
|
1262
1281
|
worker[kForwardEvents] = value
|
|
1263
1282
|
}
|
|
@@ -1282,7 +1301,7 @@ class Runtime extends EventEmitter {
|
|
|
1282
1301
|
await this.startService(serviceId)
|
|
1283
1302
|
}
|
|
1284
1303
|
|
|
1285
|
-
this.logger?.info(`
|
|
1304
|
+
this.logger?.info(`The service "${serviceId}" has been successfully reloaded ...`)
|
|
1286
1305
|
|
|
1287
1306
|
if (serviceConfig.entrypoint) {
|
|
1288
1307
|
this.#showUrl()
|
|
@@ -1323,18 +1342,46 @@ class Runtime extends EventEmitter {
|
|
|
1323
1342
|
return worker
|
|
1324
1343
|
}
|
|
1325
1344
|
|
|
1326
|
-
#
|
|
1345
|
+
async #getHealth (worker) {
|
|
1346
|
+
if (features.node.worker.getHeapStatistics) {
|
|
1347
|
+
const { used_heap_size: heapUsed, total_heap_size: heapTotal } = await worker.getHeapStatistics()
|
|
1348
|
+
const currentELU = worker.performance.eventLoopUtilization()
|
|
1349
|
+
const elu = worker[kLastELU]
|
|
1350
|
+
? worker.performance.eventLoopUtilization(currentELU, worker[kLastELU])
|
|
1351
|
+
: currentELU
|
|
1352
|
+
worker[kLastELU] = currentELU
|
|
1353
|
+
return { elu: elu.utilization, heapUsed, heapTotal }
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
const health = await worker[kITC].send('getHealth')
|
|
1357
|
+
return health
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
#setupHealthCheck (config, serviceConfig, workersCount, id, index, worker, errorLabel, timeout) {
|
|
1327
1361
|
// Clear the timeout when exiting
|
|
1328
1362
|
worker.on('exit', () => clearTimeout(worker[kHealthCheckTimer]))
|
|
1329
1363
|
|
|
1330
1364
|
const { maxELU, maxHeapUsed, maxHeapTotal, maxUnhealthyChecks, interval } = worker[kConfig].health
|
|
1365
|
+
const maxHeapTotalNumber = typeof maxHeapTotal === 'string' ? parseMemorySize(maxHeapTotal) : maxHeapTotal
|
|
1366
|
+
|
|
1331
1367
|
let unhealthyChecks = 0
|
|
1332
1368
|
|
|
1333
1369
|
worker[kHealthCheckTimer] = setTimeout(async () => {
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1370
|
+
if (worker[kWorkerStatus] !== 'started') {
|
|
1371
|
+
return
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
let health, unhealthy, memoryUsage
|
|
1375
|
+
try {
|
|
1376
|
+
health = await this.#getHealth(worker)
|
|
1377
|
+
memoryUsage = health.heapUsed / maxHeapTotalNumber
|
|
1378
|
+
unhealthy = health.elu > maxELU || memoryUsage > maxHeapUsed
|
|
1379
|
+
} catch (err) {
|
|
1380
|
+
this.logger.error({ err }, `Failed to get health for ${errorLabel}.`)
|
|
1381
|
+
unhealthy = true
|
|
1382
|
+
memoryUsage = -1
|
|
1383
|
+
health = { elu: -1, heapUsed: -1, heapTotal: -1 }
|
|
1384
|
+
}
|
|
1338
1385
|
|
|
1339
1386
|
const serviceId = worker[kServiceId]
|
|
1340
1387
|
this.emit('health', {
|
|
@@ -1347,9 +1394,9 @@ class Runtime extends EventEmitter {
|
|
|
1347
1394
|
})
|
|
1348
1395
|
|
|
1349
1396
|
if (unhealthy) {
|
|
1350
|
-
if (elu > maxELU) {
|
|
1397
|
+
if (health.elu > maxELU) {
|
|
1351
1398
|
this.logger.error(
|
|
1352
|
-
`The ${errorLabel} has an ELU of ${(elu * 100).toFixed(2)} %, above the maximum allowed usage of ${(maxELU * 100).toFixed(2)} %.`
|
|
1399
|
+
`The ${errorLabel} has an ELU of ${(health.elu * 100).toFixed(2)} %, above the maximum allowed usage of ${(maxELU * 100).toFixed(2)} %.`
|
|
1353
1400
|
)
|
|
1354
1401
|
}
|
|
1355
1402
|
|
|
@@ -1367,14 +1414,14 @@ class Runtime extends EventEmitter {
|
|
|
1367
1414
|
if (unhealthyChecks === maxUnhealthyChecks) {
|
|
1368
1415
|
try {
|
|
1369
1416
|
this.logger.error(
|
|
1370
|
-
{ elu, maxELU, memoryUsage, maxMemoryUsage: maxHeapUsed },
|
|
1417
|
+
{ elu: health.elu, maxELU, memoryUsage: health.heapUsed, maxMemoryUsage: maxHeapUsed },
|
|
1371
1418
|
`The ${errorLabel} is unhealthy. Replacing it ...`
|
|
1372
1419
|
)
|
|
1373
1420
|
|
|
1374
|
-
await this.#replaceWorker(config, serviceConfig, workersCount, id, index,
|
|
1421
|
+
await this.#replaceWorker(config, serviceConfig, workersCount, id, index, worker)
|
|
1375
1422
|
} catch (e) {
|
|
1376
1423
|
this.logger.error(
|
|
1377
|
-
{ elu, maxELU, memoryUsage, maxMemoryUsage: maxHeapUsed },
|
|
1424
|
+
{ elu: health.elu, maxELU, memoryUsage: health.heapUsed, maxMemoryUsage: maxHeapUsed },
|
|
1378
1425
|
`Cannot replace the ${errorLabel}. Forcefully terminating it ...`
|
|
1379
1426
|
)
|
|
1380
1427
|
|
|
@@ -1383,7 +1430,7 @@ class Runtime extends EventEmitter {
|
|
|
1383
1430
|
} else {
|
|
1384
1431
|
worker[kHealthCheckTimer].refresh()
|
|
1385
1432
|
}
|
|
1386
|
-
}, interval)
|
|
1433
|
+
}, timeout ?? interval)
|
|
1387
1434
|
}
|
|
1388
1435
|
|
|
1389
1436
|
async #startWorker (
|
|
@@ -1397,7 +1444,6 @@ class Runtime extends EventEmitter {
|
|
|
1397
1444
|
worker = undefined,
|
|
1398
1445
|
disableRestartAttempts = false
|
|
1399
1446
|
) {
|
|
1400
|
-
const workerId = `${id}:${index}`
|
|
1401
1447
|
const label = this.#workerExtendedLabel(id, index, workersCount)
|
|
1402
1448
|
|
|
1403
1449
|
if (!silent) {
|
|
@@ -1424,7 +1470,7 @@ class Runtime extends EventEmitter {
|
|
|
1424
1470
|
if (config.startTimeout > 0) {
|
|
1425
1471
|
workerUrl = await executeWithTimeout(sendViaITC(worker, 'start'), config.startTimeout)
|
|
1426
1472
|
|
|
1427
|
-
if (workerUrl ===
|
|
1473
|
+
if (workerUrl === kTimeout) {
|
|
1428
1474
|
this.emit('service:worker:startTimeout', eventPayload)
|
|
1429
1475
|
this.logger.info(`The ${label} failed to start in ${config.startTimeout}ms. Forcefully killing the thread.`)
|
|
1430
1476
|
worker.terminate()
|
|
@@ -1442,6 +1488,7 @@ class Runtime extends EventEmitter {
|
|
|
1442
1488
|
|
|
1443
1489
|
worker[kWorkerStatus] = 'started'
|
|
1444
1490
|
this.emit('service:worker:started', eventPayload)
|
|
1491
|
+
this.#broadcastWorkers()
|
|
1445
1492
|
|
|
1446
1493
|
if (!silent) {
|
|
1447
1494
|
this.logger?.info(`Started the ${label}...`)
|
|
@@ -1449,12 +1496,9 @@ class Runtime extends EventEmitter {
|
|
|
1449
1496
|
|
|
1450
1497
|
const { enabled, gracePeriod } = worker[kConfig].health
|
|
1451
1498
|
if (enabled && config.restartOnError > 0) {
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
},
|
|
1456
|
-
gracePeriod > 0 ? gracePeriod : 1
|
|
1457
|
-
)
|
|
1499
|
+
// if gracePeriod is 0, it will be set to 1 to start health checks immediately
|
|
1500
|
+
// however, the health event will start when the worker is started
|
|
1501
|
+
this.#setupHealthCheck(config, serviceConfig, workersCount, id, index, worker, label, gracePeriod > 0 ? gracePeriod : 1)
|
|
1458
1502
|
}
|
|
1459
1503
|
} catch (error) {
|
|
1460
1504
|
// TODO: handle port allocation error here
|
|
@@ -1545,7 +1589,7 @@ class Runtime extends EventEmitter {
|
|
|
1545
1589
|
const res = await executeWithTimeout(exitPromise, exitTimeout)
|
|
1546
1590
|
|
|
1547
1591
|
// If the worker didn't exit in time, kill it
|
|
1548
|
-
if (res ===
|
|
1592
|
+
if (res === kTimeout) {
|
|
1549
1593
|
this.emit('service:worker:exit:timeout', eventPayload)
|
|
1550
1594
|
await worker.terminate()
|
|
1551
1595
|
}
|
|
@@ -1554,6 +1598,7 @@ class Runtime extends EventEmitter {
|
|
|
1554
1598
|
|
|
1555
1599
|
worker[kWorkerStatus] = 'stopped'
|
|
1556
1600
|
this.emit('service:worker:stopped', eventPayload)
|
|
1601
|
+
this.#broadcastWorkers()
|
|
1557
1602
|
}
|
|
1558
1603
|
|
|
1559
1604
|
#cleanupWorker (worker) {
|
|
@@ -1603,6 +1648,9 @@ class Runtime extends EventEmitter {
|
|
|
1603
1648
|
await this.#setupWorker(config, serviceConfig, workersCount, id, index)
|
|
1604
1649
|
await this.#startWorker(config, serviceConfig, workersCount, id, index, silent, bootstrapAttempt)
|
|
1605
1650
|
|
|
1651
|
+
this.logger.info(
|
|
1652
|
+
`The ${this.#workerExtendedLabel(id, index, workersCount)} has been successfully restarted ...`
|
|
1653
|
+
)
|
|
1606
1654
|
resolve()
|
|
1607
1655
|
} catch (err) {
|
|
1608
1656
|
// The runtime was stopped while the restart was happening, ignore any error.
|
|
@@ -1625,7 +1673,8 @@ class Runtime extends EventEmitter {
|
|
|
1625
1673
|
await restartPromise
|
|
1626
1674
|
}
|
|
1627
1675
|
|
|
1628
|
-
async #replaceWorker (config, serviceConfig, workersCount, serviceId, index,
|
|
1676
|
+
async #replaceWorker (config, serviceConfig, workersCount, serviceId, index, worker) {
|
|
1677
|
+
const workerId = `${serviceId}:${index}`
|
|
1629
1678
|
let newWorker
|
|
1630
1679
|
|
|
1631
1680
|
try {
|
|
@@ -1685,7 +1734,15 @@ class Runtime extends EventEmitter {
|
|
|
1685
1734
|
return null
|
|
1686
1735
|
}
|
|
1687
1736
|
|
|
1688
|
-
|
|
1737
|
+
if (this.#servicesIds.includes(serviceId)) {
|
|
1738
|
+
const availableWorkers = Array.from(this.#workers.keys())
|
|
1739
|
+
.filter(key => key.startsWith(serviceId + ':'))
|
|
1740
|
+
.map(key => key.split(':')[1])
|
|
1741
|
+
.join(', ')
|
|
1742
|
+
throw new errors.WorkerNotFoundError(workerId, serviceId, availableWorkers)
|
|
1743
|
+
} else {
|
|
1744
|
+
throw new errors.ServiceNotFoundError(serviceId, Array.from(this.#servicesIds).join(', '))
|
|
1745
|
+
}
|
|
1689
1746
|
}
|
|
1690
1747
|
|
|
1691
1748
|
if (ensureStarted) {
|
|
@@ -1699,6 +1756,59 @@ class Runtime extends EventEmitter {
|
|
|
1699
1756
|
return worker
|
|
1700
1757
|
}
|
|
1701
1758
|
|
|
1759
|
+
async #createWorkersBroadcastChannel () {
|
|
1760
|
+
this.#workersBroadcastChannel?.close()
|
|
1761
|
+
this.#workersBroadcastChannel = new BroadcastChannel(kWorkersBroadcast)
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
async #broadcastWorkers () {
|
|
1765
|
+
const workers = new Map()
|
|
1766
|
+
|
|
1767
|
+
// Create the list of workers
|
|
1768
|
+
for (const worker of this.#workers.values()) {
|
|
1769
|
+
if (worker[kWorkerStatus] !== 'started') {
|
|
1770
|
+
continue
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
const service = worker[kServiceId]
|
|
1774
|
+
let serviceWorkers = workers.get(service)
|
|
1775
|
+
|
|
1776
|
+
if (!serviceWorkers) {
|
|
1777
|
+
serviceWorkers = []
|
|
1778
|
+
workers.set(service, serviceWorkers)
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
serviceWorkers.push({
|
|
1782
|
+
id: worker[kId],
|
|
1783
|
+
service: worker[kServiceId],
|
|
1784
|
+
worker: worker[kWorkerId],
|
|
1785
|
+
thread: worker.threadId
|
|
1786
|
+
})
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
this.#workersBroadcastChannel.postMessage(workers)
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
async #getWorkerMessagingChannel ({ service, worker }, context) {
|
|
1793
|
+
const target = await this.#getWorkerById(service, worker, true, true)
|
|
1794
|
+
|
|
1795
|
+
const { port1, port2 } = new MessageChannel()
|
|
1796
|
+
|
|
1797
|
+
// Send the first port to the target
|
|
1798
|
+
const response = await executeWithTimeout(
|
|
1799
|
+
sendViaITC(target, 'saveMessagingChannel', port1, [port1]),
|
|
1800
|
+
this.#configManager.current.messagingTimeout
|
|
1801
|
+
)
|
|
1802
|
+
|
|
1803
|
+
if (response === kTimeout) {
|
|
1804
|
+
throw new errors.MessagingError(service, 'Timeout while establishing a communication channel.')
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
context.transferList = [port2]
|
|
1808
|
+
this.emit('service:worker:messagingChannel', { service, worker })
|
|
1809
|
+
return port2
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1702
1812
|
async #getRuntimePackageJson () {
|
|
1703
1813
|
const runtimeDir = this.#configManager.dirname
|
|
1704
1814
|
const packageJsonPath = join(runtimeDir, 'package.json')
|
|
@@ -1804,7 +1914,8 @@ class Runtime extends EventEmitter {
|
|
|
1804
1914
|
}
|
|
1805
1915
|
}
|
|
1806
1916
|
|
|
1807
|
-
const pinoLog =
|
|
1917
|
+
const pinoLog =
|
|
1918
|
+
typeof message?.level === 'number' && typeof message?.time === 'number' && typeof message?.msg === 'string'
|
|
1808
1919
|
|
|
1809
1920
|
// Directly write to the Pino destination
|
|
1810
1921
|
if (pinoLog) {
|
|
@@ -1849,10 +1960,16 @@ class Runtime extends EventEmitter {
|
|
|
1849
1960
|
|
|
1850
1961
|
async getServiceResourcesInfo (id) {
|
|
1851
1962
|
const workers = this.#workers.getCount(id)
|
|
1852
|
-
|
|
1963
|
+
|
|
1964
|
+
const worker = await this.#getWorkerById(id, 0, false, false)
|
|
1965
|
+
const health = worker[kConfig].health
|
|
1966
|
+
|
|
1967
|
+
return { workers, health }
|
|
1853
1968
|
}
|
|
1854
1969
|
|
|
1855
|
-
async #
|
|
1970
|
+
async #updateServiceConfigWorkers (serviceId, workers) {
|
|
1971
|
+
this.logger.info(`Updating service "${serviceId}" config workers to ${workers}`)
|
|
1972
|
+
|
|
1856
1973
|
this.#configManager.current.services.find(s => s.id === serviceId).workers = workers
|
|
1857
1974
|
const service = await this.#getServiceById(serviceId)
|
|
1858
1975
|
this.#workers.setCount(serviceId, workers)
|
|
@@ -1868,14 +1985,109 @@ class Runtime extends EventEmitter {
|
|
|
1868
1985
|
const results = await Promise.allSettled(promises)
|
|
1869
1986
|
for (const result of results) {
|
|
1870
1987
|
if (result.status === 'rejected') {
|
|
1988
|
+
this.logger.error({ err: result.reason }, `Cannot update service "${serviceId}" workers`)
|
|
1871
1989
|
throw result.reason
|
|
1872
1990
|
}
|
|
1873
1991
|
}
|
|
1874
1992
|
}
|
|
1875
1993
|
|
|
1994
|
+
async #updateServiceConfigHealth (serviceId, health) {
|
|
1995
|
+
this.logger.info(`Updating service "${serviceId}" config health heap to ${JSON.stringify(health)}`)
|
|
1996
|
+
const { maxHeapTotal, maxYoungGeneration } = health
|
|
1997
|
+
|
|
1998
|
+
const service = this.#configManager.current.services.find(s => s.id === serviceId)
|
|
1999
|
+
if (maxHeapTotal) {
|
|
2000
|
+
service.health.maxHeapTotal = maxHeapTotal
|
|
2001
|
+
}
|
|
2002
|
+
if (maxYoungGeneration) {
|
|
2003
|
+
service.health.maxYoungGeneration = maxYoungGeneration
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
/**
|
|
2008
|
+
* Updates the resources of the services, such as the number of workers and health configurations (e.g., heap memory settings).
|
|
2009
|
+
*
|
|
2010
|
+
* This function handles three update scenarios for each service:
|
|
2011
|
+
* 1. **Updating workers only**: Adjusts the number of workers for the service.
|
|
2012
|
+
* 2. **Updating health configurations only**: Updates health parameters like `maxHeapTotal` or `maxYoungGeneration`.
|
|
2013
|
+
* 3. **Updating both workers and health configurations**: Scales the workers and also applies health settings.
|
|
2014
|
+
*
|
|
2015
|
+
* When updating both workers and health:
|
|
2016
|
+
* - **Scaling down workers**: Stops extra workers, then restarts the remaining workers with the previous settings.
|
|
2017
|
+
* - **Scaling up workers**: Starts new workers with the updated heap settings, then restarts the old workers with the updated settings.
|
|
2018
|
+
*
|
|
2019
|
+
* Scaling up new resources (workers and/or heap memory) may fails due to insufficient memory, in this case the operation may fail partially or entirely.
|
|
2020
|
+
* Scaling down is expected to succeed without issues.
|
|
2021
|
+
*
|
|
2022
|
+
* @param {Array<Object>} updates - An array of objects that define the updates for each service.
|
|
2023
|
+
* @param {string} updates[].service - The ID of the service to update.
|
|
2024
|
+
* @param {number} [updates[].workers] - The desired number of workers for the service. If omitted, workers will not be updated.
|
|
2025
|
+
* @param {Object} [updates[].health] - The health configuration to update for the service, which may include:
|
|
2026
|
+
* @param {string|number} [updates[].health.maxHeapTotal] - The maximum heap memory for the service. Can be a valid memory string (e.g., '1G', '512MB') or a number representing bytes.
|
|
2027
|
+
* @param {string|number} [updates[].health.maxYoungGeneration] - The maximum young generation memory for the service. Can be a valid memory string (e.g., '128MB') or a number representing bytes.
|
|
2028
|
+
*
|
|
2029
|
+
* @returns {Promise<Array<Object>>} - A promise that resolves to an array of reports for each service, detailing the success or failure of the operations:
|
|
2030
|
+
* - `service`: The service ID.
|
|
2031
|
+
* - `workers`: The workers' update report, including the current, new number of workers, started workers, and success status.
|
|
2032
|
+
* - `health`: The health update report, showing the current and new heap settings, updated workers, and success status.
|
|
2033
|
+
*
|
|
2034
|
+
* @example
|
|
2035
|
+
* await runtime.updateServicesResources([
|
|
2036
|
+
* { service: 'service-1', workers: 2, health: { maxHeapTotal: '1G', maxYoungGeneration: '128 MB' } },
|
|
2037
|
+
* { service: 'service-2', health: { maxHeapTotal: '1G' } },
|
|
2038
|
+
* { service: 'service-3', workers: 2 },
|
|
2039
|
+
* ])
|
|
2040
|
+
*
|
|
2041
|
+
* In this example:
|
|
2042
|
+
* - `service-1` will have 2 workers and updated heap memory configurations.
|
|
2043
|
+
* - `service-2` will have updated heap memory settings (without changing workers).
|
|
2044
|
+
* - `service-3` will have its workers set to 2 but no change in memory settings.
|
|
2045
|
+
*
|
|
2046
|
+
* @throws {InvalidArgumentError} - Throws if any update parameter is invalid, such as:
|
|
2047
|
+
* - Missing service ID.
|
|
2048
|
+
* - Invalid worker count (not a positive integer).
|
|
2049
|
+
* - Invalid memory size format for `maxHeapTotal` or `maxYoungGeneration`.
|
|
2050
|
+
* @throws {ServiceNotFoundError} - Throws if the specified service ID does not exist in the current service configuration.
|
|
2051
|
+
*/
|
|
1876
2052
|
async updateServicesResources (updates) {
|
|
1877
|
-
if (this.#status === 'stopping' || this.#status === 'closed')
|
|
2053
|
+
if (this.#status === 'stopping' || this.#status === 'closed') {
|
|
2054
|
+
this.logger.warn('Cannot update service resources when the runtime is stopping or closed')
|
|
2055
|
+
return
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
const ups = await this.#validateUpdateServiceResources(updates)
|
|
2059
|
+
const config = this.#configManager.current
|
|
2060
|
+
|
|
2061
|
+
const report = []
|
|
2062
|
+
for (const update of ups) {
|
|
2063
|
+
const { serviceId, config: serviceConfig, workers, health, currentWorkers, currentHealth } = update
|
|
2064
|
+
|
|
2065
|
+
if (workers && health) {
|
|
2066
|
+
const r = await this.#updateServiceWorkersAndHealth(serviceId, config, serviceConfig, workers, health, currentWorkers, currentHealth)
|
|
2067
|
+
report.push({
|
|
2068
|
+
service: serviceId,
|
|
2069
|
+
workers: r.workers,
|
|
2070
|
+
health: r.health
|
|
2071
|
+
})
|
|
2072
|
+
} else if (health) {
|
|
2073
|
+
const r = await this.#updateServiceHealth(serviceId, config, serviceConfig, currentWorkers, currentHealth, health)
|
|
2074
|
+
report.push({
|
|
2075
|
+
service: serviceId,
|
|
2076
|
+
health: r.health
|
|
2077
|
+
})
|
|
2078
|
+
} else if (workers) {
|
|
2079
|
+
const r = await this.#updateServiceWorkers(serviceId, config, serviceConfig, workers, currentWorkers)
|
|
2080
|
+
report.push({
|
|
2081
|
+
service: serviceId,
|
|
2082
|
+
workers: r.workers
|
|
2083
|
+
})
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
return report
|
|
2088
|
+
}
|
|
1878
2089
|
|
|
2090
|
+
async #validateUpdateServiceResources (updates) {
|
|
1879
2091
|
if (!Array.isArray(updates)) {
|
|
1880
2092
|
throw new errors.InvalidArgumentError('updates', 'must be an array')
|
|
1881
2093
|
}
|
|
@@ -1884,47 +2096,207 @@ class Runtime extends EventEmitter {
|
|
|
1884
2096
|
}
|
|
1885
2097
|
|
|
1886
2098
|
const config = this.#configManager.current
|
|
1887
|
-
|
|
2099
|
+
const validatedUpdates = []
|
|
1888
2100
|
for (const update of updates) {
|
|
1889
|
-
const { service: serviceId
|
|
2101
|
+
const { service: serviceId } = update
|
|
1890
2102
|
|
|
1891
|
-
if (typeof workers !== 'number') {
|
|
1892
|
-
throw new errors.InvalidArgumentError('workers', 'must be a number')
|
|
1893
|
-
}
|
|
1894
2103
|
if (!serviceId) {
|
|
1895
2104
|
throw new errors.InvalidArgumentError('service', 'must be a string')
|
|
1896
2105
|
}
|
|
1897
|
-
if (workers <= 0) {
|
|
1898
|
-
throw new errors.InvalidArgumentError('workers', 'must be greater than 0')
|
|
1899
|
-
}
|
|
1900
|
-
if (workers > MAX_WORKERS) {
|
|
1901
|
-
throw new errors.InvalidArgumentError('workers', `must be less than ${MAX_WORKERS}`)
|
|
1902
|
-
}
|
|
1903
2106
|
const serviceConfig = config.services.find(s => s.id === serviceId)
|
|
1904
2107
|
if (!serviceConfig) {
|
|
1905
2108
|
throw new errors.ServiceNotFoundError(serviceId, Array.from(this.#servicesIds).join(', '))
|
|
1906
2109
|
}
|
|
1907
2110
|
|
|
1908
|
-
const { workers: currentWorkers } = await this.getServiceResourcesInfo(serviceId)
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
2111
|
+
const { workers: currentWorkers, health: currentHealth } = await this.getServiceResourcesInfo(serviceId)
|
|
2112
|
+
|
|
2113
|
+
let workers
|
|
2114
|
+
if (update.workers !== undefined) {
|
|
2115
|
+
if (typeof update.workers !== 'number') {
|
|
2116
|
+
throw new errors.InvalidArgumentError('workers', 'must be a number')
|
|
2117
|
+
}
|
|
2118
|
+
if (update.workers <= 0) {
|
|
2119
|
+
throw new errors.InvalidArgumentError('workers', 'must be greater than 0')
|
|
2120
|
+
}
|
|
2121
|
+
if (update.workers > MAX_WORKERS) {
|
|
2122
|
+
throw new errors.InvalidArgumentError('workers', `must be less than ${MAX_WORKERS}`)
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
if (currentWorkers === update.workers) {
|
|
2126
|
+
this.logger.warn({ serviceId, workers: update.workers }, 'No change in the number of workers for service')
|
|
2127
|
+
} else {
|
|
2128
|
+
workers = update.workers
|
|
2129
|
+
}
|
|
1912
2130
|
}
|
|
1913
2131
|
|
|
1914
|
-
|
|
1915
|
-
|
|
2132
|
+
let maxHeapTotal, maxYoungGeneration
|
|
2133
|
+
if (update.health) {
|
|
2134
|
+
if (update.health.maxHeapTotal !== undefined) {
|
|
2135
|
+
if (typeof update.health.maxHeapTotal === 'string') {
|
|
2136
|
+
try {
|
|
2137
|
+
maxHeapTotal = parseMemorySize(update.health.maxHeapTotal)
|
|
2138
|
+
} catch {
|
|
2139
|
+
throw new errors.InvalidArgumentError('maxHeapTotal', 'must be a valid memory size')
|
|
2140
|
+
}
|
|
2141
|
+
} else if (typeof update.health.maxHeapTotal === 'number') {
|
|
2142
|
+
maxHeapTotal = update.health.maxHeapTotal
|
|
2143
|
+
if (update.health.maxHeapTotal <= 0) {
|
|
2144
|
+
throw new errors.InvalidArgumentError('maxHeapTotal', 'must be greater than 0')
|
|
2145
|
+
}
|
|
2146
|
+
} else {
|
|
2147
|
+
throw new errors.InvalidArgumentError('maxHeapTotal', 'must be a number or a string representing a memory size')
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
if (currentHealth.maxHeapTotal === maxHeapTotal) {
|
|
2151
|
+
this.logger.warn({ serviceId, maxHeapTotal }, 'No change in the max heap total for service')
|
|
2152
|
+
maxHeapTotal = undefined
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
if (update.health.maxYoungGeneration !== undefined) {
|
|
2157
|
+
if (typeof update.health.maxYoungGeneration === 'string') {
|
|
2158
|
+
try {
|
|
2159
|
+
maxYoungGeneration = parseMemorySize(update.health.maxYoungGeneration)
|
|
2160
|
+
} catch {
|
|
2161
|
+
throw new errors.InvalidArgumentError('maxYoungGeneration', 'must be a valid memory size')
|
|
2162
|
+
}
|
|
2163
|
+
} else if (typeof update.health.maxYoungGeneration === 'number') {
|
|
2164
|
+
maxYoungGeneration = update.health.maxYoungGeneration
|
|
2165
|
+
if (update.health.maxYoungGeneration <= 0) {
|
|
2166
|
+
throw new errors.InvalidArgumentError('maxYoungGeneration', 'must be greater than 0')
|
|
2167
|
+
}
|
|
2168
|
+
} else {
|
|
2169
|
+
throw new errors.InvalidArgumentError('maxYoungGeneration', 'must be a number or a string representing a memory size')
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
if (currentHealth.maxYoungGeneration && currentHealth.maxYoungGeneration === maxYoungGeneration) {
|
|
2173
|
+
this.logger.warn({ serviceId, maxYoungGeneration }, 'No change in the max young generation for service')
|
|
2174
|
+
maxYoungGeneration = undefined
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
if (workers || maxHeapTotal || maxYoungGeneration) {
|
|
2180
|
+
let health
|
|
2181
|
+
if (maxHeapTotal || maxYoungGeneration) {
|
|
2182
|
+
health = { }
|
|
2183
|
+
if (maxHeapTotal) {
|
|
2184
|
+
health.maxHeapTotal = maxHeapTotal
|
|
2185
|
+
}
|
|
2186
|
+
if (maxYoungGeneration) {
|
|
2187
|
+
health.maxYoungGeneration = maxYoungGeneration
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
validatedUpdates.push({ serviceId, config: serviceConfig, workers, health, currentWorkers, currentHealth })
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
return validatedUpdates
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
async #updateServiceWorkersAndHealth (serviceId, config, serviceConfig, workers, health, currentWorkers, currentHealth) {
|
|
2198
|
+
if (currentWorkers > workers) {
|
|
2199
|
+
// stop workers
|
|
2200
|
+
const reportWorkers = await this.#updateServiceWorkers(serviceId, config, serviceConfig, workers, currentWorkers)
|
|
2201
|
+
// update heap for current workers
|
|
2202
|
+
const reportHealth = await this.#updateServiceHealth(serviceId, config, serviceConfig, workers, currentHealth, health)
|
|
2203
|
+
|
|
2204
|
+
return { workers: reportWorkers, health: reportHealth }
|
|
2205
|
+
} else {
|
|
2206
|
+
// update service heap
|
|
2207
|
+
await this.#updateServiceConfigHealth(serviceId, health)
|
|
2208
|
+
// start new workers with new heap
|
|
2209
|
+
const reportWorkers = await this.#updateServiceWorkers(serviceId, config, serviceConfig, workers, currentWorkers)
|
|
2210
|
+
// update heap for current workers
|
|
2211
|
+
const reportHealth = await this.#updateServiceHealth(serviceId, config, serviceConfig, currentWorkers, currentHealth, health, false)
|
|
2212
|
+
|
|
2213
|
+
return { workers: reportWorkers, health: reportHealth }
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
async #updateServiceHealth (serviceId, config, serviceConfig, currentWorkers, currentHealth, health, updateConfig = true) {
|
|
2218
|
+
const report = {
|
|
2219
|
+
current: currentHealth,
|
|
2220
|
+
new: health,
|
|
2221
|
+
updated: []
|
|
2222
|
+
}
|
|
2223
|
+
try {
|
|
2224
|
+
if (updateConfig) {
|
|
2225
|
+
await this.#updateServiceConfigHealth(serviceId, health)
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
for (let i = 0; i < currentWorkers; i++) {
|
|
2229
|
+
this.logger.info({ health: { current: currentHealth, new: health } }, `Restarting service "${serviceId}" worker ${i} to update config health heap...`)
|
|
2230
|
+
|
|
2231
|
+
const worker = await this.#getWorkerById(serviceId, i)
|
|
2232
|
+
if (health.maxHeapTotal) { worker[kConfig].health.maxHeapTotal = health.maxHeapTotal }
|
|
2233
|
+
if (health.maxYoungGeneration) { worker[kConfig].health.maxYoungGeneration = health.maxYoungGeneration }
|
|
2234
|
+
|
|
2235
|
+
await this.#replaceWorker(config, serviceConfig, currentWorkers, serviceId, i, worker)
|
|
2236
|
+
report.updated.push(i)
|
|
2237
|
+
this.logger.info({ health: { current: currentHealth, new: health } }, `Restarted service "${serviceId}" worker ${i}`)
|
|
2238
|
+
}
|
|
2239
|
+
report.success = true
|
|
2240
|
+
} catch (err) {
|
|
2241
|
+
if (report.updated.length < 1) {
|
|
2242
|
+
this.logger.error({ err }, 'Cannot update service health heap, no worker updated')
|
|
2243
|
+
await this.#updateServiceConfigHealth(serviceId, currentHealth)
|
|
2244
|
+
} else {
|
|
2245
|
+
this.logger.error({ err }, `Cannot update service health heap, updated workers: ${report.updated.length} out of ${currentWorkers}`)
|
|
2246
|
+
}
|
|
2247
|
+
report.success = false
|
|
2248
|
+
}
|
|
2249
|
+
return report
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
async #updateServiceWorkers (serviceId, config, serviceConfig, workers, currentWorkers) {
|
|
2253
|
+
const report = {
|
|
2254
|
+
current: currentWorkers,
|
|
2255
|
+
new: workers
|
|
2256
|
+
}
|
|
2257
|
+
if (currentWorkers < workers) {
|
|
2258
|
+
report.started = []
|
|
2259
|
+
try {
|
|
2260
|
+
await this.#updateServiceConfigWorkers(serviceId, workers)
|
|
1916
2261
|
for (let i = currentWorkers; i < workers; i++) {
|
|
1917
2262
|
await this.#setupWorker(config, serviceConfig, workers, serviceId, i)
|
|
1918
2263
|
await this.#startWorker(config, serviceConfig, workers, serviceId, i, false, 0)
|
|
2264
|
+
report.started.push(i)
|
|
1919
2265
|
}
|
|
1920
|
-
|
|
2266
|
+
report.success = true
|
|
2267
|
+
} catch (err) {
|
|
2268
|
+
if (report.started.length < 1) {
|
|
2269
|
+
this.logger.error({ err }, 'Cannot start service workers, no worker started')
|
|
2270
|
+
await this.#updateServiceConfigWorkers(serviceId, currentWorkers)
|
|
2271
|
+
} else {
|
|
2272
|
+
this.logger.error({ err }, `Cannot start service workers, started workers: ${report.started.length} out of ${workers}`)
|
|
2273
|
+
await this.#updateServiceConfigWorkers(serviceId, currentWorkers + report.started.length)
|
|
2274
|
+
}
|
|
2275
|
+
report.success = false
|
|
2276
|
+
}
|
|
2277
|
+
} else {
|
|
2278
|
+
// keep the current workers count until all the service workers are all stopped
|
|
2279
|
+
report.stopped = []
|
|
2280
|
+
try {
|
|
1921
2281
|
for (let i = currentWorkers - 1; i >= workers; i--) {
|
|
1922
|
-
|
|
1923
|
-
await
|
|
2282
|
+
const worker = await this.#getWorkerById(serviceId, i, false, false)
|
|
2283
|
+
await sendViaITC(worker, 'removeFromMesh')
|
|
2284
|
+
await this.#stopWorker(currentWorkers, serviceId, i, false, worker)
|
|
2285
|
+
report.stopped.push(i)
|
|
2286
|
+
}
|
|
2287
|
+
await this.#updateServiceConfigWorkers(serviceId, workers)
|
|
2288
|
+
report.success = true
|
|
2289
|
+
} catch (err) {
|
|
2290
|
+
if (report.stopped.length < 1) {
|
|
2291
|
+
this.logger.error({ err }, 'Cannot stop service workers, no worker stopped')
|
|
2292
|
+
} else {
|
|
2293
|
+
this.logger.error({ err }, `Cannot stop service workers, stopped workers: ${report.stopped.length} out of ${workers}`)
|
|
2294
|
+
await this.#updateServiceConfigWorkers(serviceId, currentWorkers - report.stopped)
|
|
1924
2295
|
}
|
|
1925
|
-
|
|
2296
|
+
report.success = false
|
|
1926
2297
|
}
|
|
1927
2298
|
}
|
|
2299
|
+
return report
|
|
1928
2300
|
}
|
|
1929
2301
|
}
|
|
1930
2302
|
|