@platformatic/runtime 3.13.1 → 3.14.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.
@@ -1,23 +1,28 @@
1
- class ScalingAlgorithm {
2
- #scaleUpELU
3
- #scaleDownELU
1
+ export const scaleUpELUThreshold = 0.8
2
+ export const scaleDownELUThreshold = 0.2
3
+ export const scaleUpTimeWindow = 10_000
4
+ export const scaleDownTimeWindow = 60_000
5
+
6
+ export class ScalingAlgorithm {
4
7
  #maxTotalWorkers
5
- #scaleUpTimeWindowSec
6
- #scaleDownTimeWindowSec
7
8
  #appsMetrics
8
9
  #appsConfigs
9
10
 
10
11
  constructor (options = {}) {
11
- this.#scaleUpELU = options.scaleUpELU ?? 0.8
12
- this.#scaleDownELU = options.scaleDownELU ?? 0.2
13
12
  this.#maxTotalWorkers = options.maxTotalWorkers ?? Infinity
14
- this.#scaleUpTimeWindowSec = options.scaleUpTimeWindowSec ?? 10
15
- this.#scaleDownTimeWindowSec = options.scaleDownTimeWindowSec ?? 60
16
13
  this.#appsConfigs = options.applications ?? {}
17
-
18
14
  this.#appsMetrics = {}
19
15
  }
20
16
 
17
+ addApplication (id, config) {
18
+ this.#appsConfigs[id] = config
19
+ }
20
+
21
+ removeApplication (id) {
22
+ delete this.#appsConfigs[id]
23
+ delete this.#appsMetrics[id]
24
+ }
25
+
21
26
  addWorkerHealthInfo (healthInfo) {
22
27
  const { workerId, applicationId, elu, heapUsed } = healthInfo
23
28
  const timestamp = Date.now()
@@ -50,7 +55,7 @@ class ScalingAlgorithm {
50
55
  appsInfo.push({
51
56
  applicationId,
52
57
  workersCount,
53
- avgHeapUsed: heapUsed,
58
+ avgHeapUsed: heapUsed
54
59
  })
55
60
 
56
61
  totalWorkersCount += workersCount
@@ -112,9 +117,7 @@ class ScalingAlgorithm {
112
117
  if (workersCount >= appMaxWorkers) continue
113
118
  if (avgHeapUsed >= totalAvailableMemory) continue
114
119
 
115
- const isScaled = recommendations.some(
116
- r => r.applicationId === applicationId
117
- )
120
+ const isScaled = recommendations.some(r => r.applicationId === applicationId)
118
121
  if (isScaled) continue
119
122
 
120
123
  const recommendation = this.#getApplicationScaleRecommendation(applicationId)
@@ -122,10 +125,8 @@ class ScalingAlgorithm {
122
125
 
123
126
  if (
124
127
  !scaleUpCandidate ||
125
- (recommendation.scaleUpELU > scaleUpCandidate.scaleUpELU) ||
126
- (recommendation.scaleUpELU === scaleUpCandidate.scaleUpELU &&
127
- workersCount < scaleUpCandidate.workersCount
128
- )
128
+ recommendation.scaleUpELU > scaleUpCandidate.scaleUpELU ||
129
+ (recommendation.scaleUpELU === scaleUpCandidate.scaleUpELU && workersCount < scaleUpCandidate.workersCount)
129
130
  ) {
130
131
  scaleUpCandidate = {
131
132
  applicationId,
@@ -186,8 +187,8 @@ class ScalingAlgorithm {
186
187
  count++
187
188
  }
188
189
 
189
- const elu = Math.round(eluSum / count * 100) / 100
190
- const heapUsed = Math.round(heapUsedSum / count * 100) / 100
190
+ const elu = Math.round((eluSum / count) * 100) / 100
191
+ const heapUsed = Math.round((heapUsedSum / count) * 100) / 100
191
192
  return { elu, heapUsed }
192
193
  }
193
194
 
@@ -226,28 +227,22 @@ class ScalingAlgorithm {
226
227
  }
227
228
 
228
229
  #getMetricsTimeWindow () {
229
- return Math.max(this.#scaleUpTimeWindowSec, this.#scaleDownTimeWindowSec) * 1000
230
+ return Math.max(scaleUpTimeWindow, scaleDownTimeWindow) * 1000
230
231
  }
231
232
 
232
233
  #getApplicationScaleRecommendation (applicationId) {
233
- const { elu: scaleUpELU } = this.#calculateAppAvgMetrics(applicationId, {
234
- timeWindow: this.#scaleUpTimeWindowSec * 1000
235
- })
236
- const { elu: scaleDownELU } = this.#calculateAppAvgMetrics(applicationId, {
237
- timeWindow: this.#scaleDownTimeWindowSec * 1000
238
- })
234
+ const { elu: scaleUpELU } = this.#calculateAppAvgMetrics(applicationId, { timeWindow: scaleUpTimeWindow })
235
+ const { elu: scaleDownELU } = this.#calculateAppAvgMetrics(applicationId, { timeWindow: scaleDownTimeWindow })
239
236
  const { heapUsed: avgHeapUsage } = this.#calculateAppAvgMetrics(applicationId)
240
237
 
241
238
  let recommendation = null
242
- if (scaleUpELU > this.#scaleUpELU) {
239
+ if (scaleUpELU > scaleUpELUThreshold) {
243
240
  recommendation = 'scaleUp'
244
241
  }
245
- if (scaleDownELU < this.#scaleDownELU) {
242
+ if (scaleDownELU < scaleDownELUThreshold) {
246
243
  recommendation = 'scaleDown'
247
244
  }
248
245
 
249
246
  return { recommendation, scaleUpELU, scaleDownELU, avgHeapUsage }
250
247
  }
251
248
  }
252
-
253
- export default ScalingAlgorithm
@@ -161,7 +161,7 @@ export class Controller extends EventEmitter {
161
161
  return
162
162
  }
163
163
 
164
- this.capability.updateStatus('starting')
164
+ this.#updateCapabilityStatus('starting')
165
165
  this.emit('starting')
166
166
 
167
167
  if (this.#watch) {
@@ -185,7 +185,7 @@ export class Controller extends EventEmitter {
185
185
  this.#listening = listen
186
186
  /* c8 ignore next 5 */
187
187
  } catch (err) {
188
- this.capability.updateStatus('start:error')
188
+ this.#updateCapabilityStatus('start:error')
189
189
  this.emit('start:error', err)
190
190
 
191
191
  this.capability.log({ message: err.message, level: 'debug' })
@@ -196,7 +196,7 @@ export class Controller extends EventEmitter {
196
196
  this.#started = true
197
197
  this.#starting = false
198
198
 
199
- this.capability.updateStatus('started')
199
+ this.#updateCapabilityStatus('started')
200
200
  this.emit('started')
201
201
  }
202
202
 
@@ -218,7 +218,7 @@ export class Controller extends EventEmitter {
218
218
  this.#starting = false
219
219
  this.#listening = false
220
220
 
221
- this.capability.updateStatus('stopped')
221
+ this.#updateCapabilityStatus('stopped')
222
222
  this.emit('stopped')
223
223
  }
224
224
 
@@ -333,4 +333,14 @@ export class Controller extends EventEmitter {
333
333
  }
334
334
  })
335
335
  }
336
+
337
+ #updateCapabilityStatus (status) {
338
+ if (typeof this.capability.updateStatus === 'function') {
339
+ this.capability.updateStatus(status)
340
+ } else {
341
+ // This is horrible but needed for backward compatibility
342
+ this.capability.status = status
343
+ this.capability.emit(status)
344
+ }
345
+ }
336
346
  }
