@platformatic/runtime 3.28.2 → 3.29.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/config.d.ts 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/errors.js CHANGED
@@ -144,3 +144,7 @@ export const CannotRemoveEntrypointError = createError(
144
144
  `${ERROR_PREFIX}_CANNOT_REMOVE_ENTRYPOINT`,
145
145
  'Cannot remove the entrypoint application.'
146
146
  )
147
+ export const WorkerInterceptorNotReadyError = createError(
148
+ `${ERROR_PREFIX}_WORKER_INTERCEPTOR_NOT_READY`,
149
+ 'The "%s" application worker interceptor is not ready'
150
+ )
package/lib/runtime.js CHANGED
@@ -35,7 +35,8 @@ import {
35
35
  MissingEntrypointError,
36
36
  MissingPprofCapture,
37
37
  RuntimeAbortedError,
38
- WorkerNotFoundError
38
+ WorkerNotFoundError,
39
+ WorkerInterceptorNotReadyError
39
40
  } from './errors.js'
40
41
  import { abstractLogger, createLogger } from './logger.js'
41
42
  import { startManagementApi } from './management-api.js'
@@ -61,7 +62,8 @@ import {
61
62
  kWorkerId,
62
63
  kWorkersBroadcast,
63
64
  kWorkerStartTime,
64
- kWorkerStatus
65
+ kWorkerStatus,
66
+ kInterceptorReadyPromise
65
67
  } from './worker/symbols.js'
66
68
 
67
69
  const kWorkerFile = join(import.meta.dirname, 'worker/main.js')
