@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 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 kLastELU: unique symbol
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
+ )
@@ -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.size) {
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
- kLastELU,
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
- const workersConfig = []
194
- for (const application of config.applications) {
195
- const count = application.workers ?? this.#config.workers ?? 1
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
- workersConfig.push({ id: application.id, workers: 1 })
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['PLT_DEV'] = 'false'
210
- this.#env['PLT_ENVIRONMENT'] = 'production'
209
+ this.#env.PLT_DEV = 'false'
210
+ this.#env.PLT_ENVIRONMENT = 'production'
211
211
  } else {
212
- this.#env['PLT_DEV'] = 'true'
213
- this.#env['PLT_ENVIRONMENT'] = 'development'
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 workersCount = await this.#workers.getCount(applicationConfig.id)
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 < workersCount; i++) {
501
- await this.#startWorker(config, applicationConfig, workersCount, id, i, silent)
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 workersCount = await this.#workers.getCount(applicationConfig.id)
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 (let i = 0; i < workersCount; i++) {
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
- const workersCount = await this.#workers.getCount(id)
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 label = `${id}:${i}`
540
- const worker = this.#workers.get(label)
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 [application, { count }] of Object.entries(this.#workers.configuration)) {
865
- for (let i = 0; i < count; i++) {
866
- const label = `${application}:${i}`
867
- const worker = this.#workers.get(label)
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 [application, { count }] of Object.entries(this.#workers.configuration)) {
882
- for (let i = 0; i < count; i++) {
883
- const label = `${application}:${i}`
884
- const worker = this.#workers.get(label)
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 workers = this.#workers.getCount(id)
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 [application, { count }] of Object.entries(this.#workers.configuration)) {
1084
- for (let i = 0; i < count; i++) {
1085
- const label = `${application}:${i}`
1086
- const worker = this.#workers.get(label)
1087
-
1088
- status[label] = {
1089
- application,
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.getCount(id)
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
- const workersCount = await this.#workers.getCount(applicationConfig.id)
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 < workersCount; i++) {
1281
- setupInvocations.push([config, applicationConfig, workersCount, id, i])
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['NODE_OPTIONS'] ?? ''
1343
+ const originalNodeOptions = workerEnv.NODE_OPTIONS ?? ''
1348
1344
 
1349
- workerEnv['NODE_OPTIONS'] = `${originalNodeOptions} ${applicationConfig.nodeOptions}`.trim()
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, workersCount > 1 ? index : undefined)
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] = workersCount > 1 ? workerId : applicationId
1444
+ worker[kId] = workerId
1449
1445
  worker[kFullId] = workerId
1450
1446
  worker[kApplicationId] = applicationId
1451
- worker[kWorkerId] = workersCount > 1 ? index : undefined
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
- const { used_heap_size: heapUsed, total_heap_size: heapTotal } = await worker.getHeapStatistics()
1533
- const currentELU = worker.performance.eventLoopUtilization()
1534
- const elu = worker[kLastELU] ? worker.performance.eventLoopUtilization(currentELU, worker[kLastELU]) : currentELU
1535
- worker[kLastELU] = currentELU
1536
- return { elu: elu.utilization, heapUsed, heapTotal }
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 health = await worker[kITC].send('getHealth')
1540
- return health
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.#getWorkerById(id, index, false, false)
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.#getWorkerById(id, index)
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.#getWorkerById(id, index, false, false)
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, workersCount) {
1831
- return workersCount > 1
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.#getWorkerById(applicationId, workerId, ensureStarted, mustExist)
1937
+ return this.#getWorkerByIdOrNext(applicationId, workerId, ensureStarted, mustExist)
1937
1938
  }
1938
1939
 
1939
- async #getWorkerById (applicationId, workerId, ensureStarted = false, mustExist = true) {
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
- if (typeof workerId !== 'undefined') {
1943
- worker = this.#workers.get(`${applicationId}:${workerId}`)
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 = Array.from(this.#workers.keys())
1957
- .filter(key => key.startsWith(applicationId + ':'))
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.#getWorkerById(application, worker, true, true)
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
- for (const [workerId, worker] of this.#workers.entries()) {
2164
- if (workerId.startsWith(`${applicationId}:`)) {
2165
- promises.push(sendViaITC(worker, 'updateWorkersCount', { applicationId, workers }))
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.#getWorkerById(applicationId, i)
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
- current: currentWorkers,
2428
- new: workers
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 (report.started.length < 1) {
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: ${report.started.length} out of ${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.#getWorkerById(applicationId, i, false, false)
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 (report.stopped.length < 1) {
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: ${report.stopped.length} out of ${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({
@@ -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
 
@@ -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.count > 1 ? workerData.worker.index : undefined,
167
+ workerData.worker.index,
171
168
  serverConfig,
172
169
  metricsConfig
173
170
  )
@@ -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 (iterable, instances = {}) {
5
- super(iterable)
6
- this.#instances = instances
7
- }
8
-
9
- get configuration () {
10
- return { ...this.#instances }
4
+ constructor () {
5
+ super()
6
+ this.#instances = {}
11
7
  }
12
8
 
13
- configure (applications) {
14
- this.#instances = {}
9
+ set (key, worker) {
10
+ const hasKey = super.has(key)
11
+ if (!hasKey) {
12
+ const application = key.split(':')[0]
15
13
 
16
- for (const application of applications) {
17
- this.#instances[application.id] = { next: application.next ?? 0, count: application.workers }
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
- getCount (application) {
22
- if (!this.#instances[application]) {
23
- return null
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 this.#instances[application].count
42
+ return removed
27
43
  }
28
44
 
29
- setCount (application, count) {
30
- if (!this.#instances[application]) {
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 worker
43
- let { next, count } = this.#instances[application]
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.#instances[application].next = next
60
- return worker
58
+ return this.get(keys[next])
61
59
  }
62
60
  }
@@ -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 kLastELU = Symbol.for('plt.runtime.worker.lastELU')
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.12.0",
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.12.0",
39
- "@platformatic/db": "3.12.0",
40
- "@platformatic/node": "3.12.0",
41
- "@platformatic/gateway": "3.12.0",
42
- "@platformatic/sql-graphql": "3.12.0",
43
- "@platformatic/service": "3.12.0",
44
- "@platformatic/sql-mapper": "3.12.0",
45
- "@platformatic/wattpm-pprof-capture": "3.12.0"
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/foundation": "3.12.0",
77
- "@platformatic/generators": "3.12.0",
78
- "@platformatic/basic": "3.12.0",
79
- "@platformatic/itc": "3.12.0",
80
- "@platformatic/metrics": "3.12.0",
81
- "@platformatic/telemetry": "3.12.0"
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.12.0.json",
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",