@@ -0,0 +1,80 @@
1
+ import {
2
+ HealthSignalMustBeObjectError,
3
+ HealthSignalTypeMustBeStringError
4
+ } from '../errors.js'
5
+
6
+ export class HealthSignalsQueue {
7
+ #size
8
+ #values
9
+
10
+ constructor (options = {}) {
11
+ this.#size = options.size ?? 100
12
+ this.#values = []
13
+ }
14
+
15
+ add (value) {
16
+ if (Array.isArray(value)) {
17
+ for (const v of value) {
18
+ this.#values.push(v)
19
+ }
20
+ } else {
21
+ this.#values.push(value)
22
+ }
23
+ if (this.#values.length > this.#size) {
24
+ this.#values.splice(0, this.#values.length - this.#size)
25
+ }
26
+ }
27
+
28
+ getAll () {
29
+ const values = this.#values
30
+ this.#values = []
31
+ return values
32
+ }
33
+ }
34
+
35
+ export function initHealthSignalsApi (options = {}) {
36
+ const queue = new HealthSignalsQueue()
37
+ const timeout = options.timeout ?? 1000
38
+ const workerId = options.workerId
39
+
40
+ let isSending = false
41
+ let promise = null
42
+
43
+ async function sendHealthSignal (signal) {
44
+ if (typeof signal !== 'object') {
45
+ throw new HealthSignalMustBeObjectError()
46
+ }
47
+ if (typeof signal.type !== 'string') {
48
+ throw new HealthSignalTypeMustBeStringError(signal.type)
49
+ }
50
+ if (!signal.timestamp || typeof signal.timestamp !== 'number') {
51
+ signal.timestamp = Date.now()
52
+ }
53
+
54
+ queue.add(signal)
55
+
56
+ if (!isSending) {
57
+ isSending = true
58
+ promise = new Promise((resolve, reject) => {
59
+ setTimeout(async () => {
60
+ isSending = false
61
+ try {
62
+ const signals = queue.getAll()
63
+ await globalThis.platformatic.itc.send('sendHealthSignals', {
64
+ workerId,
65
+ signals
66
+ })
67
+ } catch (err) {
68
+ reject(err)
69
+ return
70
+ }
71
+ resolve()
72
+ }, timeout)
73
+ })
74
+ }
75
+
76
+ return promise
77
+ }
78
+
79
+ globalThis.platformatic.sendHealthSignal = sendHealthSignal
80
+ }
@@ -20,6 +20,7 @@ import { setDispatcher } from './interceptors.js'
20
20
  import { setupITC } from './itc.js'
