@platformatic/runtime 3.12.0 → 3.13.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/index.d.ts +2 -1
- package/lib/errors.js +4 -0
- package/lib/prom-server.js +2 -4
- package/lib/runtime.js +128 -116
- package/lib/worker/controller.js +15 -6
- package/lib/worker/main.js +2 -5
- package/lib/worker/messaging.js +0 -6
- package/lib/worker/round-robin-map.js +39 -41
- package/lib/worker/symbols.js +2 -1
- package/package.json +15 -15
- package/schema.json +1 -1
package/index.d.ts
CHANGED
|
@@ -55,7 +55,8 @@ export module symbols {
|
|
|
55
55
|
export declare const kWorkerId: unique symbol
|
|
56
56
|
export declare const kITC: unique symbol
|
|
57
57
|
export declare const kHealthCheckTimer: unique symbol
|
|
58
|
-
export declare const
|
|
58
|
+
export declare const kLastHealthCheckELU: unique symbol
|
|
59
|
+
export declare const kLastVerticalScalerELU: unique symbol
|
|
59
60
|
export declare const kWorkerStatus: unique symbol
|
|
60
61
|
export declare const kStderrMarker: string
|
|
61
62
|
export declare const kInterceptors: unique symbol
|
package/lib/errors.js
CHANGED
|
@@ -127,3 +127,7 @@ export const MissingPprofCapture = createError(
|
|
|
127
127
|
`${ERROR_PREFIX}_MISSING_PPROF_CAPTURE`,
|
|
128
128
|
'Please install @platformatic/wattpm-pprof-capture'
|
|
129
129
|
)
|
|
130
|
+
export const GetHeapStatisticUnavailable = createError(
|
|
131
|
+
`${ERROR_PREFIX}_GET_HEAP_STATISTIC_UNAVAILABLE`,
|
|
132
|
+
'The getHeapStatistics method is not available in your Node version'
|
|
133
|
+
)
|
package/lib/prom-server.js
CHANGED
|
@@ -21,19 +21,17 @@ const DEFAULT_LIVENESS_FAIL_BODY = 'ERR'
|
|
|
21
21
|
|
|
22
22
|
async function checkReadiness (runtime) {
|
|
23
23
|
const workers = await runtime.getWorkers()
|
|
24
|
+
const applications = await runtime.getApplicationsIds()
|
|
24
25
|
|
|
25
26
|
// Make sure there is at least one started worker
|
|
26
|
-
const applications = new Set()
|
|
27
27
|
const started = new Set()
|
|
28
28
|
for (const worker of Object.values(workers)) {
|
|
29
|
-
applications.add(worker.application)
|
|
30
|
-
|
|
31
29
|
if (worker.status === 'started') {
|
|
32
30
|
started.add(worker.application)
|
|
33
31
|
}
|
|
34
32
|
}
|
|
35
33
|
|
|
36
|
-
if (started.size !== applications.
|
|
34
|
+
if (started.size !== applications.length) {
|
|
37
35
|
return { status: false }
|
|
38
36
|
}
|
|
39
37
|
|
package/lib/runtime.js
CHANGED
|
@@ -36,7 +36,8 @@ import {
|
|
|
36
36
|
MissingPprofCapture,
|
|
37
37
|
RuntimeAbortedError,
|
|
38
38
|
RuntimeExitedError,
|
|
39
|
-
WorkerNotFoundError
|
|
39
|
+
WorkerNotFoundError,
|
|
40
|
+
GetHeapStatisticUnavailable
|
|
40
41
|
} from './errors.js'
|
|
41
42
|
import { abstractLogger, createLogger } from './logger.js'
|
|
42
43
|
import { startManagementApi } from './management-api.js'
|
|
@@ -56,7 +57,8 @@ import {
|
|
|
56
57
|
kHealthCheckTimer,
|
|
57
58
|
kId,
|
|
58
59
|
kITC,
|
|
59
|
-
|
|
60
|
+
kLastHealthCheckELU,
|
|
61
|
+
kLastVerticalScalerELU,
|
|
60
62
|
kStderrMarker,
|
|
61
63
|
kWorkerId,
|
|
62
64
|
kWorkersBroadcast,
|
|
@@ -107,6 +109,7 @@ export class Runtime extends EventEmitter {
|
|
|
107
109
|
|
|
108
110
|
#applicationsConfigsPatches
|
|
109
111
|
#workers
|
|
112
|
+
#workersConfigs
|
|
110
113
|
#workersBroadcastChannel
|
|
111
114
|
#workerITCHandlers
|
|
112
115
|
#restartingWorkers
|
|
@@ -190,27 +193,24 @@ export class Runtime extends EventEmitter {
|
|
|
190
193
|
|
|
191
194
|
this.#createWorkersBroadcastChannel()
|
|
192
195
|
|
|
193
|
-
|
|
194
|
-
for (const application of config.applications) {
|
|
195
|
-
|
|
196
|
+
this.#workersConfigs = {}
|
|
197
|
+
for (const application of this.#config.applications) {
|
|
198
|
+
let count = application.workers ?? this.#config.workers ?? 1
|
|
196
199
|
if (count > 1 && application.entrypoint && !features.node.reusePort) {
|
|
197
200
|
this.logger.warn(
|
|
198
201
|
`"${application.id}" is set as the entrypoint, but reusePort is not available in your OS; setting workers to 1 instead of ${count}`
|
|
199
202
|
)
|
|
200
|
-
|
|
201
|
-
} else {
|
|
202
|
-
workersConfig.push({ id: application.id, workers: count })
|
|
203
|
+
count = 1
|
|
203
204
|
}
|
|
205
|
+
this.#workersConfigs[application.id] = { count }
|
|
204
206
|
}
|
|
205
207
|
|
|
206
|
-
this.#workers.configure(workersConfig)
|
|
207
|
-
|
|
208
208
|
if (this.#isProduction) {
|
|
209
|
-
this.#env
|
|
210
|
-
this.#env
|
|
209
|
+
this.#env.PLT_DEV = 'false'
|
|
210
|
+
this.#env.PLT_ENVIRONMENT = 'production'
|
|
211
211
|
} else {
|
|
212
|
-
this.#env
|
|
213
|
-
this.#env
|
|
212
|
+
this.#env.PLT_DEV = 'true'
|
|
213
|
+
this.#env.PLT_ENVIRONMENT = 'development'
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
await this.#setupApplications()
|
|
@@ -479,13 +479,6 @@ export class Runtime extends EventEmitter {
|
|
|
479
479
|
}
|
|
480
480
|
|
|
481
481
|
async startApplication (id, silent = false) {
|
|
482
|
-
// Since when an application is stopped the worker is deleted, we consider an application start if its first application
|
|
483
|
-
// is no longer in the init phase
|
|
484
|
-
const firstWorker = this.#workers.get(`${id}:0`)
|
|
485
|
-
if (firstWorker && firstWorker[kWorkerStatus] !== 'boot' && firstWorker[kWorkerStatus] !== 'init') {
|
|
486
|
-
throw new ApplicationAlreadyStartedError()
|
|
487
|
-
}
|
|
488
|
-
|
|
489
482
|
const config = this.#config
|
|
490
483
|
const applicationConfig = config.applications.find(s => s.id === id)
|
|
491
484
|
|
|
@@ -493,12 +486,21 @@ export class Runtime extends EventEmitter {
|
|
|
493
486
|
throw new ApplicationNotFoundError(id, this.getApplicationsIds().join(', '))
|
|
494
487
|
}
|
|
495
488
|
|
|
496
|
-
const
|
|
489
|
+
const workersConfigs = this.#workersConfigs[id]
|
|
490
|
+
|
|
491
|
+
for (let i = 0; i < workersConfigs.count; i++) {
|
|
492
|
+
const worker = this.#workers.get(`${id}:${i}`)
|
|
493
|
+
const status = worker?.[kWorkerStatus]
|
|
494
|
+
|
|
495
|
+
if (status && status !== 'boot' && status !== 'init') {
|
|
496
|
+
throw new ApplicationAlreadyStartedError()
|
|
497
|
+
}
|
|
498
|
+
}
|
|
497
499
|
|
|
498
500
|
this.emitAndNotify('application:starting', id)
|
|
499
501
|
|
|
500
|
-
for (let i = 0; i <
|
|
501
|
-
await this.#startWorker(config, applicationConfig,
|
|
502
|
+
for (let i = 0; i < workersConfigs.count; i++) {
|
|
503
|
+
await this.#startWorker(config, applicationConfig, workersConfigs.count, id, i, silent)
|
|
502
504
|
}
|
|
503
505
|
|
|
504
506
|
this.emitAndNotify('application:started', id)
|
|
@@ -512,13 +514,15 @@ export class Runtime extends EventEmitter {
|
|
|
512
514
|
throw new ApplicationNotFoundError(id, this.getApplicationsIds().join(', '))
|
|
513
515
|
}
|
|
514
516
|
|
|
515
|
-
const
|
|
517
|
+
const workersIds = this.#workers.getKeys(id)
|
|
518
|
+
const workersCount = workersIds.length
|
|
516
519
|
|
|
517
520
|
this.emitAndNotify('application:stopping', id)
|
|
518
521
|
|
|
519
522
|
if (typeof workersCount === 'number') {
|
|
520
523
|
const stopInvocations = []
|
|
521
|
-
for (
|
|
524
|
+
for (const workerId of workersIds) {
|
|
525
|
+
const i = parseInt(workerId.split(':')[1])
|
|
522
526
|
stopInvocations.push([workersCount, id, i, silent, undefined, dependents])
|
|
523
527
|
}
|
|
524
528
|
|
|
@@ -531,13 +535,15 @@ export class Runtime extends EventEmitter {
|
|
|
531
535
|
async restartApplication (id) {
|
|
532
536
|
const config = this.#config
|
|
533
537
|
const applicationConfig = this.#config.applications.find(s => s.id === id)
|
|
534
|
-
|
|
538
|
+
|
|
539
|
+
const workersIds = await this.#workers.getKeys(id)
|
|
540
|
+
const workersCount = workersIds.length
|
|
535
541
|
|
|
536
542
|
this.emitAndNotify('application:restarting', id)
|
|
537
543
|
|
|
538
544
|
for (let i = 0; i < workersCount; i++) {
|
|
539
|
-
const
|
|
540
|
-
const worker = this.#workers.get(
|
|
545
|
+
const workerId = workersIds[i]
|
|
546
|
+
const worker = this.#workers.get(workerId)
|
|
541
547
|
|
|
542
548
|
if (i > 0 && config.workersRestartDelay > 0) {
|
|
543
549
|
await sleep(config.workersRestartDelay)
|
|
@@ -861,14 +867,11 @@ export class Runtime extends EventEmitter {
|
|
|
861
867
|
async getCustomHealthChecks () {
|
|
862
868
|
const status = {}
|
|
863
869
|
|
|
864
|
-
for (const
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
const worker = this.#workers.get(
|
|
868
|
-
|
|
869
|
-
if (worker) {
|
|
870
|
-
status[label] = await sendViaITC(worker, 'getCustomHealthCheck')
|
|
871
|
-
}
|
|
870
|
+
for (const application of this.#config.applications) {
|
|
871
|
+
const workersIds = this.#workers.getKeys(application.id)
|
|
872
|
+
for (const workerId of workersIds) {
|
|
873
|
+
const worker = this.#workers.get(workerId)
|
|
874
|
+
status[workerId] = await sendViaITC(worker, 'getCustomHealthCheck')
|
|
872
875
|
}
|
|
873
876
|
}
|
|
874
877
|
|
|
@@ -878,14 +881,11 @@ export class Runtime extends EventEmitter {
|
|
|
878
881
|
async getCustomReadinessChecks () {
|
|
879
882
|
const status = {}
|
|
880
883
|
|
|
881
|
-
for (const
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
const worker = this.#workers.get(
|
|
885
|
-
|
|
886
|
-
if (worker) {
|
|
887
|
-
status[label] = await sendViaITC(worker, 'getCustomReadinessCheck')
|
|
888
|
-
}
|
|
884
|
+
for (const application of this.#config.applications) {
|
|
885
|
+
const workersIds = this.#workers.getKeys(application.id)
|
|
886
|
+
for (const workerId of workersIds) {
|
|
887
|
+
const worker = this.#workers.get(workerId)
|
|
888
|
+
status[workerId] = await sendViaITC(worker, 'getCustomReadinessCheck')
|
|
889
889
|
}
|
|
890
890
|
}
|
|
891
891
|
|
|
@@ -1055,12 +1055,11 @@ export class Runtime extends EventEmitter {
|
|
|
1055
1055
|
}
|
|
1056
1056
|
|
|
1057
1057
|
async getApplicationResourcesInfo (id) {
|
|
1058
|
-
const
|
|
1059
|
-
|
|
1060
|
-
const worker = await this.#getWorkerById(id, 0, false, false)
|
|
1058
|
+
const workersCount = this.#workers.getKeys(id).length
|
|
1059
|
+
const worker = await this.#getWorkerByIdOrNext(id, 0, false, false)
|
|
1061
1060
|
const health = worker[kConfig].health
|
|
1062
1061
|
|
|
1063
|
-
return { workers, health }
|
|
1062
|
+
return { workers: workersCount, health }
|
|
1064
1063
|
}
|
|
1065
1064
|
|
|
1066
1065
|
getApplicationsIds () {
|
|
@@ -1080,17 +1079,13 @@ export class Runtime extends EventEmitter {
|
|
|
1080
1079
|
async getWorkers () {
|
|
1081
1080
|
const status = {}
|
|
1082
1081
|
|
|
1083
|
-
for (const [
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
status[
|
|
1089
|
-
|
|
1090
|
-
worker: i,
|
|
1091
|
-
status: worker?.[kWorkerStatus] ?? 'exited',
|
|
1092
|
-
thread: worker?.threadId
|
|
1093
|
-
}
|
|
1082
|
+
for (const [key, worker] of this.#workers.entries()) {
|
|
1083
|
+
const [application, index] = key.split(':')
|
|
1084
|
+
status[key] = {
|
|
1085
|
+
application,
|
|
1086
|
+
worker: index,
|
|
1087
|
+
status: worker[kWorkerStatus],
|
|
1088
|
+
thread: worker.threadId
|
|
1094
1089
|
}
|
|
1095
1090
|
}
|
|
1096
1091
|
|
|
@@ -1141,7 +1136,7 @@ export class Runtime extends EventEmitter {
|
|
|
1141
1136
|
}
|
|
1142
1137
|
|
|
1143
1138
|
if (this.#isProduction) {
|
|
1144
|
-
applicationDetails.workers = this.#workers.
|
|
1139
|
+
applicationDetails.workers = this.#workers.getKeys(id).length
|
|
1145
1140
|
}
|
|
1146
1141
|
|
|
1147
1142
|
if (entrypoint) {
|
|
@@ -1273,12 +1268,13 @@ export class Runtime extends EventEmitter {
|
|
|
1273
1268
|
}
|
|
1274
1269
|
|
|
1275
1270
|
const config = this.#config
|
|
1276
|
-
|
|
1271
|
+
|
|
1272
|
+
const workersConfigs = this.#workersConfigs[applicationConfig.id]
|
|
1277
1273
|
const id = applicationConfig.id
|
|
1278
1274
|
const setupInvocations = []
|
|
1279
1275
|
|
|
1280
|
-
for (let i = 0; i <
|
|
1281
|
-
setupInvocations.push([config, applicationConfig,
|
|
1276
|
+
for (let i = 0; i < workersConfigs.count; i++) {
|
|
1277
|
+
setupInvocations.push([config, applicationConfig, workersConfigs.count, id, i])
|
|
1282
1278
|
}
|
|
1283
1279
|
|
|
1284
1280
|
await executeInParallel(this.#setupWorker.bind(this), setupInvocations, this.#concurrency)
|
|
@@ -1344,9 +1340,9 @@ export class Runtime extends EventEmitter {
|
|
|
1344
1340
|
const workerEnv = structuredClone(this.#env)
|
|
1345
1341
|
|
|
1346
1342
|
if (applicationConfig.nodeOptions?.trim().length > 0) {
|
|
1347
|
-
const originalNodeOptions = workerEnv
|
|
1343
|
+
const originalNodeOptions = workerEnv.NODE_OPTIONS ?? ''
|
|
1348
1344
|
|
|
1349
|
-
workerEnv
|
|
1345
|
+
workerEnv.NODE_OPTIONS = `${originalNodeOptions} ${applicationConfig.nodeOptions}`.trim()
|
|
1350
1346
|
}
|
|
1351
1347
|
|
|
1352
1348
|
const maxHeapTotal =
|
|
@@ -1391,7 +1387,7 @@ export class Runtime extends EventEmitter {
|
|
|
1391
1387
|
stderr: true
|
|
1392
1388
|
})
|
|
1393
1389
|
|
|
1394
|
-
this.#handleWorkerStandardStreams(worker, applicationId,
|
|
1390
|
+
this.#handleWorkerStandardStreams(worker, applicationId, index)
|
|
1395
1391
|
|
|
1396
1392
|
// Make sure the listener can handle a lot of API requests at once before raising a warning
|
|
1397
1393
|
worker.setMaxListeners(1e3)
|
|
@@ -1445,10 +1441,10 @@ export class Runtime extends EventEmitter {
|
|
|
1445
1441
|
})
|
|
1446
1442
|
})
|
|
1447
1443
|
|
|
1448
|
-
worker[kId] =
|
|
1444
|
+
worker[kId] = workerId
|
|
1449
1445
|
worker[kFullId] = workerId
|
|
1450
1446
|
worker[kApplicationId] = applicationId
|
|
1451
|
-
worker[kWorkerId] =
|
|
1447
|
+
worker[kWorkerId] = index
|
|
1452
1448
|
worker[kWorkerStatus] = 'boot'
|
|
1453
1449
|
|
|
1454
1450
|
if (inspectorOptions) {
|
|
@@ -1527,17 +1523,21 @@ export class Runtime extends EventEmitter {
|
|
|
1527
1523
|
return worker
|
|
1528
1524
|
}
|
|
1529
1525
|
|
|
1530
|
-
async #getHealth (worker) {
|
|
1531
|
-
if (features.node.worker.getHeapStatistics) {
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1526
|
+
async #getHealth (worker, options = {}) {
|
|
1527
|
+
if (!features.node.worker.getHeapStatistics) {
|
|
1528
|
+
throw new GetHeapStatisticUnavailable()
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
const currentELU = worker.performance.eventLoopUtilization()
|
|
1532
|
+
const previousELU = options.previousELU
|
|
1533
|
+
|
|
1534
|
+
let elu = currentELU
|
|
1535
|
+
if (previousELU) {
|
|
1536
|
+
elu = worker.performance.eventLoopUtilization(elu, previousELU)
|
|
1537
1537
|
}
|
|
1538
1538
|
|
|
1539
|
-
const
|
|
1540
|
-
return
|
|
1539
|
+
const { used_heap_size: heapUsed, total_heap_size: heapTotal } = await worker.getHeapStatistics()
|
|
1540
|
+
return { elu: elu.utilization, heapUsed, heapTotal, currentELU }
|
|
1541
1541
|
}
|
|
1542
1542
|
|
|
1543
1543
|
#setupHealthCheck (config, applicationConfig, workersCount, id, index, worker, errorLabel) {
|
|
@@ -1556,11 +1556,15 @@ export class Runtime extends EventEmitter {
|
|
|
1556
1556
|
|
|
1557
1557
|
let health, unhealthy, memoryUsage
|
|
1558
1558
|
try {
|
|
1559
|
-
health = await this.#getHealth(worker
|
|
1559
|
+
health = await this.#getHealth(worker, {
|
|
1560
|
+
previousELU: worker[kLastHealthCheckELU]
|
|
1561
|
+
})
|
|
1562
|
+
worker[kLastHealthCheckELU] = health.currentELU
|
|
1560
1563
|
memoryUsage = health.heapUsed / maxHeapTotalNumber
|
|
1561
1564
|
unhealthy = health.elu > maxELU || memoryUsage > maxHeapUsed
|
|
1562
1565
|
} catch (err) {
|
|
1563
1566
|
this.logger.error({ err }, `Failed to get health for ${errorLabel}.`)
|
|
1567
|
+
worker[kLastHealthCheckELU] = null
|
|
1564
1568
|
unhealthy = true
|
|
1565
1569
|
memoryUsage = -1
|
|
1566
1570
|
health = { elu: -1, heapUsed: -1, heapTotal: -1 }
|
|
@@ -1635,7 +1639,7 @@ export class Runtime extends EventEmitter {
|
|
|
1635
1639
|
}
|
|
1636
1640
|
|
|
1637
1641
|
if (!worker) {
|
|
1638
|
-
worker = await this.#
|
|
1642
|
+
worker = await this.#getWorkerByIdOrNext(id, index, false, false)
|
|
1639
1643
|
}
|
|
1640
1644
|
|
|
1641
1645
|
const eventPayload = { application: id, worker: index, workersCount }
|
|
@@ -1643,7 +1647,7 @@ export class Runtime extends EventEmitter {
|
|
|
1643
1647
|
// The application was stopped, recreate the thread
|
|
1644
1648
|
if (!worker) {
|
|
1645
1649
|
await this.#setupApplication(applicationConfig, index)
|
|
1646
|
-
worker = await this.#
|
|
1650
|
+
worker = await this.#getWorkerByIdOrNext(id, index)
|
|
1647
1651
|
}
|
|
1648
1652
|
|
|
1649
1653
|
worker[kWorkerStatus] = 'starting'
|
|
@@ -1746,7 +1750,7 @@ export class Runtime extends EventEmitter {
|
|
|
1746
1750
|
|
|
1747
1751
|
async #stopWorker (workersCount, id, index, silent, worker, dependents) {
|
|
1748
1752
|
if (!worker) {
|
|
1749
|
-
worker = await this.#
|
|
1753
|
+
worker = await this.#getWorkerByIdOrNext(id, index, false, false)
|
|
1750
1754
|
}
|
|
1751
1755
|
|
|
1752
1756
|
if (!worker) {
|
|
@@ -1827,10 +1831,8 @@ export class Runtime extends EventEmitter {
|
|
|
1827
1831
|
return this.#cleanupWorker(worker)
|
|
1828
1832
|
}
|
|
1829
1833
|
|
|
1830
|
-
#workerExtendedLabel (applicationId, workerId,
|
|
1831
|
-
return
|
|
1832
|
-
? `worker ${workerId} of the application "${applicationId}"`
|
|
1833
|
-
: `application "${applicationId}"`
|
|
1834
|
+
#workerExtendedLabel (applicationId, workerId, _workersCount) {
|
|
1835
|
+
return `worker ${workerId} of the application "${applicationId}"`
|
|
1834
1836
|
}
|
|
1835
1837
|
|
|
1836
1838
|
async #restartCrashedWorker (config, applicationConfig, workersCount, id, index, silent, bootstrapAttempt) {
|
|
@@ -1924,7 +1926,6 @@ export class Runtime extends EventEmitter {
|
|
|
1924
1926
|
}
|
|
1925
1927
|
|
|
1926
1928
|
async #getApplicationById (applicationId, ensureStarted = false, mustExist = true) {
|
|
1927
|
-
// If the applicationId includes the worker, properly split
|
|
1928
1929
|
let workerId
|
|
1929
1930
|
const matched = applicationId.match(/^(.+):(\d+)$/)
|
|
1930
1931
|
|
|
@@ -1933,16 +1934,19 @@ export class Runtime extends EventEmitter {
|
|
|
1933
1934
|
workerId = matched[2]
|
|
1934
1935
|
}
|
|
1935
1936
|
|
|
1936
|
-
return this.#
|
|
1937
|
+
return this.#getWorkerByIdOrNext(applicationId, workerId, ensureStarted, mustExist)
|
|
1937
1938
|
}
|
|
1938
1939
|
|
|
1939
|
-
|
|
1940
|
+
// This method can work in two modes: when workerId is provided, it will return the specific worker
|
|
1941
|
+
// otherwise it will return the next available worker for the application.
|
|
1942
|
+
async #getWorkerByIdOrNext (applicationId, workerId, ensureStarted = false, mustExist = true) {
|
|
1940
1943
|
let worker
|
|
1941
1944
|
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
} else {
|
|
1945
|
+
// Note that in this class "== null" is purposely used instead of "===" to check for both null and undefined
|
|
1946
|
+
if (workerId == null) {
|
|
1945
1947
|
worker = this.#workers.next(applicationId)
|
|
1948
|
+
} else {
|
|
1949
|
+
worker = this.#workers.get(`${applicationId}:${workerId}`)
|
|
1946
1950
|
}
|
|
1947
1951
|
|
|
1948
1952
|
const applicationsIds = this.getApplicationsIds()
|
|
@@ -1953,8 +1957,8 @@ export class Runtime extends EventEmitter {
|
|
|
1953
1957
|
}
|
|
1954
1958
|
|
|
1955
1959
|
if (applicationsIds.includes(applicationId)) {
|
|
1956
|
-
const availableWorkers =
|
|
1957
|
-
.
|
|
1960
|
+
const availableWorkers = this.#workers
|
|
1961
|
+
.getKeys(applicationId)
|
|
1958
1962
|
.map(key => key.split(':')[1])
|
|
1959
1963
|
.join(', ')
|
|
1960
1964
|
throw new WorkerNotFoundError(workerId, applicationId, availableWorkers)
|
|
@@ -2019,7 +2023,7 @@ export class Runtime extends EventEmitter {
|
|
|
2019
2023
|
)
|
|
2020
2024
|
}
|
|
2021
2025
|
|
|
2022
|
-
const target = await this.#
|
|
2026
|
+
const target = await this.#getWorkerByIdOrNext(application, worker, true, true)
|
|
2023
2027
|
|
|
2024
2028
|
const { port1, port2 } = new MessageChannel()
|
|
2025
2029
|
|
|
@@ -2156,14 +2160,14 @@ export class Runtime extends EventEmitter {
|
|
|
2156
2160
|
|
|
2157
2161
|
this.#config.applications.find(s => s.id === applicationId).workers = workers
|
|
2158
2162
|
const application = await this.#getApplicationById(applicationId)
|
|
2159
|
-
this.#workers.setCount(applicationId, workers)
|
|
2160
2163
|
application[kConfig].workers = workers
|
|
2161
2164
|
|
|
2165
|
+
const workersIds = this.#workers.getKeys(applicationId)
|
|
2162
2166
|
const promises = []
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
}
|
|
2167
|
+
|
|
2168
|
+
for (const workerId of workersIds) {
|
|
2169
|
+
const worker = this.#workers.get(workerId)
|
|
2170
|
+
promises.push(sendViaITC(worker, 'updateWorkersCount', { applicationId, workers }))
|
|
2167
2171
|
}
|
|
2168
2172
|
|
|
2169
2173
|
const results = await Promise.allSettled(promises)
|
|
@@ -2391,7 +2395,7 @@ export class Runtime extends EventEmitter {
|
|
|
2391
2395
|
`Restarting application "${applicationId}" worker ${i} to update config health heap...`
|
|
2392
2396
|
)
|
|
2393
2397
|
|
|
2394
|
-
const worker = await this.#
|
|
2398
|
+
const worker = await this.#getWorkerByIdOrNext(applicationId, i)
|
|
2395
2399
|
if (health.maxHeapTotal) {
|
|
2396
2400
|
worker[kConfig].health.maxHeapTotal = health.maxHeapTotal
|
|
2397
2401
|
}
|
|
@@ -2423,30 +2427,29 @@ export class Runtime extends EventEmitter {
|
|
|
2423
2427
|
}
|
|
2424
2428
|
|
|
2425
2429
|
async #updateApplicationWorkers (applicationId, config, applicationConfig, workers, currentWorkers) {
|
|
2426
|
-
const report = {
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
+
const report = { current: currentWorkers, new: workers }
|
|
2431
|
+
|
|
2432
|
+
let startedWorkersCount = 0
|
|
2433
|
+
let stoppedWorkersCount = 0
|
|
2434
|
+
|
|
2430
2435
|
if (currentWorkers < workers) {
|
|
2431
2436
|
report.started = []
|
|
2432
2437
|
try {
|
|
2433
|
-
await this.#updateApplicationConfigWorkers(applicationId, workers)
|
|
2434
2438
|
for (let i = currentWorkers; i < workers; i++) {
|
|
2435
2439
|
await this.#setupWorker(config, applicationConfig, workers, applicationId, i)
|
|
2436
2440
|
await this.#startWorker(config, applicationConfig, workers, applicationId, i, false, 0)
|
|
2437
2441
|
report.started.push(i)
|
|
2442
|
+
startedWorkersCount++
|
|
2438
2443
|
}
|
|
2439
2444
|
report.success = true
|
|
2440
2445
|
} catch (err) {
|
|
2441
|
-
if (
|
|
2446
|
+
if (startedWorkersCount < 1) {
|
|
2442
2447
|
this.logger.error({ err }, 'Cannot start application workers, no worker started')
|
|
2443
|
-
await this.#updateApplicationConfigWorkers(applicationId, currentWorkers)
|
|
2444
2448
|
} else {
|
|
2445
2449
|
this.logger.error(
|
|
2446
2450
|
{ err },
|
|
2447
|
-
`Cannot start application workers, started workers: ${
|
|
2451
|
+
`Cannot start application workers, started workers: ${startedWorkersCount} out of ${workers}`
|
|
2448
2452
|
)
|
|
2449
|
-
await this.#updateApplicationConfigWorkers(applicationId, currentWorkers + report.started.length)
|
|
2450
2453
|
}
|
|
2451
2454
|
report.success = false
|
|
2452
2455
|
}
|
|
@@ -2455,26 +2458,31 @@ export class Runtime extends EventEmitter {
|
|
|
2455
2458
|
report.stopped = []
|
|
2456
2459
|
try {
|
|
2457
2460
|
for (let i = currentWorkers - 1; i >= workers; i--) {
|
|
2458
|
-
const worker = await this.#
|
|
2461
|
+
const worker = await this.#getWorkerByIdOrNext(applicationId, i, false, false)
|
|
2459
2462
|
await sendViaITC(worker, 'removeFromMesh')
|
|
2460
2463
|
await this.#stopWorker(currentWorkers, applicationId, i, false, worker, [])
|
|
2461
2464
|
report.stopped.push(i)
|
|
2465
|
+
stoppedWorkersCount++
|
|
2462
2466
|
}
|
|
2463
|
-
await this.#updateApplicationConfigWorkers(applicationId, workers)
|
|
2464
2467
|
report.success = true
|
|
2465
2468
|
} catch (err) {
|
|
2466
|
-
if (
|
|
2469
|
+
if (stoppedWorkersCount < 1) {
|
|
2467
2470
|
this.logger.error({ err }, 'Cannot stop application workers, no worker stopped')
|
|
2468
2471
|
} else {
|
|
2469
2472
|
this.logger.error(
|
|
2470
2473
|
{ err },
|
|
2471
|
-
`Cannot stop application workers, stopped workers: ${
|
|
2474
|
+
`Cannot stop application workers, stopped workers: ${stoppedWorkersCount} out of ${workers}`
|
|
2472
2475
|
)
|
|
2473
|
-
await this.#updateApplicationConfigWorkers(applicationId, currentWorkers - report.stopped)
|
|
2474
2476
|
}
|
|
2475
2477
|
report.success = false
|
|
2476
2478
|
}
|
|
2477
2479
|
}
|
|
2480
|
+
|
|
2481
|
+
const newWorkersCount = currentWorkers + startedWorkersCount - stoppedWorkersCount
|
|
2482
|
+
if (newWorkersCount !== currentWorkers) {
|
|
2483
|
+
await this.#updateApplicationConfigWorkers(applicationId, newWorkersCount)
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2478
2486
|
return report
|
|
2479
2487
|
}
|
|
2480
2488
|
|
|
@@ -2599,7 +2607,11 @@ export class Runtime extends EventEmitter {
|
|
|
2599
2607
|
}
|
|
2600
2608
|
|
|
2601
2609
|
try {
|
|
2602
|
-
const health = await this.#getHealth(worker
|
|
2610
|
+
const health = await this.#getHealth(worker, {
|
|
2611
|
+
previousELU: worker[kLastVerticalScalerELU]
|
|
2612
|
+
})
|
|
2613
|
+
worker[kLastVerticalScalerELU] = health.currentELU
|
|
2614
|
+
|
|
2603
2615
|
if (!health) continue
|
|
2604
2616
|
|
|
2605
2617
|
scalingAlgorithm.addWorkerHealthInfo({
|
package/lib/worker/controller.js
CHANGED
|
@@ -26,10 +26,7 @@ function fetchApplicationUrl (application, key) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function handleUnhandled (app, type, err) {
|
|
29
|
-
const label =
|
|
30
|
-
workerData.worker.count > 1
|
|
31
|
-
? `worker ${workerData.worker.index} of the application "${workerData.applicationConfig.id}"`
|
|
32
|
-
: `application "${workerData.applicationConfig.id}"`
|
|
29
|
+
const label = `worker ${workerData.worker.index} of the application "${workerData.applicationConfig.id}"`
|
|
33
30
|
|
|
34
31
|
globalThis.platformatic.logger.error({ err: ensureLoggableError(err) }, `The ${label} threw an ${type}.`)
|
|
35
32
|
|
|
@@ -160,12 +157,13 @@ export class Controller extends EventEmitter {
|
|
|
160
157
|
this.#logAndThrow(err)
|
|
161
158
|
}
|
|
162
159
|
|
|
163
|
-
this.emit('starting')
|
|
164
|
-
|
|
165
160
|
if (this.capability.status === 'stopped') {
|
|
166
161
|
return
|
|
167
162
|
}
|
|
168
163
|
|
|
164
|
+
this.capability.updateStatus('starting')
|
|
165
|
+
this.emit('starting')
|
|
166
|
+
|
|
169
167
|
if (this.#watch) {
|
|
170
168
|
const watchConfig = await this.capability.getWatchConfig()
|
|
171
169
|
|
|
@@ -187,6 +185,9 @@ export class Controller extends EventEmitter {
|
|
|
187
185
|
this.#listening = listen
|
|
188
186
|
/* c8 ignore next 5 */
|
|
189
187
|
} catch (err) {
|
|
188
|
+
this.capability.updateStatus('start:error')
|
|
189
|
+
this.emit('start:error', err)
|
|
190
|
+
|
|
190
191
|
this.capability.log({ message: err.message, level: 'debug' })
|
|
191
192
|
this.#starting = false
|
|
192
193
|
throw err
|
|
@@ -194,6 +195,8 @@ export class Controller extends EventEmitter {
|
|
|
194
195
|
|
|
195
196
|
this.#started = true
|
|
196
197
|
this.#starting = false
|
|
198
|
+
|
|
199
|
+
this.capability.updateStatus('started')
|
|
197
200
|
this.emit('started')
|
|
198
201
|
}
|
|
199
202
|
|
|
@@ -203,6 +206,10 @@ export class Controller extends EventEmitter {
|
|
|
203
206
|
}
|
|
204
207
|
|
|
205
208
|
this.emit('stopping')
|
|
209
|
+
// Do not update status of the capability to "stopping" here otherwise
|
|
210
|
+
// if stop is called before start is finished, the capability will not
|
|
211
|
+
// be able to wait for start to finish and it will create a race condition.
|
|
212
|
+
|
|
206
213
|
await this.#stopFileWatching()
|
|
207
214
|
await this.capability.waitForDependentsStop(dependents)
|
|
208
215
|
await this.capability.stop()
|
|
@@ -210,6 +217,8 @@ export class Controller extends EventEmitter {
|
|
|
210
217
|
this.#started = false
|
|
211
218
|
this.#starting = false
|
|
212
219
|
this.#listening = false
|
|
220
|
+
|
|
221
|
+
this.capability.updateStatus('stopped')
|
|
213
222
|
this.emit('stopped')
|
|
214
223
|
}
|
|
215
224
|
|
package/lib/worker/main.js
CHANGED
|
@@ -59,13 +59,10 @@ function createLogger () {
|
|
|
59
59
|
const pinoOptions = {
|
|
60
60
|
level: 'trace',
|
|
61
61
|
name: workerData.applicationConfig.id,
|
|
62
|
+
base: { pid: process.pid, hostname: hostname(), worker: workerData.worker.index },
|
|
62
63
|
...workerData.config.logger
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
if (workerData.worker?.count > 1) {
|
|
66
|
-
pinoOptions.base = { pid: process.pid, hostname: hostname(), worker: workerData.worker.index }
|
|
67
|
-
}
|
|
68
|
-
|
|
69
66
|
if (pinoOptions.formatters) {
|
|
70
67
|
pinoOptions.formatters = buildPinoFormatters(pinoOptions.formatters)
|
|
71
68
|
}
|
|
@@ -167,7 +164,7 @@ async function main () {
|
|
|
167
164
|
const controller = new Controller(
|
|
168
165
|
runtimeConfig,
|
|
169
166
|
applicationConfig,
|
|
170
|
-
workerData.worker.
|
|
167
|
+
workerData.worker.index,
|
|
171
168
|
serverConfig,
|
|
172
169
|
metricsConfig
|
|
173
170
|
)
|
package/lib/worker/messaging.js
CHANGED
|
@@ -179,12 +179,8 @@ export class MessagingITC extends ITC {
|
|
|
179
179
|
// Create a brand new map
|
|
180
180
|
this.#workers = new RoundRobinMap()
|
|
181
181
|
|
|
182
|
-
const instances = []
|
|
183
182
|
for (const [application, workers] of event.data) {
|
|
184
183
|
const count = workers.length
|
|
185
|
-
const next = Math.floor(Math.random() * count)
|
|
186
|
-
|
|
187
|
-
instances.push({ id: application, next, workers: count })
|
|
188
184
|
|
|
189
185
|
for (let i = 0; i < count; i++) {
|
|
190
186
|
const worker = workers[i]
|
|
@@ -194,8 +190,6 @@ export class MessagingITC extends ITC {
|
|
|
194
190
|
this.#workers.set(`${application}:${i}`, { ...worker, channel })
|
|
195
191
|
}
|
|
196
192
|
}
|
|
197
|
-
|
|
198
|
-
this.#workers.configure(instances)
|
|
199
193
|
}
|
|
200
194
|
|
|
201
195
|
async #handleNotification (messageEvent) {
|
|
@@ -1,62 +1,60 @@
|
|
|
1
1
|
export class RoundRobinMap extends Map {
|
|
2
2
|
#instances
|
|
3
3
|
|
|
4
|
-
constructor (
|
|
5
|
-
super(
|
|
6
|
-
this.#instances =
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
get configuration () {
|
|
10
|
-
return { ...this.#instances }
|
|
4
|
+
constructor () {
|
|
5
|
+
super()
|
|
6
|
+
this.#instances = {}
|
|
11
7
|
}
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
set (key, worker) {
|
|
10
|
+
const hasKey = super.has(key)
|
|
11
|
+
if (!hasKey) {
|
|
12
|
+
const application = key.split(':')[0]
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
if (!this.#instances[application]) {
|
|
15
|
+
this.#instances[application] = { keys: [] }
|
|
16
|
+
}
|
|
17
|
+
this.#instances[application].next = null
|
|
18
|
+
this.#instances[application].keys.push(key)
|
|
18
19
|
}
|
|
20
|
+
|
|
21
|
+
return super.set(key, worker)
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
delete (key) {
|
|
25
|
+
const removed = super.delete(key)
|
|
26
|
+
|
|
27
|
+
if (removed) {
|
|
28
|
+
const application = key.split(':')[0]
|
|
29
|
+
|
|
30
|
+
if (this.#instances[application]) {
|
|
31
|
+
const keys = this.#instances[application].keys
|
|
32
|
+
if (keys.length <= 1) {
|
|
33
|
+
delete this.#instances[application]
|
|
34
|
+
} else {
|
|
35
|
+
const keys = this.#instances[application].keys
|
|
36
|
+
keys.splice(keys.indexOf(key), 1)
|
|
37
|
+
this.#instances[application].next = null
|
|
38
|
+
}
|
|
39
|
+
}
|
|
24
40
|
}
|
|
25
41
|
|
|
26
|
-
return
|
|
42
|
+
return removed
|
|
27
43
|
}
|
|
28
44
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
throw new Error(`Application ${application} is not configured.`)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
this.#instances[application].count = count
|
|
45
|
+
getKeys (application) {
|
|
46
|
+
return this.#instances[application]?.keys ?? []
|
|
35
47
|
}
|
|
36
48
|
|
|
37
49
|
next (application) {
|
|
38
|
-
if (!this.#instances[application])
|
|
39
|
-
return null
|
|
40
|
-
}
|
|
50
|
+
if (!this.#instances[application]) return
|
|
41
51
|
|
|
42
|
-
let
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// Try count times to get the next worker. This is to handle the case where a worker is being restarted.
|
|
46
|
-
for (let i = 0; i < count; i++) {
|
|
47
|
-
const current = next++
|
|
48
|
-
if (next >= count) {
|
|
49
|
-
next = 0
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
worker = this.get(`${application}:${current}`)
|
|
53
|
-
|
|
54
|
-
if (worker) {
|
|
55
|
-
break
|
|
56
|
-
}
|
|
52
|
+
let { next, keys } = this.#instances[application]
|
|
53
|
+
if (next === null) {
|
|
54
|
+
next = Math.floor(Math.random() * keys.length)
|
|
57
55
|
}
|
|
56
|
+
this.#instances[application].next = (next + 1) % keys.length
|
|
58
57
|
|
|
59
|
-
this
|
|
60
|
-
return worker
|
|
58
|
+
return this.get(keys[next])
|
|
61
59
|
}
|
|
62
60
|
}
|
package/lib/worker/symbols.js
CHANGED
|
@@ -8,7 +8,8 @@ export const kHealthCheckTimer = Symbol.for('plt.runtime.worker.healthCheckTimer
|
|
|
8
8
|
export const kWorkerStatus = Symbol('plt.runtime.worker.status')
|
|
9
9
|
export const kWorkerStartTime = Symbol.for('plt.runtime.worker.startTime')
|
|
10
10
|
export const kInterceptors = Symbol.for('plt.runtime.worker.interceptors')
|
|
11
|
-
export const
|
|
11
|
+
export const kLastHealthCheckELU = Symbol.for('plt.runtime.worker.lastHealthCheckELU')
|
|
12
|
+
export const kLastVerticalScalerELU = Symbol.for('plt.runtime.worker.lastVerticalScalerELU')
|
|
12
13
|
|
|
13
14
|
// This string marker should be safe to use since it belongs to Unicode private area
|
|
14
15
|
export const kStderrMarker = '\ue002'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/runtime",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.13.1",
|
|
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/composer": "3.
|
|
39
|
-
"@platformatic/db": "3.
|
|
40
|
-
"@platformatic/
|
|
41
|
-
"@platformatic/
|
|
42
|
-
"@platformatic/
|
|
43
|
-
"@platformatic/
|
|
44
|
-
"@platformatic/sql-mapper": "3.
|
|
45
|
-
"@platformatic/wattpm-pprof-capture": "3.
|
|
38
|
+
"@platformatic/composer": "3.13.1",
|
|
39
|
+
"@platformatic/db": "3.13.1",
|
|
40
|
+
"@platformatic/gateway": "3.13.1",
|
|
41
|
+
"@platformatic/node": "3.13.1",
|
|
42
|
+
"@platformatic/service": "3.13.1",
|
|
43
|
+
"@platformatic/sql-graphql": "3.13.1",
|
|
44
|
+
"@platformatic/sql-mapper": "3.13.1",
|
|
45
|
+
"@platformatic/wattpm-pprof-capture": "3.13.1"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@fastify/accepts": "^5.0.0",
|
|
@@ -73,12 +73,12 @@
|
|
|
73
73
|
"undici": "^7.0.0",
|
|
74
74
|
"undici-thread-interceptor": "^0.15.0",
|
|
75
75
|
"ws": "^8.16.0",
|
|
76
|
-
"@platformatic/
|
|
77
|
-
"@platformatic/generators": "3.
|
|
78
|
-
"@platformatic/
|
|
79
|
-
"@platformatic/
|
|
80
|
-
"@platformatic/
|
|
81
|
-
"@platformatic/
|
|
76
|
+
"@platformatic/basic": "3.13.1",
|
|
77
|
+
"@platformatic/generators": "3.13.1",
|
|
78
|
+
"@platformatic/foundation": "3.13.1",
|
|
79
|
+
"@platformatic/telemetry": "3.13.1",
|
|
80
|
+
"@platformatic/itc": "3.13.1",
|
|
81
|
+
"@platformatic/metrics": "3.13.1"
|
|
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.
|
|
2
|
+
"$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.13.1.json",
|
|
3
3
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
4
|
"title": "Platformatic Runtime Config",
|
|
5
5
|
"type": "object",
|