@@ -1640,11 +1642,13 @@ export class Runtime extends EventEmitter {
1640
1642
  if (enabled) {
1641
1643
  // Store locally
1642
1644
  this.#workers.set(workerId, worker)
1643
-
1644
- // Setup the interceptor
1645
- this.#meshInterceptor.route(applicationId, worker)
1646
1645
  }
1647
1646
 
1647
+ // Setup the interceptor
1648
+ // kInterceptorReadyPromise resolves when the worker
1649
+ // is ready to receive requests: after calling the replaceServer method
1650
+ worker[kInterceptorReadyPromise] = this.#meshInterceptor.route(applicationId, worker)
1651
+
1648
1652
  // Wait for initialization
1649
1653
  await waitEventFromITC(worker, 'init')
1650
1654
 
@@ -1884,6 +1888,8 @@ export class Runtime extends EventEmitter {
1884
1888
  this.#url = workerUrl
1885
1889
  }
1886
1890
 
1891
+ await this.#waitForWorkerInterceptor(worker)
1892
+
1887
1893
  worker[kWorkerStatus] = 'started'
1888
1894
  worker[kWorkerStartTime] = Date.now()
1889
1895
 
@@ -2034,7 +2040,7 @@ export class Runtime extends EventEmitter {
2034
2040
  }
2035
2041
 
2036
2042
  async #discardWorker (worker) {
2037
- this.#meshInterceptor.unroute(worker[kApplicationId], worker, true)
2043
+ await this.#meshInterceptor.unroute(worker[kApplicationId], worker, true)
2038
2044
  worker.removeAllListeners('exit')
2039
2045
  await worker.terminate()
2040
2046
 
@@ -2128,7 +2134,6 @@ export class Runtime extends EventEmitter {
2128
2134
  }
2129
2135
 
2130
2136
  this.#workers.set(workerId, newWorker)
2131
- this.#meshInterceptor.route(applicationId, newWorker)
2132
2137
  } catch (e) {
2133
2138
  newWorker?.terminate?.()
2134
2139
  throw e
@@ -2336,7 +2341,7 @@ export class Runtime extends EventEmitter {
2336
2341
 
2337
2342
  let pinoLog
2338
2343
 
2339
- if (typeof message === 'object') {
2344
+ if (message !== null && typeof message === 'object') {
2340
2345
  pinoLog =
2341
2346
  typeof message.level === 'number' &&
2342
2347
  // We want to accept both pino raw time (number) and time as formatted string
@@ -2794,4 +2799,24 @@ export class Runtime extends EventEmitter {
2794
2799
 
2795
2800
  this.#loggerContext.updatePrefixes(ids)
2796
2801
  }
2802
+
2803
+ async #waitForWorkerInterceptor (worker, timeout = 10000) {
2804
+ const workerId = worker[kId]
2805
+ const applicationId = worker[kApplicationId]
2806
+
2807
+ const interceptorReadyTimeout = setTimeout(() => {
2808
+ this.logger.error(
2809
+ { applicationId, workerId },
2810
+ 'The worker interceptor is not ready after 10s'
2811
+ )
2812
+ throw new WorkerInterceptorNotReadyError(applicationId)
2813
+ }, timeout)
2814
+
2815
+ try {
2816
+ await worker[kInterceptorReadyPromise]
2817
+ worker[kInterceptorReadyPromise] = null
2818
+ } finally {
2819
+ clearTimeout(interceptorReadyTimeout)
2820
+ }
2821
+ }
2797
2822
  }
@@ -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
  }
@@ -11,6 +11,7 @@ export const kWorkerStartTime = Symbol.for('plt.runtime.worker.startTime')
11
11
  export const kInterceptors = Symbol.for('plt.runtime.worker.interceptors')
12
12
  export const kLastHealthCheckELU = Symbol.for('plt.runtime.worker.lastHealthCheckELU')
13
13
  export const kLastWorkerScalerELU = Symbol.for('plt.runtime.worker.lastWorkerScalerELU')
14
+ export const kInterceptorReadyPromise = Symbol.for('plt.runtime.worker.interceptorReadyPromise')
14
15
 
15
16
  // This string marker should be safe to use since it belongs to Unicode private area
16
17
  export const kStderrMarker = '\ue002'
@@ -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.28.2",
3
+ "version": "3.29.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/db": "3.28.2",
39
- "@platformatic/composer": "3.28.2",
40
- "@platformatic/gateway": "3.28.2",
41
- "@platformatic/node": "3.28.2",
42
- "@platformatic/sql-graphql": "3.28.2",
43
- "@platformatic/service": "3.28.2",
44
- "@platformatic/sql-mapper": "3.28.2",
45
- "@platformatic/wattpm-pprof-capture": "3.28.2"
38
+ "@platformatic/composer": "3.29.1",
39
+ "@platformatic/db": "3.29.1",
40
+ "@platformatic/node": "3.29.1",
41
+ "@platformatic/gateway": "3.29.1",
42
+ "@platformatic/service": "3.29.1",
43
+ "@platformatic/sql-graphql": "3.29.1",
44
+ "@platformatic/sql-mapper": "3.29.1",
45
+ "@platformatic/wattpm-pprof-capture": "3.29.1"
46
46
  },
47
47
  "dependencies": {
48
48
  "@fastify/accepts": "^5.0.0",
@@ -69,14 +69,14 @@
69
69
  "sonic-boom": "^4.2.0",
70
70
  "systeminformation": "^5.27.11",
71
71
  "undici": "^7.0.0",
72
- "undici-thread-interceptor": "^0.15.0",
72
+ "undici-thread-interceptor": "^1.0.0",
73
73
  "ws": "^8.16.0",
74
- "@platformatic/basic": "3.28.2",
75
- "@platformatic/generators": "3.28.2",
76
- "@platformatic/foundation": "3.28.2",
77
- "@platformatic/itc": "3.28.2",
78
- "@platformatic/metrics": "3.28.2",
79
- "@platformatic/telemetry": "3.28.2"
74
+ "@platformatic/basic": "3.29.1",
75
+ "@platformatic/generators": "3.29.1",
76
+ "@platformatic/itc": "3.29.1",
77
+ "@platformatic/foundation": "3.29.1",
78
+ "@platformatic/metrics": "3.29.1",
79
+ "@platformatic/telemetry": "3.29.1"
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.28.2.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.29.1.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