21
21
  import { SharedContext } from './shared-context.js'
22
22
  import { kId, kITC, kStderrMarker } from './symbols.js'
23
+ import { initHealthSignalsApi } from './health-signals.js'
23
24
 
24
25
  class ForwardingEventEmitter extends EventEmitter {
25
26
  emitAndNotify (event, ...args) {
@@ -190,6 +191,11 @@ async function main () {
190
191
  globalThis[kITC] = itc
191
192
  globalThis.platformatic.itc = itc
192
193
 
194
+ initHealthSignalsApi({
195
+ workerId: workerData.worker.id,
196
+ applicationId: applicationConfig.id
197
+ })
198
+
193
199
  itc.notify('init')
194
200
  }
195
201
 
@@ -5,7 +5,9 @@ export const kApplicationId = Symbol.for('plt.runtime.application.id')
5
5
  export const kWorkerId = Symbol.for('plt.runtime.worker.id')
6
6
  export const kITC = Symbol.for('plt.runtime.itc')
7
7
  export const kHealthCheckTimer = Symbol.for('plt.runtime.worker.healthCheckTimer')
8
+ export const kHealthMetricsTimer = Symbol.for('plt.runtime.worker.healthMetricsTimer')
8
9
  export const kWorkerStatus = Symbol('plt.runtime.worker.status')
10
+ export const kWorkerHealthSignals = Symbol.for('plt.runtime.worker.healthSignals')
9
11
  export const kWorkerStartTime = Symbol.for('plt.runtime.worker.startTime')
10
12
  export const kInterceptors = Symbol.for('plt.runtime.worker.interceptors')
11
13
  export const kLastHealthCheckELU = Symbol.for('plt.runtime.worker.lastHealthCheckELU')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "3.13.1",
3
+ "version": "3.14.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.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"
38
+ "@platformatic/db": "3.14.0",
39
+ "@platformatic/gateway": "3.14.0",
40
+ "@platformatic/composer": "3.14.0",
41
+ "@platformatic/node": "3.14.0",
42
+ "@platformatic/service": "3.14.0",
43
+ "@platformatic/sql-graphql": "3.14.0",
44
+ "@platformatic/sql-mapper": "3.14.0",
45
+ "@platformatic/wattpm-pprof-capture": "3.14.0"
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/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"
76
+ "@platformatic/basic": "3.14.0",
77
+ "@platformatic/metrics": "3.14.0",
78
+ "@platformatic/foundation": "3.14.0",
79
+ "@platformatic/generators": "3.14.0",
80
+ "@platformatic/telemetry": "3.14.0",
81
+ "@platformatic/itc": "3.14.0"
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.13.1.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.14.0.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "title": "Platformatic Runtime Config",
5
5
  "type": "object",
@@ -67,11 +67,27 @@
67
67
  "workers": {
68
68
  "anyOf": [
69
69
  {
70
- "type": "number",
71
- "minimum": 1
70
+ "type": "number"
72
71
  },
73
72
  {
74
73
  "type": "string"
74
+ },
75
+ {
76
+ "type": "object",
77
+ "properties": {
78
+ "static": {
79
+ "type": "number",
80
+ "minimum": 1
81
+ },
82
+ "minimum": {
83
+ "type": "number",
84
+ "minimum": 1
85
+ },
86
+ "maximum": {
87
+ "type": "number",
88
+ "minimum": 0
89
+ }
90
+ }
75
91
  }
76
92
  ]
77
93
  },
@@ -329,11 +345,27 @@
329
345
  "workers": {
330
346
  "anyOf": [
331
347
  {
332
- "type": "number",
333
- "minimum": 1
348
+ "type": "number"
334
349
  },
335
350
  {
336
351
  "type": "string"
352
+ },
353
+ {
354
+ "type": "object",
355
+ "properties": {
356
+ "static": {
357
+ "type": "number",
358
+ "minimum": 1
359
+ },
360
+ "minimum": {
361
+ "type": "number",
362
+ "minimum": 1
363
+ },
364
+ "maximum": {
365
+ "type": "number",
366
+ "minimum": 0
367
+ }
368
+ }
337
369
  }
338
370
  ]
339
371
  },
@@ -589,11 +621,27 @@
589
621
  "workers": {
590
622
  "anyOf": [
591
623
  {
592
- "type": "number",
593
- "minimum": 1
624
+ "type": "number"
594
625
  },
595
626
  {
596
627
  "type": "string"
628
+ },
629
+ {
630
+ "type": "object",
631
+ "properties": {
632
+ "static": {
633
+ "type": "number",
634
+ "minimum": 1
635
+ },
636
+ "minimum": {
637
+ "type": "number",
638
+ "minimum": 1
639
+ },
640
+ "maximum": {
641
+ "type": "number",
642
+ "minimum": 0
643
+ }
644
+ }
597
645
  }
598
646
  ]
599
647
  },
@@ -849,11 +897,27 @@
849
897
  "workers": {
850
898
  "anyOf": [
851
899
  {
852
- "type": "number",
853
- "minimum": 1
900
+ "type": "number"
854
901
  },
855
902
  {
856
903
  "type": "string"
904
+ },
905
+ {
906
+ "type": "object",
907
+ "properties": {
908
+ "static": {
909
+ "type": "number",
910
+ "minimum": 1
911
+ },
912
+ "minimum": {
913
+ "type": "number",
914
+ "minimum": 1
915
+ },
916
+ "maximum": {
917
+ "type": "number",
918
+ "minimum": 0
919
+ }
920
+ }
857
921
  }
858
922
  ]
859
923
  },
@@ -1074,6 +1138,43 @@
1074
1138
  },
1075
1139
  {
1076
1140
  "type": "string"
1141
+ },
1142
+ {
1143
+ "type": "object",
1144
+ "properties": {
1145
+ "static": {
1146
+ "type": "number",
1147
+ "minimum": 1
1148
+ },
1149
+ "dynamic": {
1150
+ "type": "boolean",
1151
+ "default": false
1152
+ },
1153
+ "minimum": {
1154
+ "type": "number",
1155
+ "minimum": 1
1156
+ },
1157
+ "maximum": {
1158
+ "type": "number",
1159
+ "minimum": 0
1160
+ },
1161
+ "total": {
1162
+ "type": "number",
1163
+ "minimum": 1
1164
+ },
1165
+ "maxMemory": {
1166
+ "type": "number",
1167
+ "minimum": 0
1168
+ },
1169
+ "cooldown": {
1170
+ "type": "number",
1171
+ "minimum": 0
1172
+ },
1173
+ "gracePeriod": {
1174
+ "type": "number",
1175
+ "minimum": 0
1176
+ }
1177
+ }
1077
1178
  }
1078
1179
  ]
