@platformatic/runtime 2.69.0 → 2.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/config.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * and run json-schema-to-typescript to regenerate this file.
6
6
  */
7
7
 
8
- export type HttpsSchemasPlatformaticDevPlatformaticRuntime2690Json = {
8
+ export type HttpsSchemasPlatformaticDevPlatformaticRuntime2700Json = {
9
9
  [k: string]: unknown;
10
10
  } & {
11
11
  $schema?: string;
@@ -29,7 +29,7 @@ export type HttpsSchemasPlatformaticDevPlatformaticRuntime2690Json = {
29
29
  maxELU?: number | string;
30
30
  maxHeapUsed?: number | string;
31
31
  maxHeapTotal?: number | string;
32
- maxYoungGeneration?: number;
32
+ maxYoungGeneration?: number | string;
33
33
  };
34
34
  preload?: string | string[];
35
35
  arguments?: string[];
@@ -141,7 +141,7 @@ export type HttpsSchemasPlatformaticDevPlatformaticRuntime2690Json = {
141
141
  maxELU?: number | string;
142
142
  maxHeapUsed?: number | string;
143
143
  maxHeapTotal?: number | string;
144
- maxYoungGeneration?: number;
144
+ maxYoungGeneration?: number | string;
145
145
  };
146
146
  undici?: {
147
147
  agentOptions?: {
package/lib/errors.js CHANGED
@@ -13,6 +13,7 @@ module.exports = {
13
13
  WorkerExitedError: createError(`${ERROR_PREFIX}_SERVICE_EXIT`, 'The worker %s of the service "%s" exited prematurely with error code %d'),
14
14
  UnknownRuntimeAPICommandError: createError(`${ERROR_PREFIX}_UNKNOWN_RUNTIME_API_COMMAND`, 'Unknown Runtime API command "%s"'),
15
15
  ServiceNotFoundError: createError(`${ERROR_PREFIX}_SERVICE_NOT_FOUND`, 'Service %s not found. Available services are: %s'),
16
+ ServiceWorkerNotFoundError: createError(`${ERROR_PREFIX}_SERVICE_WORKER_NOT_FOUND`, 'Service %s worker %s not found'),
16
17
  ServiceNotStartedError: createError(`${ERROR_PREFIX}_SERVICE_NOT_STARTED`, "Service with id '%s' is not started"),
17
18
  ServiceStartTimeoutError: createError(`${ERROR_PREFIX}_SERVICE_START_TIMEOUT`, "Service with id '%s' failed to start in %dms."),
18
19
  FailedToRetrieveOpenAPISchemaError: createError(`${ERROR_PREFIX}_FAILED_TO_RETRIEVE_OPENAPI_SCHEMA`, 'Failed to retrieve OpenAPI schema for service with id "%s": %s'),
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 } = 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,8 @@ const {
33
33
  kHealthCheckTimer,
34
34
  kConfig,
35
35
  kWorkerStatus,
36
- kStderrMarker
36
+ kStderrMarker,
37
+ kLastELU
37
38
  } = require('./worker/symbols')
38
39
 
39
40
  const fastify = require('fastify')
@@ -1148,10 +1149,13 @@ class Runtime extends EventEmitter {
1148
1149
  workerEnv['NODE_OPTIONS'] = `${originalNodeOptions} ${serviceConfig.nodeOptions}`.trim()
1149
1150
  }
1150
1151
 
1152
+ const maxHeapTotal = typeof health.maxHeapTotal === 'string' ? parseMemorySize(health.maxHeapTotal) : health.maxHeapTotal
1153
+ const maxYoungGeneration = typeof health.maxYoungGeneration === 'string' ? parseMemorySize(health.maxYoungGeneration) : health.maxYoungGeneration
1154
+
1151
1155
  const maxOldGenerationSizeMb = Math.floor(
1152
- (health.maxYoungGeneration > 0 ? health.maxHeapTotal - health.maxYoungGeneration : health.maxHeapTotal) / (1024 * 1024)
1156
+ (maxYoungGeneration > 0 ? maxHeapTotal - maxYoungGeneration : maxHeapTotal) / (1024 * 1024)
1153
1157
  )
1154
- const maxYoungGenerationSizeMb = health.maxYoungGeneration ? Math.floor(health.maxYoungGeneration / (1024 * 1024)) : undefined
1158
+ const maxYoungGenerationSizeMb = maxYoungGeneration ? Math.floor(maxYoungGeneration / (1024 * 1024)) : undefined
1155
1159
 
1156
1160
  const worker = new Worker(kWorkerFile, {
1157
1161
  workerData: {
@@ -1323,18 +1327,39 @@ class Runtime extends EventEmitter {
1323
1327
  return worker
1324
1328
  }
1325
1329
 
1326
- #setupHealthCheck (config, serviceConfig, workersCount, id, index, workerId, worker, errorLabel) {
1330
+ async #getHealth (worker) {
1331
+ if (features.node.worker.getHeapStatistics) {
1332
+ const { used_heap_size: heapUsed, total_heap_size: heapTotal } = await worker.getHeapStatistics()
1333
+ const currentELU = worker.performance.eventLoopUtilization()
1334
+ const elu = worker.performance.eventLoopUtilization(currentELU, worker[kLastELU]).utilization
1335
+ worker[kLastELU] = currentELU
1336
+ return { elu, heapUsed, heapTotal }
1337
+ }
1338
+
1339
+ const health = await worker[kITC].send('getHealth')
1340
+ return health
1341
+ }
1342
+
1343
+ #setupHealthCheck (config, serviceConfig, workersCount, id, index, worker, errorLabel) {
1327
1344
  // Clear the timeout when exiting
1328
1345
  worker.on('exit', () => clearTimeout(worker[kHealthCheckTimer]))
1329
1346
 
1330
1347
  const { maxELU, maxHeapUsed, maxHeapTotal, maxUnhealthyChecks, interval } = worker[kConfig].health
1348
+ const maxHeapTotalNumber = typeof maxHeapTotal === 'string' ? parseMemorySize(maxHeapTotal) : maxHeapTotal
1349
+
1331
1350
  let unhealthyChecks = 0
1332
1351
 
1333
1352
  worker[kHealthCheckTimer] = setTimeout(async () => {
1334
- const health = await worker[kITC].send('getHealth')
1335
- const { elu, heapUsed } = health
1336
- const memoryUsage = heapUsed / maxHeapTotal
1337
- const unhealthy = elu > maxELU || memoryUsage > maxHeapUsed
1353
+ let health, unhealthy, memoryUsage
1354
+ try {
1355
+ health = await this.#getHealth(worker)
1356
+ memoryUsage = health.heapUsed / maxHeapTotalNumber
1357
+ unhealthy = health.elu > maxELU || memoryUsage > maxHeapUsed
1358
+ } catch (err) {
1359
+ this.logger.error({ err }, `Failed to get health for ${errorLabel}.`)
1360
+ unhealthy = true
1361
+ memoryUsage = 0
1362
+ }
1338
1363
 
1339
1364
  const serviceId = worker[kServiceId]
1340
1365
  this.emit('health', {
@@ -1347,9 +1372,9 @@ class Runtime extends EventEmitter {
1347
1372
  })
1348
1373
 
1349
1374
  if (unhealthy) {
1350
- if (elu > maxELU) {
1375
+ if (health.elu > maxELU) {
1351
1376
  this.logger.error(
1352
- `The ${errorLabel} has an ELU of ${(elu * 100).toFixed(2)} %, above the maximum allowed usage of ${(maxELU * 100).toFixed(2)} %.`
1377
+ `The ${errorLabel} has an ELU of ${(health.elu * 100).toFixed(2)} %, above the maximum allowed usage of ${(maxELU * 100).toFixed(2)} %.`
1353
1378
  )
1354
1379
  }
1355
1380
 
@@ -1367,14 +1392,14 @@ class Runtime extends EventEmitter {
1367
1392
  if (unhealthyChecks === maxUnhealthyChecks) {
1368
1393
  try {
1369
1394
  this.logger.error(
1370
- { elu, maxELU, memoryUsage, maxMemoryUsage: maxHeapUsed },
1395
+ { elu: health.elu, maxELU, memoryUsage: health.heapUsed, maxMemoryUsage: maxHeapUsed },
1371
1396
  `The ${errorLabel} is unhealthy. Replacing it ...`
1372
1397
  )
1373
1398
 
1374
- await this.#replaceWorker(config, serviceConfig, workersCount, id, index, workerId, worker)
1399
+ await this.#replaceWorker(config, serviceConfig, workersCount, id, index, worker)
1375
1400
  } catch (e) {
1376
1401
  this.logger.error(
1377
- { elu, maxELU, memoryUsage, maxMemoryUsage: maxHeapUsed },
1402
+ { elu: health.elu, maxELU, memoryUsage: health.heapUsed, maxMemoryUsage: maxHeapUsed },
1378
1403
  `Cannot replace the ${errorLabel}. Forcefully terminating it ...`
1379
1404
  )
1380
1405
 
@@ -1397,7 +1422,6 @@ class Runtime extends EventEmitter {
1397
1422
  worker = undefined,
1398
1423
  disableRestartAttempts = false
1399
1424
  ) {
1400
- const workerId = `${id}:${index}`
1401
1425
  const label = this.#workerExtendedLabel(id, index, workersCount)
1402
1426
 
1403
1427
  if (!silent) {
@@ -1451,7 +1475,7 @@ class Runtime extends EventEmitter {
1451
1475
  if (enabled && config.restartOnError > 0) {
1452
1476
  worker[kHealthCheckTimer] = setTimeout(
1453
1477
  () => {
1454
- this.#setupHealthCheck(config, serviceConfig, workersCount, id, index, workerId, worker, label)
1478
+ this.#setupHealthCheck(config, serviceConfig, workersCount, id, index, worker, label)
1455
1479
  },
1456
1480
  gracePeriod > 0 ? gracePeriod : 1
1457
1481
  )
@@ -1625,7 +1649,8 @@ class Runtime extends EventEmitter {
1625
1649
  await restartPromise
1626
1650
  }
1627
1651
 
1628
- async #replaceWorker (config, serviceConfig, workersCount, serviceId, index, workerId, worker) {
1652
+ async #replaceWorker (config, serviceConfig, workersCount, serviceId, index, worker) {
1653
+ const workerId = `${serviceId}:${index}`
1629
1654
  let newWorker
1630
1655
 
1631
1656
  try {
@@ -1685,7 +1710,7 @@ class Runtime extends EventEmitter {
1685
1710
  return null
1686
1711
  }
1687
1712
 
1688
- throw new errors.ServiceNotFoundError(serviceId, Array.from(this.#servicesIds).join(', '))
1713
+ throw new errors.ServiceWorkerNotFoundError(serviceId, workerId)
1689
1714
  }
1690
1715
 
1691
1716
  if (ensureStarted) {
@@ -1849,10 +1874,16 @@ class Runtime extends EventEmitter {
1849
1874
 
1850
1875
  async getServiceResourcesInfo (id) {
1851
1876
  const workers = this.#workers.getCount(id)
1852
- return { workers }
1877
+
1878
+ const worker = await this.#getWorkerById(id, 0, false, false)
1879
+ const health = worker[kConfig].health
1880
+
1881
+ return { workers, health }
1853
1882
  }
1854
1883
 
1855
- async #updateWorkerCount (serviceId, workers) {
1884
+ async #updateServiceConfigWorkers (serviceId, workers) {
1885
+ this.logger.info(`Updating service "${serviceId}" config workers to ${workers}`)
1886
+
1856
1887
  this.#configManager.current.services.find(s => s.id === serviceId).workers = workers
1857
1888
  const service = await this.#getServiceById(serviceId)
1858
1889
  this.#workers.setCount(serviceId, workers)
@@ -1868,14 +1899,109 @@ class Runtime extends EventEmitter {
1868
1899
  const results = await Promise.allSettled(promises)
1869
1900
  for (const result of results) {
1870
1901
  if (result.status === 'rejected') {
1902
+ this.logger.error({ err: result.reason }, `Cannot update service "${serviceId}" workers`)
1871
1903
  throw result.reason
1872
1904
  }
1873
1905
  }
1874
1906
  }
1875
1907
 
1908
+ async #updateServiceConfigHealth (serviceId, health) {
1909
+ this.logger.info(`Updating service "${serviceId}" config health heap to ${JSON.stringify(health)}`)
1910
+ const { maxHeapTotal, maxYoungGeneration } = health
1911
+
1912
+ const service = this.#configManager.current.services.find(s => s.id === serviceId)
1913
+ if (maxHeapTotal) {
1914
+ service.health.maxHeapTotal = maxHeapTotal
1915
+ }
1916
+ if (maxYoungGeneration) {
1917
+ service.health.maxYoungGeneration = maxYoungGeneration
1918
+ }
1919
+ }
1920
+
1921
+ /**
1922
+ * Updates the resources of the services, such as the number of workers and health configurations (e.g., heap memory settings).
1923
+ *
1924
+ * This function handles three update scenarios for each service:
1925
+ * 1. **Updating workers only**: Adjusts the number of workers for the service.
1926
+ * 2. **Updating health configurations only**: Updates health parameters like `maxHeapTotal` or `maxYoungGeneration`.
1927
+ * 3. **Updating both workers and health configurations**: Scales the workers and also applies health settings.
1928
+ *
1929
+ * When updating both workers and health:
1930
+ * - **Scaling down workers**: Stops extra workers, then restarts the remaining workers with the previous settings.
1931
+ * - **Scaling up workers**: Starts new workers with the updated heap settings, then restarts the old workers with the updated settings.
1932
+ *
1933
+ * 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.
1934
+ * Scaling down is expected to succeed without issues.
1935
+ *
1936
+ * @param {Array<Object>} updates - An array of objects that define the updates for each service.
1937
+ * @param {string} updates[].service - The ID of the service to update.
1938
+ * @param {number} [updates[].workers] - The desired number of workers for the service. If omitted, workers will not be updated.
1939
+ * @param {Object} [updates[].health] - The health configuration to update for the service, which may include:
1940
+ * @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.
1941
+ * @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.
1942
+ *
1943
+ * @returns {Promise<Array<Object>>} - A promise that resolves to an array of reports for each service, detailing the success or failure of the operations:
1944
+ * - `service`: The service ID.
1945
+ * - `workers`: The workers' update report, including the current, new number of workers, started workers, and success status.
1946
+ * - `health`: The health update report, showing the current and new heap settings, updated workers, and success status.
1947
+ *
1948
+ * @example
1949
+ * await runtime.updateServicesResources([
1950
+ * { service: 'service-1', workers: 2, health: { maxHeapTotal: '1G', maxYoungGeneration: '128 MB' } },
1951
+ * { service: 'service-2', health: { maxHeapTotal: '1G' } },
1952
+ * { service: 'service-3', workers: 2 },
1953
+ * ])
1954
+ *
1955
+ * In this example:
1956
+ * - `service-1` will have 2 workers and updated heap memory configurations.
1957
+ * - `service-2` will have updated heap memory settings (without changing workers).
1958
+ * - `service-3` will have its workers set to 2 but no change in memory settings.
1959
+ *
1960
+ * @throws {InvalidArgumentError} - Throws if any update parameter is invalid, such as:
1961
+ * - Missing service ID.
1962
+ * - Invalid worker count (not a positive integer).
1963
+ * - Invalid memory size format for `maxHeapTotal` or `maxYoungGeneration`.
1964
+ * @throws {ServiceNotFoundError} - Throws if the specified service ID does not exist in the current service configuration.
1965
+ */
1876
1966
  async updateServicesResources (updates) {
1877
- if (this.#status === 'stopping' || this.#status === 'closed') return
1967
+ if (this.#status === 'stopping' || this.#status === 'closed') {
1968
+ this.logger.warn('Cannot update service resources when the runtime is stopping or closed')
1969
+ return
1970
+ }
1971
+
1972
+ const ups = await this.#validateUpdateServiceResources(updates)
1973
+ const config = this.#configManager.current
1878
1974
 
1975
+ const report = []
1976
+ for (const update of ups) {
1977
+ const { serviceId, config: serviceConfig, workers, health, currentWorkers, currentHealth } = update
1978
+
1979
+ if (workers && health) {
1980
+ const r = await this.#updateServiceWorkersAndHealth(serviceId, config, serviceConfig, workers, health, currentWorkers, currentHealth)
1981
+ report.push({
1982
+ service: serviceId,
1983
+ workers: r.workers,
1984
+ health: r.health
1985
+ })
1986
+ } else if (health) {
1987
+ const r = await this.#updateServiceHealth(serviceId, config, serviceConfig, currentWorkers, currentHealth, health)
1988
+ report.push({
1989
+ service: serviceId,
1990
+ health: r.health
1991
+ })
1992
+ } else if (workers) {
1993
+ const r = await this.#updateServiceWorkers(serviceId, config, serviceConfig, workers, currentWorkers)
1994
+ report.push({
1995
+ service: serviceId,
1996
+ workers: r.workers
1997
+ })
1998
+ }
1999
+ }
2000
+
2001
+ return report
2002
+ }
2003
+
2004
+ async #validateUpdateServiceResources (updates) {
1879
2005
  if (!Array.isArray(updates)) {
1880
2006
  throw new errors.InvalidArgumentError('updates', 'must be an array')
1881
2007
  }
@@ -1884,47 +2010,207 @@ class Runtime extends EventEmitter {
1884
2010
  }
1885
2011
 
1886
2012
  const config = this.#configManager.current
1887
-
2013
+ const validatedUpdates = []
1888
2014
  for (const update of updates) {
1889
- const { service: serviceId, workers } = update
2015
+ const { service: serviceId } = update
1890
2016
 
1891
- if (typeof workers !== 'number') {
1892
- throw new errors.InvalidArgumentError('workers', 'must be a number')
1893
- }
1894
2017
  if (!serviceId) {
1895
2018
  throw new errors.InvalidArgumentError('service', 'must be a string')
1896
2019
  }
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
2020
  const serviceConfig = config.services.find(s => s.id === serviceId)
1904
2021
  if (!serviceConfig) {
1905
2022
  throw new errors.ServiceNotFoundError(serviceId, Array.from(this.#servicesIds).join(', '))
1906
2023
  }
1907
2024
 
1908
- const { workers: currentWorkers } = await this.getServiceResourcesInfo(serviceId)
1909
- if (currentWorkers === workers) {
1910
- this.logger.warn({ serviceId, workers }, 'No change in the number of workers for service')
1911
- continue
2025
+ const { workers: currentWorkers, health: currentHealth } = await this.getServiceResourcesInfo(serviceId)
2026
+
2027
+ let workers
2028
+ if (update.workers !== undefined) {
2029
+ if (typeof update.workers !== 'number') {
2030
+ throw new errors.InvalidArgumentError('workers', 'must be a number')
2031
+ }
2032
+ if (update.workers <= 0) {
2033
+ throw new errors.InvalidArgumentError('workers', 'must be greater than 0')
2034
+ }
2035
+ if (update.workers > MAX_WORKERS) {
2036
+ throw new errors.InvalidArgumentError('workers', `must be less than ${MAX_WORKERS}`)
2037
+ }
2038
+
2039
+ if (currentWorkers === update.workers) {
2040
+ this.logger.warn({ serviceId, workers: update.workers }, 'No change in the number of workers for service')
2041
+ } else {
2042
+ workers = update.workers
2043
+ }
1912
2044
  }
1913
2045
 
1914
- if (currentWorkers < workers) {
1915
- await this.#updateWorkerCount(serviceId, workers)
2046
+ let maxHeapTotal, maxYoungGeneration
2047
+ if (update.health) {
2048
+ if (update.health.maxHeapTotal !== undefined) {
2049
+ if (typeof update.health.maxHeapTotal === 'string') {
2050
+ try {
2051
+ maxHeapTotal = parseMemorySize(update.health.maxHeapTotal)
2052
+ } catch {
2053
+ throw new errors.InvalidArgumentError('maxHeapTotal', 'must be a valid memory size')
2054
+ }
2055
+ } else if (typeof update.health.maxHeapTotal === 'number') {
2056
+ maxHeapTotal = update.health.maxHeapTotal
2057
+ if (update.health.maxHeapTotal <= 0) {
2058
+ throw new errors.InvalidArgumentError('maxHeapTotal', 'must be greater than 0')
2059
+ }
2060
+ } else {
2061
+ throw new errors.InvalidArgumentError('maxHeapTotal', 'must be a number or a string representing a memory size')
2062
+ }
2063
+
2064
+ if (currentHealth.maxHeapTotal === maxHeapTotal) {
2065
+ this.logger.warn({ serviceId, maxHeapTotal }, 'No change in the max heap total for service')
2066
+ maxHeapTotal = undefined
2067
+ }
2068
+ }
2069
+
2070
+ if (update.health.maxYoungGeneration !== undefined) {
2071
+ if (typeof update.health.maxYoungGeneration === 'string') {
2072
+ try {
2073
+ maxYoungGeneration = parseMemorySize(update.health.maxYoungGeneration)
2074
+ } catch {
2075
+ throw new errors.InvalidArgumentError('maxYoungGeneration', 'must be a valid memory size')
2076
+ }
2077
+ } else if (typeof update.health.maxYoungGeneration === 'number') {
2078
+ maxYoungGeneration = update.health.maxYoungGeneration
2079
+ if (update.health.maxYoungGeneration <= 0) {
2080
+ throw new errors.InvalidArgumentError('maxYoungGeneration', 'must be greater than 0')
2081
+ }
2082
+ } else {
2083
+ throw new errors.InvalidArgumentError('maxYoungGeneration', 'must be a number or a string representing a memory size')
2084
+ }
2085
+
2086
+ if (currentHealth.maxYoungGeneration && currentHealth.maxYoungGeneration === maxYoungGeneration) {
2087
+ this.logger.warn({ serviceId, maxYoungGeneration }, 'No change in the max young generation for service')
2088
+ maxYoungGeneration = undefined
2089
+ }
2090
+ }
2091
+ }
2092
+
2093
+ if (workers || maxHeapTotal || maxYoungGeneration) {
2094
+ let health
2095
+ if (maxHeapTotal || maxYoungGeneration) {
2096
+ health = { }
2097
+ if (maxHeapTotal) {
2098
+ health.maxHeapTotal = maxHeapTotal
2099
+ }
2100
+ if (maxYoungGeneration) {
2101
+ health.maxYoungGeneration = maxYoungGeneration
2102
+ }
2103
+ }
2104
+ validatedUpdates.push({ serviceId, config: serviceConfig, workers, health, currentWorkers, currentHealth })
2105
+ }
2106
+ }
2107
+
2108
+ return validatedUpdates
2109
+ }
2110
+
2111
+ async #updateServiceWorkersAndHealth (serviceId, config, serviceConfig, workers, health, currentWorkers, currentHealth) {
2112
+ if (currentWorkers > workers) {
2113
+ // stop workers
2114
+ const reportWorkers = await this.#updateServiceWorkers(serviceId, config, serviceConfig, workers, currentWorkers)
2115
+ // update heap for current workers
2116
+ const reportHealth = await this.#updateServiceHealth(serviceId, config, serviceConfig, workers, currentHealth, health)
2117
+
2118
+ return { workers: reportWorkers, health: reportHealth }
2119
+ } else {
2120
+ // update service heap
2121
+ await this.#updateServiceConfigHealth(serviceId, health)
2122
+ // start new workers with new heap
2123
+ const reportWorkers = await this.#updateServiceWorkers(serviceId, config, serviceConfig, workers, currentWorkers)
2124
+ // update heap for current workers
2125
+ const reportHealth = await this.#updateServiceHealth(serviceId, config, serviceConfig, currentWorkers, currentHealth, health, false)
2126
+
2127
+ return { workers: reportWorkers, health: reportHealth }
2128
+ }
2129
+ }
2130
+
2131
+ async #updateServiceHealth (serviceId, config, serviceConfig, currentWorkers, currentHealth, health, updateConfig = true) {
2132
+ const report = {
2133
+ current: currentHealth,
2134
+ new: health,
2135
+ updated: []
2136
+ }
2137
+ try {
2138
+ if (updateConfig) {
2139
+ await this.#updateServiceConfigHealth(serviceId, health)
2140
+ }
2141
+
2142
+ for (let i = 0; i < currentWorkers; i++) {
2143
+ this.logger.info({ health: { current: currentHealth, new: health } }, `Restarting service "${serviceId}" worker ${i} to update config health heap...`)
2144
+
2145
+ const worker = await this.#getWorkerById(serviceId, i)
2146
+ if (health.maxHeapTotal) { worker[kConfig].health.maxHeapTotal = health.maxHeapTotal }
2147
+ if (health.maxYoungGeneration) { worker[kConfig].health.maxYoungGeneration = health.maxYoungGeneration }
2148
+
2149
+ await this.#replaceWorker(config, serviceConfig, currentWorkers, serviceId, i, worker)
2150
+ report.updated.push(i)
2151
+ this.logger.info({ health: { current: currentHealth, new: health } }, `Restarted service "${serviceId}" worker ${i}`)
2152
+ }
2153
+ report.success = true
2154
+ } catch (err) {
2155
+ if (report.updated.length < 1) {
2156
+ this.logger.error({ err }, 'Cannot update service health heap, no worker updated')
2157
+ await this.#updateServiceConfigHealth(serviceId, currentHealth)
2158
+ } else {
2159
+ this.logger.error({ err }, `Cannot update service health heap, updated workers: ${report.updated.length} out of ${currentWorkers}`)
2160
+ }
2161
+ report.success = false
2162
+ }
2163
+ return report
2164
+ }
2165
+
2166
+ async #updateServiceWorkers (serviceId, config, serviceConfig, workers, currentWorkers) {
2167
+ const report = {
2168
+ current: currentWorkers,
2169
+ new: workers
2170
+ }
2171
+ if (currentWorkers < workers) {
2172
+ report.started = []
2173
+ try {
2174
+ await this.#updateServiceConfigWorkers(serviceId, workers)
1916
2175
  for (let i = currentWorkers; i < workers; i++) {
1917
2176
  await this.#setupWorker(config, serviceConfig, workers, serviceId, i)
1918
2177
  await this.#startWorker(config, serviceConfig, workers, serviceId, i, false, 0)
2178
+ report.started.push(i)
1919
2179
  }
1920
- } else {
2180
+ report.success = true
2181
+ } catch (err) {
2182
+ if (report.started.length < 1) {
2183
+ this.logger.error({ err }, 'Cannot start service workers, no worker started')
2184
+ await this.#updateServiceConfigWorkers(serviceId, currentWorkers)
2185
+ } else {
2186
+ this.logger.error({ err }, `Cannot start service workers, started workers: ${report.started.length} out of ${workers}`)
2187
+ await this.#updateServiceConfigWorkers(serviceId, currentWorkers + report.started.length)
2188
+ }
2189
+ report.success = false
2190
+ }
2191
+ } else {
2192
+ // keep the current workers count until all the service workers are all stopped
2193
+ report.stopped = []
2194
+ try {
1921
2195
  for (let i = currentWorkers - 1; i >= workers; i--) {
1922
- // keep the current workers count until the workers are stopped
1923
- await this.#stopWorker(currentWorkers, serviceId, i, false)
2196
+ const worker = await this.#getWorkerById(serviceId, i, false, false)
2197
+ await sendViaITC(worker, 'removeFromMesh')
2198
+ await this.#stopWorker(currentWorkers, serviceId, i, false, worker)
2199
+ report.stopped.push(i)
2200
+ }
2201
+ await this.#updateServiceConfigWorkers(serviceId, workers)
2202
+ report.success = true
2203
+ } catch (err) {
2204
+ if (report.stopped.length < 1) {
2205
+ this.logger.error({ err }, 'Cannot stop service workers, no worker stopped')
2206
+ } else {
2207
+ this.logger.error({ err }, `Cannot stop service workers, stopped workers: ${report.stopped.length} out of ${workers}`)
2208
+ await this.#updateServiceConfigWorkers(serviceId, currentWorkers - report.stopped)
1924
2209
  }
1925
- await this.#updateWorkerCount(serviceId, workers)
2210
+ report.success = false
1926
2211
  }
1927
2212
  }
2213
+ return report
1928
2214
  }
1929
2215
  }
1930
2216
 
@@ -164,14 +164,16 @@ async function getDispatcherOpts (undiciConfig) {
164
164
 
165
165
  function createThreadInterceptor (runtimeConfig) {
166
166
  const telemetry = runtimeConfig.telemetry
167
- const hooks = telemetry ? createTelemetryThreadInterceptorHooks() : {}
167
+
168
+ const telemetryHooks = telemetry ? createTelemetryThreadInterceptorHooks() : {}
169
+
168
170
  const threadDispatcher = wire({
169
171
  // Specifying the domain is critical to avoid flooding the DNS
170
172
  // with requests for a domain that's never going to exist.
171
173
  domain: '.plt.local',
172
174
  port: parentPort,
173
175
  timeout: runtimeConfig.serviceTimeout,
174
- ...hooks
176
+ ...telemetryHooks,
175
177
  })
176
178
  return threadDispatcher
177
179
  }
@@ -9,6 +9,7 @@ const kITC = Symbol.for('plt.runtime.itc')
9
9
  const kHealthCheckTimer = Symbol.for('plt.runtime.worker.healthCheckTimer')
10
10
  const kWorkerStatus = Symbol('plt.runtime.worker.status')
11
11
  const kInterceptors = Symbol.for('plt.runtime.worker.interceptors')
12
+ const kLastELU = Symbol.for('plt.runtime.worker.lastELU')
12
13
 
13
14
  // This string marker should be safe to use since it belongs to Unicode private area
14
15
  const kStderrMarker = '\ue002'
@@ -21,6 +22,7 @@ module.exports = {
21
22
  kWorkerId,
22
23
  kITC,
23
24
  kHealthCheckTimer,
25
+ kLastELU,
24
26
  kWorkerStatus,
25
27
  kStderrMarker,
26
28
  kInterceptors
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "2.69.0",
3
+ "version": "2.70.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -37,12 +37,12 @@
37
37
  "typescript": "^5.5.4",
38
38
  "undici-oidc-interceptor": "^0.5.0",
39
39
  "why-is-node-running": "^2.2.2",
40
- "@platformatic/composer": "2.69.0",
41
- "@platformatic/db": "2.69.0",
42
- "@platformatic/node": "2.69.0",
43
- "@platformatic/service": "2.69.0",
44
- "@platformatic/sql-graphql": "2.69.0",
45
- "@platformatic/sql-mapper": "2.69.0"
40
+ "@platformatic/composer": "2.70.0",
41
+ "@platformatic/db": "2.70.0",
42
+ "@platformatic/node": "2.70.0",
43
+ "@platformatic/service": "2.70.0",
44
+ "@platformatic/sql-graphql": "2.70.0",
45
+ "@platformatic/sql-mapper": "2.70.0"
46
46
  },
47
47
  "dependencies": {
48
48
  "@fastify/accepts": "^5.0.0",
@@ -50,7 +50,6 @@
50
50
  "@fastify/websocket": "^11.0.0",
51
51
  "@hapi/topo": "^6.0.2",
52
52
  "@opentelemetry/api": "^1.8.0",
53
- "@platformatic/http-metrics": "^0.2.1",
54
53
  "@platformatic/undici-cache-memory": "^0.8.1",
55
54
  "@watchable/unpromise": "^1.0.2",
56
55
  "change-case-all": "^2.1.0",
@@ -75,23 +74,24 @@
75
74
  "sonic-boom": "^4.2.0",
76
75
  "tail-file-stream": "^0.2.0",
77
76
  "undici": "^7.0.0",
78
- "undici-thread-interceptor": "^0.13.1",
77
+ "undici-thread-interceptor": "^0.14.0",
79
78
  "ws": "^8.16.0",
80
- "@platformatic/basic": "2.69.0",
81
- "@platformatic/config": "2.69.0",
82
- "@platformatic/generators": "2.69.0",
83
- "@platformatic/itc": "2.69.0",
84
- "@platformatic/telemetry": "2.69.0",
85
- "@platformatic/ts-compiler": "2.69.0",
86
- "@platformatic/utils": "2.69.0"
79
+ "@platformatic/basic": "2.70.0",
80
+ "@platformatic/generators": "2.70.0",
81
+ "@platformatic/metrics": "2.70.0",
82
+ "@platformatic/itc": "2.70.0",
83
+ "@platformatic/telemetry": "2.70.0",
84
+ "@platformatic/ts-compiler": "2.70.0",
85
+ "@platformatic/utils": "2.70.0",
86
+ "@platformatic/config": "2.70.0"
87
87
  },
88
88
  "scripts": {
89
- "test": "pnpm run lint && borp --concurrency=1 --timeout=600000 && tsd",
89
+ "test": "pnpm run lint && borp --concurrency=1 --timeout=1200000 && tsd",
90
90
  "test:main": "borp --concurrency=1 --timeout=300000 test/*.test.js test/*.test.mjs test/versions/*.test.js test/versions/*.test.mjs",
91
91
  "test:api": "borp --concurrency=1 --timeout=300000 test/api/*.test.js test/api/*.test.mjs test/management-api/*.test.js test/management-api/*.test.mjs",
92
92
  "test:cli": "borp --concurrency=1 --timeout=300000 test/cli/*.test.js test/cli/*.test.mjs test/cli/**/*.test.js test/cli/**/*.test.mjs",
93
93
  "test:start": "borp --concurrency=1 --timeout=300000 test/start/*.test.js test/start/*.test.mjs",
94
- "test:multiple-workers": "borp --concurrency=1 --timeout=600000 test/multiple-workers/*.test.js test/multiple-workers/*.test.mjs",
94
+ "test:multiple-workers": "borp --concurrency=1 --timeout=1200000 test/multiple-workers/*.test.js test/multiple-workers/*.test.mjs",
95
95
  "test:types": "tsd",
96
96
  "coverage": "pnpm run lint && borp -X fixtures -X test -C --concurrency=1 --timeout=300000 && tsd",
97
97
  "gen-schema": "node lib/schema.js > schema.json",
package/schema.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/runtime/2.69.0.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/runtime/2.70.0.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "type": "object",
5
5
  "properties": {
@@ -76,6 +76,7 @@
76
76
  },
77
77
  "health": {
78
78
  "type": "object",
79
+ "default": {},
79
80
  "properties": {
80
81
  "enabled": {
81
82
  "anyOf": [
@@ -156,8 +157,15 @@
156
157
  ]
157
158
  },
158
159
  "maxYoungGeneration": {
159
- "type": "number",
160
- "minimum": 0
160
+ "anyOf": [
161
+ {
162
+ "type": "number",
163
+ "minimum": 0
164
+ },
165
+ {
166
+ "type": "string"
167
+ }
168
+ ]
161
169
  }
162
170
  },
163
171
  "additionalProperties": false
@@ -244,6 +252,7 @@
244
252
  },
245
253
  "health": {
246
254
  "type": "object",
255
+ "default": {},
247
256
  "properties": {
248
257
  "enabled": {
249
258
  "anyOf": [
@@ -324,8 +333,15 @@
324
333
  ]
325
334
  },
326
335
  "maxYoungGeneration": {
327
- "type": "number",
328
- "minimum": 0
336
+ "anyOf": [
337
+ {
338
+ "type": "number",
339
+ "minimum": 0
340
+ },
341
+ {
342
+ "type": "string"
343
+ }
344
+ ]
329
345
  }
330
346
  },
331
347
  "additionalProperties": false
@@ -477,6 +493,7 @@
477
493
  },
478
494
  "health": {
479
495
  "type": "object",
496
+ "default": {},
480
497
  "properties": {
481
498
  "enabled": {
482
499
  "anyOf": [
@@ -557,8 +574,15 @@
557
574
  ]
558
575
  },
559
576
  "maxYoungGeneration": {
560
- "type": "number",
561
- "minimum": 0
577
+ "anyOf": [
578
+ {
579
+ "type": "number",
580
+ "minimum": 0
581
+ },
582
+ {
583
+ "type": "string"
584
+ }
585
+ ]
562
586
  }
563
587
  },
564
588
  "additionalProperties": false
@@ -976,7 +1000,6 @@
976
1000
  "default": {},
977
1001
  "properties": {
978
1002
  "enabled": {
979
- "default": true,
980
1003
  "anyOf": [
981
1004
  {
982
1005
  "type": "boolean"
@@ -984,10 +1007,10 @@
984
1007
  {
985
1008
  "type": "string"
986
1009
  }
987
- ]
1010
+ ],
1011
+ "default": true
988
1012
  },
989
1013
  "interval": {
990
- "default": 30000,
991
1014
  "anyOf": [
992
1015
  {
993
1016
  "type": "number",
@@ -996,10 +1019,10 @@
996
1019
  {
997
1020
  "type": "string"
998
1021
  }
999
- ]
1022
+ ],
1023
+ "default": 30000
1000
1024
  },
1001
1025
  "gracePeriod": {
1002
- "default": 30000,
1003
1026
  "anyOf": [
1004
1027
  {
1005
1028
  "type": "number",
@@ -1008,10 +1031,10 @@
1008
1031
  {
1009
1032
  "type": "string"
1010
1033
  }
1011
- ]
1034
+ ],
1035
+ "default": 30000
1012
1036
  },
1013
1037
  "maxUnhealthyChecks": {
1014
- "default": 10,
1015
1038
  "anyOf": [
1016
1039
  {
1017
1040
  "type": "number",
@@ -1020,10 +1043,10 @@
1020
1043
  {
1021
1044
  "type": "string"
1022
1045
  }
1023
- ]
1046
+ ],
1047
+ "default": 10
1024
1048
  },
1025
1049
  "maxELU": {
1026
- "default": 0.99,
1027
1050
  "anyOf": [
1028
1051
  {
1029
1052
  "type": "number",
@@ -1033,10 +1056,10 @@
1033
1056
  {
1034
1057
  "type": "string"
1035
1058
  }
1036
- ]
1059
+ ],
1060
+ "default": 0.99
1037
1061
  },
1038
1062
  "maxHeapUsed": {
1039
- "default": 0.99,
1040
1063
  "anyOf": [
1041
1064
  {
1042
1065
  "type": "number",
@@ -1046,10 +1069,10 @@
1046
1069
  {
1047
1070
  "type": "string"
1048
1071
  }
1049
- ]
1072
+ ],
1073
+ "default": 0.99
1050
1074
  },
1051
1075
  "maxHeapTotal": {
1052
- "default": 4294967296,
1053
1076
  "anyOf": [
1054
1077
  {
1055
1078
  "type": "number",
@@ -1058,11 +1081,19 @@
1058
1081
  {
1059
1082
  "type": "string"
1060
1083
  }
1061
- ]
1084
+ ],
1085
+ "default": 4294967296
1062
1086
  },
1063
1087
  "maxYoungGeneration": {
1064
- "type": "number",
1065
- "minimum": 0
1088
+ "anyOf": [
1089
+ {
1090
+ "type": "number",
1091
+ "minimum": 0
1092
+ },
1093
+ {
1094
+ "type": "string"
1095
+ }
1096
+ ]
1066
1097
  }
1067
1098
  },
1068
1099
  "additionalProperties": false