@platformatic/runtime 3.29.0 → 3.30.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
@@ -28,6 +28,8 @@ export type PlatformaticRuntimeConfig = {
28
28
  static?: number;
29
29
  minimum?: number;
30
30
  maximum?: number;
31
+ scaleUpELU?: number;
32
+ scaleDownELU?: number;
31
33
  [k: string]: unknown;
32
34
  };
33
35
  health?: {
@@ -100,6 +102,8 @@ export type PlatformaticRuntimeConfig = {
100
102
  maxMemory?: number;
101
103
  cooldown?: number;
102
104
  gracePeriod?: number;
105
+ scaleUpELU?: number;
106
+ scaleDownELU?: number;
103
107
  [k: string]: unknown;
104
108
  };
105
109
  workersRestartDelay?: number | string;
@@ -433,13 +437,7 @@ export type PlatformaticRuntimeConfig = {
433
437
  maxWorkers?: number;
434
438
  cooldownSec?: number;
435
439
  gracePeriod?: number;
436
- /**
437
- * @deprecated
438
- */
439
440
  scaleUpELU?: number;
440
- /**
441
- * @deprecated
442
- */
443
441
  scaleDownELU?: number;
444
442
  /**
445
443
  * @deprecated
@@ -457,6 +455,8 @@ export type PlatformaticRuntimeConfig = {
457
455
  [k: string]: {
458
456
  minWorkers?: number;
459
457
  maxWorkers?: number;
458
+ scaleUpELU?: number;
459
+ scaleDownELU?: number;
460
460
  };
461
461
  };
462
462
  };
package/lib/config.js CHANGED
@@ -298,12 +298,40 @@ export async function transform (config, _, context) {
298
298
  config.workers.maximum ??= config.verticalScaler.maxWorkers ?? config.verticalScaler.maxTotalWorkers ?? 1
299
299
  config.workers.maxMemory ??= config.verticalScaler.maxTotalMemory
300
300
 
301
- if (typeof config.workers.cooldown === 'undefined' && typeof config.verticalScaler.cooldownSec === 'number') {
302
- config.workers.cooldown = config.verticalScaler.cooldownSec * 1000
301
+ if (config.verticalScaler.cooldownSec) {
302
+ config.workers.cooldown ??= config.verticalScaler.cooldownSec * 1000
303
303
  }
304
+ if (config.verticalScaler.gracePeriod) {
305
+ config.workers.gracePeriod ??= config.verticalScaler.gracePeriod
306
+ }
307
+ if (config.verticalScaler.scaleUpELU) {
308
+ config.workers.scaleUpELU ??= config.verticalScaler.scaleUpELU
309
+ }
310
+ if (config.verticalScaler.scaleDownELU) {
311
+ config.workers.scaleDownELU ??= config.verticalScaler.scaleDownELU
312
+ }
313
+
314
+ if (config.verticalScaler.applications) {
315
+ for (const appId in config.verticalScaler.applications) {
316
+ let appConfig = applications.find((app) => app.id === appId)
317
+ if (!appConfig) {
318
+ appConfig = { id: appId, workers: {} }
319
+ applications.push(appConfig)
320
+ }
304
321
 
305
- if (typeof config.workers.gracePeriod === 'undefined' && typeof config.verticalScaler.gracePeriod === 'number') {
306
- config.workers.gracePeriod = config.verticalScaler.gracePeriod
322
+ const scaleConfig = config.verticalScaler.applications[appId]
323
+ const workersConfig = appConfig.workers
324
+
325
+ workersConfig.minimum ??= scaleConfig.minWorkers ?? config.workers.minimum
326
+ workersConfig.maximum ??= scaleConfig.maxWorkers ?? config.workers.maximum
327
+
328
+ if (scaleConfig.scaleUpELU) {
329
+ workersConfig.scaleUpELU ??= scaleConfig.scaleUpELU
330
+ }
331
+ if (scaleConfig.scaleDownELU) {
332
+ workersConfig.scaleDownELU ??= scaleConfig.scaleDownELU
333
+ }
334
+ }
307
335
  }
308
336
 
309
337
  config.verticalScaler = undefined
package/lib/runtime.js CHANGED
@@ -710,6 +710,56 @@ export class Runtime extends EventEmitter {
710
710
  }
711
711
  }
712
712
 
713
+ /**
714
+ * Updates the metrics configuration at runtime without restarting the runtime or workers.
715
+ *
716
+ * This method allows you to:
717
+ * - Enable or disable metrics collection
718
+ * - Change Prometheus server settings (port, endpoint, authentication)
719
+ * - Update custom labels for metrics
720
+ *
721
+ * @example
722
+ * // Enable metrics with custom port
723
+ * await runtime.updateMetricsConfig({
724
+ * enabled: true,
725
+ * port: 9091,
726
+ * labels: { environment: 'production' }
727
+ * })
728
+ *
729
+ * // Disable metrics
730
+ * await runtime.updateMetricsConfig({ enabled: false })
731
+ */
732
+ async updateMetricsConfig (metricsConfig) {
733
+ if (this.#prometheusServer) {
734
+ await this.#prometheusServer.close()
735
+ this.#prometheusServer = null
736
+ }
737
+
738
+ this.#config.metrics = metricsConfig
739
+ this.#metricsLabelName = metricsConfig?.applicationLabel || 'applicationId'
740
+
741
+ if (metricsConfig.enabled !== false) {
742
+ this.#prometheusServer = await startPrometheusServer(this, metricsConfig)
743
+ }
744
+
745
+ const promises = []
746
+ for (const worker of this.#workers.values()) {
747
+ if (worker[kWorkerStatus] === 'started') {
748
+ promises.push(sendViaITC(worker, 'updateMetricsConfig', metricsConfig))
749
+ }
750
+ }
751
+
752
+ const results = await Promise.allSettled(promises)
753
+ for (const result of results) {
754
+ if (result.status === 'rejected') {
755
+ this.logger.error({ err: result.reason }, 'Cannot update metrics config on worker')
756
+ }
757
+ }
758
+
759
+ this.logger.info({ metricsConfig }, 'Metrics configuration updated')
760
+ return { success: true, config: metricsConfig }
761
+ }
762
+
713
763
  // TODO: Remove in next major version
714
764
  startCollectingMetrics () {
715
765
  this.logger.warn(
@@ -1,5 +1,3 @@
1
- export const scaleUpELUThreshold = 0.8
2
- export const scaleDownELUThreshold = 0.2
3
1
  export const scaleUpTimeWindow = 10_000
4
2
  export const scaleDownTimeWindow = 60_000
5
3
 
@@ -7,15 +5,23 @@ export class ScalingAlgorithm {
7
5
  #maxTotalWorkers
8
6
  #appsMetrics
9
7
  #appsConfigs
8
+ #scaleUpELU
9
+ #scaleDownELU
10
10
 
11
11
  constructor (options = {}) {
12
12
  this.#maxTotalWorkers = options.maxTotalWorkers ?? Infinity
13
13
  this.#appsConfigs = options.applications ?? {}
14
14
  this.#appsMetrics = {}
15
+ this.#scaleUpELU = options.scaleUpELU ?? 0.8
16
+ this.#scaleDownELU = options.scaleDownELU ?? 0.2
15
17
  }
16
18
 
17
- addApplication (id, config) {
19
+ addApplication (id, config = {}) {
20
+ config.scaleUpELU ??= this.#scaleUpELU
21
+ config.scaleDownELU ??= this.#scaleDownELU
22
+
18
23
  this.#appsConfigs[id] = config
24
+ this.#appsMetrics[id] ??= {}
19
25
  }
20
26
 
21
27
  removeApplication (id) {
@@ -28,8 +34,9 @@ export class ScalingAlgorithm {
28
34
  const timestamp = Date.now()
29
35
 
30
36
  if (!this.#appsMetrics[applicationId]) {
31
- this.#appsMetrics[applicationId] = {}
37
+ throw new Error(`Missing application "${applicationId}" in the scaling algorithm.`)
32
38
  }
39
+
33
40
  if (!this.#appsMetrics[applicationId][workerId]) {
34
41
  this.#appsMetrics[applicationId][workerId] = []
35
42
  }
@@ -234,12 +241,13 @@ export class ScalingAlgorithm {
234
241
  const { elu: scaleUpELU } = this.#calculateAppAvgMetrics(applicationId, { timeWindow: scaleUpTimeWindow })
235
242
  const { elu: scaleDownELU } = this.#calculateAppAvgMetrics(applicationId, { timeWindow: scaleDownTimeWindow })
236
243
  const { heapUsed: avgHeapUsage } = this.#calculateAppAvgMetrics(applicationId)
244
+ const config = this.#appsConfigs[applicationId]
237
245
 
238
246
  let recommendation = null
239
- if (scaleUpELU > scaleUpELUThreshold) {
247
+ if (scaleUpELU > config.scaleUpELU) {
240
248
  recommendation = 'scaleUp'
241
249
  }
242
- if (scaleDownELU < scaleDownELUThreshold) {
250
+ if (scaleDownELU < config.scaleDownELU) {
243
251
  recommendation = 'scaleDown'
244
252
  }
245
253
 
package/lib/schema.js CHANGED
@@ -22,7 +22,9 @@ schemaComponents.runtimeProperties.verticalScaler.properties.applications = {
22
22
  type: 'object',
23
23
  properties: {
24
24
  minWorkers: { type: 'number', minimum: 1 },
25
- maxWorkers: { type: 'number', minimum: 1 }
25
+ maxWorkers: { type: 'number', minimum: 1 },
26
+ scaleUpELU: { type: 'number', minimum: 0, maximum: 1 },
27
+ scaleDownELU: { type: 'number', minimum: 0, maximum: 1 }
26
28
  },
27
29
  additionalProperties: false
28
30
  }
@@ -92,6 +92,13 @@ export class Controller extends EventEmitter {
92
92
  }
93
93
  }
94
94
 
95
+ async updateMetricsConfig (metricsConfig) {
96
+ this.#context.metricsConfig = metricsConfig
97
+ if (this.capability && typeof this.capability.updateMetricsConfig === 'function') {
98
+ await this.capability.updateMetricsConfig(metricsConfig)
99
+ }
100
+ }
101
+
95
102
  // Note: capability's init() is executed within start
96
103
  async init () {
97
104
  try {
package/lib/worker/itc.js CHANGED
@@ -2,8 +2,8 @@ import { ensureLoggableError, executeInParallel, executeWithTimeout, kTimeout }
2
2
  import { ITC } from '@platformatic/itc'
3
3
  import { Unpromise } from '@watchable/unpromise'
4
4
  import { once } from 'node:events'
5
- import repl from 'node:repl'
6
5
  import { Duplex } from 'node:stream'
6
+ import { createRequire } from 'node:module'
7
7
  import { parentPort, workerData } from 'node:worker_threads'
8
8
  import {
9
9
  ApplicationExitedError,
@@ -176,6 +176,13 @@ export function setupITC (controller, application, dispatcher, sharedContext) {
176
176
  await updateUndiciInterceptors(undiciConfig)
177
177
  },
178
178
 
179
+ async updateMetricsConfig (metricsConfig) {
180
+ if (controller && typeof controller.updateMetricsConfig === 'function') {
181
+ await controller.updateMetricsConfig(metricsConfig)
182
+ }
183
+ return { success: true }
184
+ },
185
+
179
186
  async updateWorkersCount (data) {
180
187
  const { workers } = data
181
188
  workerData.applicationConfig.workers = workers
@@ -266,6 +273,14 @@ export function setupITC (controller, application, dispatcher, sharedContext) {
266
273
  },
267
274
 
268
275
  startRepl (port) {
276
+ // We are loading the repl module dynamically here to avoid loading it
277
+ // when not needed (since it pulls in domain, which is quite expensive
278
+ // as it monkey patches EventEmitter).
279
+ // We must use local require() instead of import
280
+ // because dynamic import() is async and the
281
+ // startRepl handler is sync.
282
+ const repl = createRequire(import.meta.url)('node:repl')
283
+
269
284
  // Create a duplex stream that wraps the MessagePort
270
285
  const replStream = new Duplex({
271
286
  read () {},
@@ -1,7 +1,7 @@
1
1
  import { features } from '@platformatic/foundation'
2
2
  import { availableParallelism } from 'node:os'
3
3
  import { getMemoryInfo } from './metrics.js'
4
- import { ScalingAlgorithm, scaleUpELUThreshold } from './scaling-algorithm.js'
4
+ import { ScalingAlgorithm } from './scaling-algorithm.js'
5
5
  import { kApplicationId, kId, kLastWorkerScalerELU, kWorkerStartTime, kWorkerStatus } from './worker/symbols.js'
6
6
 
7
7
  const healthCheckInterval = 1000
@@ -20,6 +20,8 @@ export class DynamicWorkersScaler {
20
20
  #maxTotalWorkers
21
21
  #maxWorkers
22
22
  #minWorkers
23
+ #scaleUpELU
24
+ #scaleDownELU
23
25
  #cooldown
24
26
  #gracePeriod
25
27
 
@@ -29,6 +31,7 @@ export class DynamicWorkersScaler {
29
31
  #checkScalingInterval
30
32
  #isScaling
31
33
  #lastScaling
34
+ #appsConfigs
32
35
 
33
36
  constructor (runtime, config) {
34
37
  this.#runtime = runtime
@@ -37,15 +40,22 @@ export class DynamicWorkersScaler {
37
40
  this.#maxTotalWorkers = config.total ?? availableParallelism()
38
41
  this.#maxWorkers = config.maximum ?? this.#maxTotalWorkers
39
42
  this.#minWorkers = config.minimum ?? 1
43
+ this.#scaleUpELU = config.scaleUpELU ?? 0.8
44
+ this.#scaleDownELU = config.scaleDownELU ?? 0.2
40
45
  this.#cooldown = config.cooldown ?? defaultCooldown
41
46
  this.#gracePeriod = config.gracePeriod ?? defaultGracePeriod
42
47
 
43
- this.#algorithm = new ScalingAlgorithm({ maxTotalWorkers: this.#maxTotalWorkers })
48
+ this.#algorithm = new ScalingAlgorithm({
49
+ maxTotalWorkers: this.#maxTotalWorkers,
50
+ scaleUpELU: this.#scaleUpELU,
51
+ scaleDownELU: this.#scaleDownELU
52
+ })
44
53
 
45
54
  this.#isScaling = false
46
55
  this.#lastScaling = 0
47
56
  this.#initialUpdates = []
48
57
  this.#status = 'init'
58
+ this.#appsConfigs = {}
49
59
  }
50
60
 
51
61
  getConfig () {
@@ -100,10 +110,14 @@ export class DynamicWorkersScaler {
100
110
  } else {
101
111
  config.minWorkers = application.workers.minimum
102
112
  config.maxWorkers = application.workers.maximum
113
+ config.scaleUpELU = application.workers.scaleUpELU
114
+ config.scaleDownELU = application.workers.scaleDownELU
103
115
  }
104
116
 
105
117
  config.minWorkers ??= this.#minWorkers
106
118
  config.maxWorkers ??= this.#maxWorkers
119
+ config.scaleUpELU ??= this.#scaleUpELU
120
+ config.scaleDownELU ??= this.#scaleDownELU
107
121
 
108
122
  if (config.minWorkers > 1) {
109
123
  const update = { application: application.id, workers: config.minWorkers }
@@ -115,6 +129,7 @@ export class DynamicWorkersScaler {
115
129
  }
116
130
  }
117
131
 
132
+ this.#appsConfigs[application.id] = config
118
133
  this.#algorithm.addApplication(application.id, config)
119
134
  }
120
135
 
@@ -143,15 +158,20 @@ export class DynamicWorkersScaler {
143
158
 
144
159
  worker[kLastWorkerScalerELU] = health.currentELU
145
160
 
161
+ const workerId = worker[kId]
162
+ const applicationId = worker[kApplicationId]
163
+ const scaleConfig = this.#appsConfigs[applicationId]
164
+ if (!scaleConfig) continue
165
+
146
166
  this.#algorithm.addWorkerHealthInfo({
147
- workerId: worker[kId],
148
- applicationId: worker[kApplicationId],
167
+ workerId,
168
+ applicationId,
149
169
  elu: health.elu,
150
170
  heapUsed: health.heapUsed,
151
171
  heapTotal: health.heapTotal
152
172
  })
153
173
 
154
- if (health.elu > scaleUpELUThreshold) {
174
+ if (health.elu > scaleConfig.scaleUpELU) {
155
175
  shouldCheckForScaling = true
156
176
  }
157
177
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "3.29.0",
3
+ "version": "3.30.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -35,14 +35,14 @@
35
35
  "typescript": "^5.5.4",
36
36
  "undici-oidc-interceptor": "^0.5.0",
37
37
  "why-is-node-running": "^2.2.2",
38
- "@platformatic/composer": "3.29.0",
39
- "@platformatic/db": "3.29.0",
40
- "@platformatic/node": "3.29.0",
41
- "@platformatic/service": "3.29.0",
42
- "@platformatic/sql-mapper": "3.29.0",
43
- "@platformatic/sql-graphql": "3.29.0",
44
- "@platformatic/wattpm-pprof-capture": "3.29.0",
45
- "@platformatic/gateway": "3.29.0"
38
+ "@platformatic/composer": "3.30.0",
39
+ "@platformatic/db": "3.30.0",
40
+ "@platformatic/gateway": "3.30.0",
41
+ "@platformatic/node": "3.30.0",
42
+ "@platformatic/sql-graphql": "3.30.0",
43
+ "@platformatic/sql-mapper": "3.30.0",
44
+ "@platformatic/service": "3.30.0",
45
+ "@platformatic/wattpm-pprof-capture": "3.30.0"
46
46
  },
47
47
  "dependencies": {
48
48
  "@fastify/accepts": "^5.0.0",
@@ -58,7 +58,7 @@
58
58
  "cron": "^4.1.0",
59
59
  "debounce": "^2.0.0",
60
60
  "fastest-levenshtein": "^1.0.16",
61
- "fastify": "^5.0.0",
61
+ "fastify": "^5.7.0",
62
62
  "graphql": "^16.8.1",
63
63
  "help-me": "^5.0.0",
64
64
  "minimist": "^1.2.8",
@@ -71,12 +71,12 @@
71
71
  "undici": "^7.0.0",
72
72
  "undici-thread-interceptor": "^1.0.0",
73
73
  "ws": "^8.16.0",
74
- "@platformatic/basic": "3.29.0",
75
- "@platformatic/foundation": "3.29.0",
76
- "@platformatic/generators": "3.29.0",
77
- "@platformatic/itc": "3.29.0",
78
- "@platformatic/telemetry": "3.29.0",
79
- "@platformatic/metrics": "3.29.0"
74
+ "@platformatic/foundation": "3.30.0",
75
+ "@platformatic/basic": "3.30.0",
76
+ "@platformatic/generators": "3.30.0",
77
+ "@platformatic/itc": "3.30.0",
78
+ "@platformatic/telemetry": "3.30.0",
79
+ "@platformatic/metrics": "3.30.0"
80
80
  },
81
81
  "engines": {
82
82
  "node": ">=22.19.0"
package/schema.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.29.0.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.30.0.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "title": "Platformatic Runtime Config",
5
5
  "type": "object",
@@ -90,6 +90,16 @@
90
90
  "maximum": {
91
91
  "type": "number",
92
92
  "minimum": 0
93
+ },
94
+ "scaleUpELU": {
95
+ "type": "number",
96
+ "minimum": 0,
97
+ "maximum": 1
98
+ },
99
+ "scaleDownELU": {
100
+ "type": "number",
101
+ "minimum": 0,
102
+ "maximum": 1
93
103
  }
94
104
  }
95
105
  }
@@ -394,6 +404,16 @@
394
404
  "maximum": {
395
405
  "type": "number",
396
406
  "minimum": 0
407
+ },
408
+ "scaleUpELU": {
409
+ "type": "number",
410
+ "minimum": 0,
411
+ "maximum": 1
412
+ },
413
+ "scaleDownELU": {
414
+ "type": "number",
415
+ "minimum": 0,
416
+ "maximum": 1
397
417
  }
398
418
  }
399
419
  }
@@ -696,6 +716,16 @@
696
716
  "maximum": {
697
717
  "type": "number",
698
718
  "minimum": 0
719
+ },
720
+ "scaleUpELU": {
721
+ "type": "number",
722
+ "minimum": 0,
723
+ "maximum": 1
724
+ },
725
+ "scaleDownELU": {
726
+ "type": "number",
727
+ "minimum": 0,
728
+ "maximum": 1
699
729
  }
700
730
  }
701
731
  }
@@ -998,6 +1028,16 @@
998
1028
  "maximum": {
999
1029
  "type": "number",
1000
1030
  "minimum": 0
1031
+ },
1032
+ "scaleUpELU": {
1033
+ "type": "number",
1034
+ "minimum": 0,
1035
+ "maximum": 1
1036
+ },
1037
+ "scaleDownELU": {
1038
+ "type": "number",
1039
+ "minimum": 0,
1040
+ "maximum": 1
1001
1041
  }
1002
1042
  }
1003
1043
  }
@@ -1277,6 +1317,16 @@
1277
1317
  "gracePeriod": {
1278
1318
  "type": "number",
1279
1319
  "minimum": 0
1320
+ },
1321
+ "scaleUpELU": {
1322
+ "type": "number",
1323
+ "minimum": 0,
1324
+ "maximum": 1
1325
+ },
1326
+ "scaleDownELU": {
1327
+ "type": "number",
1328
+ "minimum": 0,
1329
+ "maximum": 1
1280
1330
  }
1281
1331
  }
1282
1332
  }
@@ -2313,14 +2363,12 @@
2313
2363
  "scaleUpELU": {
2314
2364
  "type": "number",
2315
2365
  "minimum": 0,
2316
- "maximum": 1,
2317
- "deprecated": true
2366
+ "maximum": 1
2318
2367
  },
2319
2368
  "scaleDownELU": {
2320
2369
  "type": "number",
2321
2370
  "minimum": 0,
2322
- "maximum": 1,
2323
- "deprecated": true
2371
+ "maximum": 1
2324
2372
  },
2325
2373
  "timeWindowSec": {
2326
2374
  "type": "number",
@@ -2349,6 +2397,16 @@
2349
2397
  "maxWorkers": {
2350
2398
  "type": "number",
2351
2399
  "minimum": 1
2400
+ },
2401
+ "scaleUpELU": {
2402
+ "type": "number",
2403
+ "minimum": 0,
2404
+ "maximum": 1
2405
+ },
2406
+ "scaleDownELU": {
2407
+ "type": "number",
2408
+ "minimum": 0,
2409
+ "maximum": 1
2352
2410
  }
2353
2411
  },
2354
2412
  "additionalProperties": false