1079
1180
  },
@@ -1854,6 +1955,58 @@
1854
1955
  }
1855
1956
  ],
1856
1957
  "default": 10000
1958
+ },
1959
+ "otlpExporter": {
1960
+ "type": "object",
1961
+ "description": "Configuration for exporting metrics to an OTLP endpoint",
1962
+ "properties": {
1963
+ "enabled": {
1964
+ "anyOf": [
1965
+ {
1966
+ "type": "boolean"
1967
+ },
1968
+ {
1969
+ "type": "string"
1970
+ }
1971
+ ],
1972
+ "description": "Enable or disable OTLP metrics export"
1973
+ },
1974
+ "endpoint": {
1975
+ "type": "string",
1976
+ "description": "OTLP endpoint URL (e.g., http://collector:4318/v1/metrics)"
1977
+ },
1978
+ "interval": {
1979
+ "anyOf": [
1980
+ {
1981
+ "type": "integer"
1982
+ },
1983
+ {
1984
+ "type": "string"
1985
+ }
1986
+ ],
1987
+ "default": 60000,
1988
+ "description": "Interval in milliseconds between metric pushes"
1989
+ },
1990
+ "headers": {
1991
+ "type": "object",
1992
+ "additionalProperties": {
1993
+ "type": "string"
1994
+ },
1995
+ "description": "Additional HTTP headers for authentication"
1996
+ },
1997
+ "serviceName": {
1998
+ "type": "string",
1999
+ "description": "Service name for OTLP resource attributes"
2000
+ },
2001
+ "serviceVersion": {
2002
+ "type": "string",
2003
+ "description": "Service version for OTLP resource attributes"
2004
+ }
2005
+ },
2006
+ "required": [
2007
+ "endpoint"
2008
+ ],
2009
+ "additionalProperties": false
1857
2010
  }
