@platformatic/runtime 3.13.0 → 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
@@ -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
 
@@ -164,7 +161,7 @@ export class Controller extends EventEmitter {
164
161
  return
165
162
  }
166
163
 
167
- this.capability.updateStatus('starting')
164
+ this.#updateCapabilityStatus('starting')
168
165
  this.emit('starting')
169
166
 
170
167
  if (this.#watch) {
@@ -188,7 +185,7 @@ export class Controller extends EventEmitter {
188
185
  this.#listening = listen
189
186
  /* c8 ignore next 5 */
190
187
  } catch (err) {
191
- this.capability.updateStatus('start:error')
188
+ this.#updateCapabilityStatus('start:error')
192
189
  this.emit('start:error', err)
193
190
 
194
191
  this.capability.log({ message: err.message, level: 'debug' })
@@ -199,7 +196,7 @@ export class Controller extends EventEmitter {
199
196
  this.#started = true
200
197
  this.#starting = false
201
198
 
202
- this.capability.updateStatus('started')
199
+ this.#updateCapabilityStatus('started')
203
200
  this.emit('started')
204
201
  }
205
202
 
@@ -221,7 +218,7 @@ export class Controller extends EventEmitter {
221
218
  this.#starting = false
222
219
  this.#listening = false
223
220
 
224
- this.capability.updateStatus('stopped')
221
+ this.#updateCapabilityStatus('stopped')
225
222
  this.emit('stopped')
226
223
  }
227
224
 
@@ -336,4 +333,14 @@ export class Controller extends EventEmitter {
336
333
  }
337
334
  })
338
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
+ }
339
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) {
@@ -59,13 +60,10 @@ function createLogger () {
59
60
  const pinoOptions = {
60
61
  level: 'trace',
61
62
  name: workerData.applicationConfig.id,
63
+ base: { pid: process.pid, hostname: hostname(), worker: workerData.worker.index },
62
64
  ...workerData.config.logger
63
65
  }
64
66
 
65
- if (workerData.worker?.count > 1) {
66
- pinoOptions.base = { pid: process.pid, hostname: hostname(), worker: workerData.worker.index }
67
- }
68
-
69
67
  if (pinoOptions.formatters) {
70
68
  pinoOptions.formatters = buildPinoFormatters(pinoOptions.formatters)
71
69
  }
@@ -167,7 +165,7 @@ async function main () {
167
165
  const controller = new Controller(
168
166
  runtimeConfig,
169
167
  applicationConfig,
170
- workerData.worker.count > 1 ? workerData.worker.index : undefined,
168
+ workerData.worker.index,
171
169
  serverConfig,
172
170
  metricsConfig
173
171
  )
@@ -193,6 +191,11 @@ async function main () {
193
191
  globalThis[kITC] = itc
194
192
  globalThis.platformatic.itc = itc
195
193
 
194
+ initHealthSignalsApi({
195
+ workerId: workerData.worker.id,
196
+ applicationId: applicationConfig.id
197
+ })
198
+
196
199
  itc.notify('init')
197
200
  }
198
201
 
@@ -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
  }
@@ -5,10 +5,13 @@ 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
- export const kLastELU = Symbol.for('plt.runtime.worker.lastELU')
13
+ export const kLastHealthCheckELU = Symbol.for('plt.runtime.worker.lastHealthCheckELU')
14
+ export const kLastVerticalScalerELU = Symbol.for('plt.runtime.worker.lastVerticalScalerELU')
12
15
 
13
16
  // This string marker should be safe to use since it belongs to Unicode private area
14
17
  export const kStderrMarker = '\ue002'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "3.13.0",
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/gateway": "3.13.0",
39
- "@platformatic/db": "3.13.0",
40
- "@platformatic/node": "3.13.0",
41
- "@platformatic/composer": "3.13.0",
42
- "@platformatic/service": "3.13.0",
43
- "@platformatic/sql-graphql": "3.13.0",
44
- "@platformatic/sql-mapper": "3.13.0",
45
- "@platformatic/wattpm-pprof-capture": "3.13.0"
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.0",
77
- "@platformatic/foundation": "3.13.0",
78
- "@platformatic/generators": "3.13.0",
79
- "@platformatic/itc": "3.13.0",
80
- "@platformatic/metrics": "3.13.0",
81
- "@platformatic/telemetry": "3.13.0"
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"