1858
2011
  },
1859
2012
  "additionalProperties": false
@@ -2013,35 +2166,40 @@
2013
2166
  "type": "number",
2014
2167
  "minimum": 1
2015
2168
  },
2169
+ "cooldownSec": {
2170
+ "type": "number",
2171
+ "minimum": 0
2172
+ },
2173
+ "gracePeriod": {
2174
+ "type": "number",
2175
+ "minimum": 0
2176
+ },
2016
2177
  "scaleUpELU": {
2017
2178
  "type": "number",
2018
2179
  "minimum": 0,
2019
- "maximum": 1
2180
+ "maximum": 1,
2181
+ "deprecated": true
2020
2182
  },
2021
2183
  "scaleDownELU": {
2022
2184
  "type": "number",
2023
2185
  "minimum": 0,
2024
- "maximum": 1
2186
+ "maximum": 1,
2187
+ "deprecated": true
2025
2188
  },
2026
2189
  "timeWindowSec": {
2027
2190
  "type": "number",
2028
- "minimum": 0
2191
+ "minimum": 0,
2192
+ "deprecated": true
2029
2193
  },
2030
2194
  "scaleDownTimeWindowSec": {
2031
2195
  "type": "number",
2032
- "minimum": 0
2033
- },
2034
- "cooldownSec": {
2035
- "type": "number",
2036
- "minimum": 0
2196
+ "minimum": 0,
2197
+ "deprecated": true
2037
2198
  },
2038
2199
  "scaleIntervalSec": {
2039
2200
  "type": "number",
2040
- "minimum": 0
2041
- },
2042
- "gracePeriod": {
2043
- "type": "number",
2044
- "minimum": 0
2201
+ "minimum": 0,
2202
+ "deprecated": true
2045
2203
  },
2046
2204
  "applications": {
2047
2205
  "type": "object",