@platformatic/runtime 3.7.1 → 3.9.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 +18 -0
- package/lib/management-api.js +2 -1
- package/lib/runtime.js +143 -3
- package/lib/scaling-algorithm.js +179 -0
- package/lib/schema.js +12 -0
- package/lib/worker/controller.js +49 -24
- package/lib/worker/itc.js +2 -2
- package/lib/worker/main.js +24 -57
- package/package.json +16 -15
- package/schema.json +68 -3
package/config.d.ts
CHANGED
|
@@ -332,6 +332,24 @@ export type PlatformaticRuntimeConfig = {
|
|
|
332
332
|
[k: string]: unknown;
|
|
333
333
|
};
|
|
334
334
|
};
|
|
335
|
+
verticalScaler?: {
|
|
336
|
+
enabled?: boolean;
|
|
337
|
+
maxTotalWorkers?: number;
|
|
338
|
+
minWorkers?: number;
|
|
339
|
+
maxWorkers?: number;
|
|
340
|
+
scaleUpELU?: number;
|
|
341
|
+
scaleDownELU?: number;
|
|
342
|
+
minELUDiff?: number;
|
|
343
|
+
timeWindowSec?: number;
|
|
344
|
+
cooldownSec?: number;
|
|
345
|
+
scaleIntervalSec?: number;
|
|
346
|
+
applications?: {
|
|
347
|
+
[k: string]: {
|
|
348
|
+
minWorkers?: number;
|
|
349
|
+
maxWorkers?: number;
|
|
350
|
+
};
|
|
351
|
+
};
|
|
352
|
+
};
|
|
335
353
|
inspectorOptions?: {
|
|
336
354
|
host?: string;
|
|
337
355
|
port?: number;
|
package/lib/management-api.js
CHANGED
|
@@ -126,7 +126,8 @@ export async function managementApiPlugin (app, opts) {
|
|
|
126
126
|
const { id } = request.params
|
|
127
127
|
app.log.debug('stop profiling', { id })
|
|
128
128
|
|
|
129
|
-
const
|
|
129
|
+
const options = request.body || {}
|
|
130
|
+
const profileData = await runtime.stopApplicationProfiling(id, options)
|
|
130
131
|
reply.type('application/octet-stream').code(200).send(profileData)
|
|
131
132
|
})
|
|
132
133
|
|
package/lib/runtime.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
kTimeout,
|
|
10
10
|
parseMemorySize
|
|
11
11
|
} from '@platformatic/foundation'
|
|
12
|
+
import os from 'node:os'
|
|
12
13
|
import { ITC } from '@platformatic/itc'
|
|
13
14
|
import fastify from 'fastify'
|
|
14
15
|
import { EventEmitter, once } from 'node:events'
|
|
@@ -44,6 +45,7 @@ import { createSharedStore } from './shared-http-cache.js'
|
|
|
44
45
|
import { version } from './version.js'
|
|
45
46
|
import { sendViaITC, waitEventFromITC } from './worker/itc.js'
|
|
46
47
|
import { RoundRobinMap } from './worker/round-robin-map.js'
|
|
48
|
+
import ScalingAlgorithm from './scaling-algorithm.js'
|
|
47
49
|
import {
|
|
48
50
|
kApplicationId,
|
|
49
51
|
kConfig,
|
|
@@ -179,7 +181,7 @@ export class Runtime extends EventEmitter {
|
|
|
179
181
|
|
|
180
182
|
const workersConfig = []
|
|
181
183
|
for (const application of config.applications) {
|
|
182
|
-
const count = application.workers ?? this.#config.workers
|
|
184
|
+
const count = application.workers ?? this.#config.workers ?? 1
|
|
183
185
|
if (count > 1 && application.entrypoint && !features.node.reusePort) {
|
|
184
186
|
this.logger.warn(
|
|
185
187
|
`"${application.id}" is set as the entrypoint, but reusePort is not available in your OS; setting workers to 1 instead of ${count}`
|
|
@@ -274,6 +276,10 @@ export class Runtime extends EventEmitter {
|
|
|
274
276
|
this.startCollectingMetrics()
|
|
275
277
|
}
|
|
276
278
|
|
|
279
|
+
if (this.#config.verticalScaler?.enabled) {
|
|
280
|
+
this.#setupVerticalScaler()
|
|
281
|
+
}
|
|
282
|
+
|
|
277
283
|
this.#showUrl()
|
|
278
284
|
return this.#url
|
|
279
285
|
}
|
|
@@ -556,11 +562,11 @@ export class Runtime extends EventEmitter {
|
|
|
556
562
|
return sendViaITC(service, 'startProfiling', options)
|
|
557
563
|
}
|
|
558
564
|
|
|
559
|
-
async stopApplicationProfiling (id, ensureStarted = true) {
|
|
565
|
+
async stopApplicationProfiling (id, options = {}, ensureStarted = true) {
|
|
560
566
|
const service = await this.#getApplicationById(id, ensureStarted)
|
|
561
567
|
this.#validatePprofCapturePreload()
|
|
562
568
|
|
|
563
|
-
return sendViaITC(service, 'stopProfiling')
|
|
569
|
+
return sendViaITC(service, 'stopProfiling', options)
|
|
564
570
|
}
|
|
565
571
|
|
|
566
572
|
async updateUndiciInterceptors (undiciConfig) {
|
|
@@ -2434,4 +2440,138 @@ export class Runtime extends EventEmitter {
|
|
|
2434
2440
|
throw new MissingPprofCapture()
|
|
2435
2441
|
}
|
|
2436
2442
|
}
|
|
2443
|
+
|
|
2444
|
+
#setupVerticalScaler () {
|
|
2445
|
+
const isWorkersFixed = this.#config.workers !== undefined
|
|
2446
|
+
if (isWorkersFixed) return
|
|
2447
|
+
|
|
2448
|
+
const scalerConfig = this.#config.verticalScaler
|
|
2449
|
+
|
|
2450
|
+
scalerConfig.maxTotalWorkers ??= os.availableParallelism()
|
|
2451
|
+
scalerConfig.maxWorkers ??= scalerConfig.maxTotalWorkers
|
|
2452
|
+
scalerConfig.minWorkers ??= 1
|
|
2453
|
+
scalerConfig.cooldownSec ??= 60
|
|
2454
|
+
scalerConfig.scaleUpELU ??= 0.8
|
|
2455
|
+
scalerConfig.scaleDownELU ??= 0.2
|
|
2456
|
+
scalerConfig.minELUDiff ??= 0.2
|
|
2457
|
+
scalerConfig.scaleIntervalSec ??= 60
|
|
2458
|
+
scalerConfig.timeWindowSec ??= 60
|
|
2459
|
+
scalerConfig.applications ??= {}
|
|
2460
|
+
|
|
2461
|
+
const maxTotalWorkers = scalerConfig.maxTotalWorkers
|
|
2462
|
+
const maxWorkers = scalerConfig.maxWorkers
|
|
2463
|
+
const minWorkers = scalerConfig.minWorkers
|
|
2464
|
+
const cooldown = scalerConfig.cooldownSec
|
|
2465
|
+
const scaleUpELU = scalerConfig.scaleUpELU
|
|
2466
|
+
const scaleDownELU = scalerConfig.scaleDownELU
|
|
2467
|
+
const minELUDiff = scalerConfig.minELUDiff
|
|
2468
|
+
const scaleIntervalSec = scalerConfig.scaleIntervalSec
|
|
2469
|
+
const timeWindowSec = scalerConfig.timeWindowSec
|
|
2470
|
+
const applicationsConfigs = scalerConfig.applications
|
|
2471
|
+
|
|
2472
|
+
for (const application of this.#config.applications) {
|
|
2473
|
+
if (application.entrypoint && !features.node.reusePort) {
|
|
2474
|
+
applicationsConfigs[application.id] = {
|
|
2475
|
+
minWorkers: 1,
|
|
2476
|
+
maxWorkers: 1
|
|
2477
|
+
}
|
|
2478
|
+
continue
|
|
2479
|
+
}
|
|
2480
|
+
if (application.workers !== undefined) {
|
|
2481
|
+
applicationsConfigs[application.id] = {
|
|
2482
|
+
minWorkers: application.workers,
|
|
2483
|
+
maxWorkers: application.workers
|
|
2484
|
+
}
|
|
2485
|
+
continue
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
applicationsConfigs[application.id] ??= {}
|
|
2489
|
+
applicationsConfigs[application.id].minWorkers ??= minWorkers
|
|
2490
|
+
applicationsConfigs[application.id].maxWorkers ??= maxWorkers
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
for (const applicationId in applicationsConfigs) {
|
|
2494
|
+
const application = this.#config.applications.find(
|
|
2495
|
+
app => app.id === applicationId
|
|
2496
|
+
)
|
|
2497
|
+
if (!application) {
|
|
2498
|
+
delete applicationsConfigs[applicationId]
|
|
2499
|
+
|
|
2500
|
+
this.logger.warn(
|
|
2501
|
+
`Vertical scaler configuration has a configuration for non-existing application "${applicationId}"`
|
|
2502
|
+
)
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
const scalingAlgorithm = new ScalingAlgorithm({
|
|
2507
|
+
maxTotalWorkers,
|
|
2508
|
+
scaleUpELU,
|
|
2509
|
+
scaleDownELU,
|
|
2510
|
+
minELUDiff,
|
|
2511
|
+
timeWindowSec,
|
|
2512
|
+
applications: applicationsConfigs
|
|
2513
|
+
})
|
|
2514
|
+
|
|
2515
|
+
this.on('application:worker:health', async (healthInfo) => {
|
|
2516
|
+
if (!healthInfo) {
|
|
2517
|
+
this.logger.error('No health info received')
|
|
2518
|
+
return
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
scalingAlgorithm.addWorkerHealthInfo(healthInfo)
|
|
2522
|
+
|
|
2523
|
+
if (healthInfo.currentHealth.elu > scaleUpELU) {
|
|
2524
|
+
await checkForScaling()
|
|
2525
|
+
}
|
|
2526
|
+
})
|
|
2527
|
+
|
|
2528
|
+
let isScaling = false
|
|
2529
|
+
let lastScaling = 0
|
|
2530
|
+
|
|
2531
|
+
const checkForScaling = async () => {
|
|
2532
|
+
const isInCooldown = Date.now() < lastScaling + cooldown * 1000
|
|
2533
|
+
if (isScaling || isInCooldown) return
|
|
2534
|
+
isScaling = true
|
|
2535
|
+
|
|
2536
|
+
try {
|
|
2537
|
+
const workersInfo = await this.getWorkers()
|
|
2538
|
+
|
|
2539
|
+
const appsWorkersInfo = {}
|
|
2540
|
+
for (const worker of Object.values(workersInfo)) {
|
|
2541
|
+
if (worker.status === 'exited') continue
|
|
2542
|
+
|
|
2543
|
+
const applicationId = worker.application
|
|
2544
|
+
appsWorkersInfo[applicationId] ??= 0
|
|
2545
|
+
appsWorkersInfo[applicationId]++
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
const recommendations = scalingAlgorithm.getRecommendations(appsWorkersInfo)
|
|
2549
|
+
if (recommendations.length > 0) {
|
|
2550
|
+
await applyRecommendations(recommendations)
|
|
2551
|
+
}
|
|
2552
|
+
} catch (err) {
|
|
2553
|
+
this.logger.error({ err }, 'Failed to scale applications')
|
|
2554
|
+
} finally {
|
|
2555
|
+
isScaling = false
|
|
2556
|
+
lastScaling = Date.now()
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
const applyRecommendations = async (recommendations) => {
|
|
2561
|
+
const resourcesUpdates = []
|
|
2562
|
+
for (const recommendation of recommendations) {
|
|
2563
|
+
const { applicationId, workersCount, direction } = recommendation
|
|
2564
|
+
this.logger.info(`Scaling ${direction} the "${applicationId}" app to ${workersCount} workers`)
|
|
2565
|
+
|
|
2566
|
+
resourcesUpdates.push({
|
|
2567
|
+
application: applicationId,
|
|
2568
|
+
workers: workersCount
|
|
2569
|
+
})
|
|
2570
|
+
}
|
|
2571
|
+
await this.updateApplicationsResources(resourcesUpdates)
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
// Interval for periodic scaling checks
|
|
2575
|
+
setInterval(checkForScaling, scaleIntervalSec * 1000).unref()
|
|
2576
|
+
}
|
|
2437
2577
|
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
class ScalingAlgorithm {
|
|
2
|
+
#scaleUpELU
|
|
3
|
+
#scaleDownELU
|
|
4
|
+
#maxTotalWorkers
|
|
5
|
+
#timeWindowSec
|
|
6
|
+
#appsELUs
|
|
7
|
+
#minELUDiff
|
|
8
|
+
#appsConfigs
|
|
9
|
+
|
|
10
|
+
constructor (options = {}) {
|
|
11
|
+
this.#scaleUpELU = options.scaleUpELU ?? 0.8
|
|
12
|
+
this.#scaleDownELU = options.scaleDownELU ?? 0.2
|
|
13
|
+
this.#maxTotalWorkers = options.maxTotalWorkers
|
|
14
|
+
this.#minELUDiff = options.minELUDiff ?? 0.2
|
|
15
|
+
this.#timeWindowSec = options.timeWindowSec ?? 60
|
|
16
|
+
this.#appsConfigs = options.applications ?? {}
|
|
17
|
+
|
|
18
|
+
this.#appsELUs = {}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
addWorkerHealthInfo (healthInfo) {
|
|
22
|
+
const workerId = healthInfo.id
|
|
23
|
+
const applicationId = healthInfo.application
|
|
24
|
+
const elu = healthInfo.currentHealth.elu
|
|
25
|
+
const timestamp = Date.now()
|
|
26
|
+
|
|
27
|
+
if (!this.#appsELUs[applicationId]) {
|
|
28
|
+
this.#appsELUs[applicationId] = {}
|
|
29
|
+
}
|
|
30
|
+
if (!this.#appsELUs[applicationId][workerId]) {
|
|
31
|
+
this.#appsELUs[applicationId][workerId] = []
|
|
32
|
+
}
|
|
33
|
+
this.#appsELUs[applicationId][workerId].push({ elu, timestamp })
|
|
34
|
+
this.#removeOutdatedAppELUs(applicationId)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getRecommendations (appsWorkersInfo) {
|
|
38
|
+
let totalWorkersCount = 0
|
|
39
|
+
let appsInfo = []
|
|
40
|
+
|
|
41
|
+
for (const applicationId in appsWorkersInfo) {
|
|
42
|
+
const workersCount = appsWorkersInfo[applicationId]
|
|
43
|
+
const elu = this.#calculateAppAvgELU(applicationId)
|
|
44
|
+
appsInfo.push({ applicationId, workersCount, elu })
|
|
45
|
+
totalWorkersCount += workersCount
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
appsInfo = appsInfo.sort(
|
|
49
|
+
(app1, app2) => {
|
|
50
|
+
if (app1.elu > app2.elu) return 1
|
|
51
|
+
if (app1.elu < app2.elu) return -1
|
|
52
|
+
if (app1.workersCount < app2.workersCount) return 1
|
|
53
|
+
if (app1.workersCount > app2.workersCount) return -1
|
|
54
|
+
return 0
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
const recommendations = []
|
|
59
|
+
|
|
60
|
+
for (const { applicationId, elu, workersCount } of appsInfo) {
|
|
61
|
+
const appMinWorkers = this.#appsConfigs[applicationId]?.minWorkers ?? 1
|
|
62
|
+
|
|
63
|
+
if (elu < this.#scaleDownELU && workersCount > appMinWorkers) {
|
|
64
|
+
recommendations.push({
|
|
65
|
+
applicationId,
|
|
66
|
+
workersCount: workersCount - 1,
|
|
67
|
+
direction: 'down'
|
|
68
|
+
})
|
|
69
|
+
totalWorkersCount--
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const scaleUpCandidate of appsInfo.toReversed()) {
|
|
74
|
+
if (scaleUpCandidate.elu < this.#scaleUpELU) break
|
|
75
|
+
|
|
76
|
+
const { applicationId, workersCount } = scaleUpCandidate
|
|
77
|
+
|
|
78
|
+
const appMaxWorkers = this.#appsConfigs[applicationId]?.maxWorkers ?? this.#maxTotalWorkers
|
|
79
|
+
if (workersCount >= appMaxWorkers) continue
|
|
80
|
+
|
|
81
|
+
if (totalWorkersCount >= this.#maxTotalWorkers) {
|
|
82
|
+
let scaleDownCandidate = null
|
|
83
|
+
for (const app of appsInfo) {
|
|
84
|
+
const appMinWorkers = this.#appsConfigs[app.applicationId]?.minWorkers ?? 1
|
|
85
|
+
if (app.workersCount > appMinWorkers) {
|
|
86
|
+
scaleDownCandidate = app
|
|
87
|
+
break
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (scaleDownCandidate) {
|
|
92
|
+
const eluDiff = scaleUpCandidate.elu - scaleDownCandidate.elu
|
|
93
|
+
const workersDiff = scaleDownCandidate.workersCount - scaleUpCandidate.workersCount
|
|
94
|
+
|
|
95
|
+
if (eluDiff >= this.#minELUDiff || workersDiff >= 2) {
|
|
96
|
+
recommendations.push({
|
|
97
|
+
applicationId: scaleDownCandidate.applicationId,
|
|
98
|
+
workersCount: scaleDownCandidate.workersCount - 1,
|
|
99
|
+
direction: 'down'
|
|
100
|
+
})
|
|
101
|
+
recommendations.push({
|
|
102
|
+
applicationId,
|
|
103
|
+
workersCount: workersCount + 1,
|
|
104
|
+
direction: 'up'
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
recommendations.push({
|
|
110
|
+
applicationId,
|
|
111
|
+
workersCount: workersCount + 1,
|
|
112
|
+
direction: 'up'
|
|
113
|
+
})
|
|
114
|
+
totalWorkersCount++
|
|
115
|
+
}
|
|
116
|
+
break
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return recommendations
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#calculateAppAvgELU (applicationId) {
|
|
123
|
+
this.#removeOutdatedAppELUs(applicationId)
|
|
124
|
+
|
|
125
|
+
const appELUs = this.#appsELUs[applicationId]
|
|
126
|
+
if (!appELUs) return 0
|
|
127
|
+
|
|
128
|
+
let eluSum = 0
|
|
129
|
+
let eluCount = 0
|
|
130
|
+
|
|
131
|
+
for (const workerId in appELUs) {
|
|
132
|
+
const workerELUs = appELUs[workerId]
|
|
133
|
+
const workerELUSum = workerELUs.reduce(
|
|
134
|
+
(sum, workerELU) => sum + workerELU.elu, 0
|
|
135
|
+
)
|
|
136
|
+
eluSum += workerELUSum / workerELUs.length
|
|
137
|
+
eluCount++
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (eluCount === 0) return 0
|
|
141
|
+
|
|
142
|
+
return Math.round(eluSum / eluCount * 100) / 100
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#removeOutdatedAppELUs (applicationId) {
|
|
146
|
+
const appELUs = this.#appsELUs[applicationId]
|
|
147
|
+
if (!appELUs) return
|
|
148
|
+
|
|
149
|
+
const now = Date.now()
|
|
150
|
+
|
|
151
|
+
for (const workerId in appELUs) {
|
|
152
|
+
const workerELUs = appELUs[workerId]
|
|
153
|
+
|
|
154
|
+
let firstValidIndex = -1
|
|
155
|
+
for (let i = 0; i < workerELUs.length; i++) {
|
|
156
|
+
const timestamp = workerELUs[i].timestamp
|
|
157
|
+
if (timestamp >= now - this.#timeWindowSec * 1000) {
|
|
158
|
+
firstValidIndex = i
|
|
159
|
+
break
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (firstValidIndex > 0) {
|
|
164
|
+
// Remove all outdated entries before the first valid one
|
|
165
|
+
workerELUs.splice(0, firstValidIndex)
|
|
166
|
+
} else if (firstValidIndex === -1) {
|
|
167
|
+
// All entries are outdated, clear the array
|
|
168
|
+
workerELUs.length = 0
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// If there are no more workerELUs, remove the workerId
|
|
172
|
+
if (workerELUs.length === 0) {
|
|
173
|
+
delete appELUs[workerId]
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export default ScalingAlgorithm
|
package/lib/schema.js
CHANGED
|
@@ -16,6 +16,18 @@ const runtimeLogger = {
|
|
|
16
16
|
|
|
17
17
|
schemaComponents.runtimeProperties.logger = runtimeLogger
|
|
18
18
|
|
|
19
|
+
schemaComponents.runtimeProperties.verticalScaler.properties.applications = {
|
|
20
|
+
type: 'object',
|
|
21
|
+
additionalProperties: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
minWorkers: { type: 'number', minimum: 1 },
|
|
25
|
+
maxWorkers: { type: 'number', minimum: 1 }
|
|
26
|
+
},
|
|
27
|
+
additionalProperties: false
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
const platformaticRuntimeSchema = {
|
|
20
32
|
$id: `https://schemas.platformatic.dev/@platformatic/runtime/${version}.json`,
|
|
21
33
|
$schema: 'http://json-schema.org/draft-07/schema#',
|
package/lib/worker/controller.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ensureLoggableError,
|
|
3
|
+
executeWithTimeout,
|
|
3
4
|
FileWatcher,
|
|
4
5
|
kHandledError,
|
|
5
6
|
listRecognizedConfigurationFiles,
|
|
@@ -24,6 +25,21 @@ function fetchApplicationUrl (application, key) {
|
|
|
24
25
|
return getApplicationUrl(application.id)
|
|
25
26
|
}
|
|
26
27
|
|
|
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}"`
|
|
33
|
+
|
|
34
|
+
globalThis.platformatic.logger.error({ err: ensureLoggableError(err) }, `The ${label} threw an ${type}.`)
|
|
35
|
+
|
|
36
|
+
executeWithTimeout(app?.stop(), 1000)
|
|
37
|
+
.catch()
|
|
38
|
+
.finally(() => {
|
|
39
|
+
process.exit(1)
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
export class Controller extends EventEmitter {
|
|
28
44
|
#starting
|
|
29
45
|
#started
|
|
@@ -34,21 +50,13 @@ export class Controller extends EventEmitter {
|
|
|
34
50
|
#context
|
|
35
51
|
#lastELU
|
|
36
52
|
|
|
37
|
-
constructor (
|
|
38
|
-
appConfig,
|
|
39
|
-
workerId,
|
|
40
|
-
telemetryConfig,
|
|
41
|
-
loggerConfig,
|
|
42
|
-
serverConfig,
|
|
43
|
-
metricsConfig,
|
|
44
|
-
hasManagementApi,
|
|
45
|
-
watch
|
|
46
|
-
) {
|
|
53
|
+
constructor (runtimeConfig, applicationConfig, workerId, serverConfig, metricsConfig) {
|
|
47
54
|
super()
|
|
48
|
-
this.
|
|
49
|
-
this.
|
|
55
|
+
this.runtimeConfig = runtimeConfig
|
|
56
|
+
this.applicationConfig = applicationConfig
|
|
57
|
+
this.applicationId = this.applicationConfig.id
|
|
50
58
|
this.workerId = workerId
|
|
51
|
-
this.#watch = watch
|
|
59
|
+
this.#watch = !!runtimeConfig.watch
|
|
52
60
|
this.#starting = false
|
|
53
61
|
this.#started = false
|
|
54
62
|
this.#listening = false
|
|
@@ -60,17 +68,17 @@ export class Controller extends EventEmitter {
|
|
|
60
68
|
controller: this,
|
|
61
69
|
applicationId: this.applicationId,
|
|
62
70
|
workerId: this.workerId,
|
|
63
|
-
directory: this.
|
|
64
|
-
dependencies: this.
|
|
65
|
-
isEntrypoint: this.
|
|
66
|
-
isProduction: this.
|
|
67
|
-
telemetryConfig,
|
|
71
|
+
directory: this.applicationConfig.path,
|
|
72
|
+
dependencies: this.applicationConfig.dependencies,
|
|
73
|
+
isEntrypoint: this.applicationConfig.entrypoint,
|
|
74
|
+
isProduction: this.applicationConfig.isProduction,
|
|
75
|
+
telemetryConfig: this.applicationConfig.telemetry,
|
|
76
|
+
loggerConfig: runtimeConfig.logger,
|
|
68
77
|
metricsConfig,
|
|
69
|
-
loggerConfig,
|
|
70
78
|
serverConfig,
|
|
71
79
|
worker: workerData?.worker,
|
|
72
|
-
hasManagementApi: !!
|
|
73
|
-
fetchApplicationUrl: fetchApplicationUrl.bind(null,
|
|
80
|
+
hasManagementApi: !!runtimeConfig.managementApi,
|
|
81
|
+
fetchApplicationUrl: fetchApplicationUrl.bind(null, applicationConfig)
|
|
74
82
|
}
|
|
75
83
|
}
|
|
76
84
|
|
|
@@ -90,7 +98,7 @@ export class Controller extends EventEmitter {
|
|
|
90
98
|
// Note: capability's init() is executed within start
|
|
91
99
|
async init () {
|
|
92
100
|
try {
|
|
93
|
-
const appConfig = this.
|
|
101
|
+
const appConfig = this.applicationConfig
|
|
94
102
|
|
|
95
103
|
if (appConfig.isProduction && !process.env.NODE_ENV) {
|
|
96
104
|
process.env.NODE_ENV = 'production'
|
|
@@ -120,6 +128,10 @@ export class Controller extends EventEmitter {
|
|
|
120
128
|
}
|
|
121
129
|
|
|
122
130
|
this.#updateDispatcher()
|
|
131
|
+
|
|
132
|
+
if (this.capability.exitOnUnhandledErrors && this.runtimeConfig.exitOnUnhandledErrors) {
|
|
133
|
+
this.#setupHandlers()
|
|
134
|
+
}
|
|
123
135
|
} catch (err) {
|
|
124
136
|
if (err.validationErrors) {
|
|
125
137
|
globalThis.platformatic.logger.error(
|
|
@@ -168,7 +180,7 @@ export class Controller extends EventEmitter {
|
|
|
168
180
|
}
|
|
169
181
|
}
|
|
170
182
|
|
|
171
|
-
const listen = !!this.
|
|
183
|
+
const listen = !!this.applicationConfig.useHttp
|
|
172
184
|
|
|
173
185
|
try {
|
|
174
186
|
await this.capability.start({ listen })
|
|
@@ -203,7 +215,7 @@ export class Controller extends EventEmitter {
|
|
|
203
215
|
|
|
204
216
|
async listen () {
|
|
205
217
|
// This server is not an entrypoint or already listened in start. Behave as no-op.
|
|
206
|
-
if (!this.
|
|
218
|
+
if (!this.applicationConfig.entrypoint || this.applicationConfig.useHttp || this.#listening) {
|
|
207
219
|
return
|
|
208
220
|
}
|
|
209
221
|
|
|
@@ -299,4 +311,17 @@ export class Controller extends EventEmitter {
|
|
|
299
311
|
|
|
300
312
|
setGlobalDispatcher(dispatcher)
|
|
301
313
|
}
|
|
314
|
+
|
|
315
|
+
#setupHandlers () {
|
|
316
|
+
process.on('uncaughtException', handleUnhandled.bind(null, this, 'uncaught exception'))
|
|
317
|
+
process.on('unhandledRejection', handleUnhandled.bind(null, this, 'unhandled rejection'))
|
|
318
|
+
|
|
319
|
+
process.on('newListener', event => {
|
|
320
|
+
if (event === 'uncaughtException' || event === 'unhandledRejection') {
|
|
321
|
+
globalThis.platformatic.logger.warn(
|
|
322
|
+
`A listener has been added for the "process.${event}" event. This listener will be never triggered as Watt default behavior will kill the process before.\n To disable this behavior, set "exitOnUnhandledErrors" to false in the runtime config.`
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
})
|
|
326
|
+
}
|
|
302
327
|
}
|
package/lib/worker/itc.js
CHANGED
|
@@ -71,7 +71,7 @@ export async function waitEventFromITC (worker, event) {
|
|
|
71
71
|
|
|
72
72
|
export function setupITC (controller, application, dispatcher, sharedContext) {
|
|
73
73
|
const logger = globalThis.platformatic.logger
|
|
74
|
-
const messaging = new MessagingITC(controller.
|
|
74
|
+
const messaging = new MessagingITC(controller.applicationConfig.id, workerData.config, logger)
|
|
75
75
|
|
|
76
76
|
Object.assign(globalThis.platformatic ?? {}, {
|
|
77
77
|
messaging: {
|
|
@@ -82,7 +82,7 @@ export function setupITC (controller, application, dispatcher, sharedContext) {
|
|
|
82
82
|
})
|
|
83
83
|
|
|
84
84
|
const itc = new ITC({
|
|
85
|
-
name: controller.
|
|
85
|
+
name: controller.applicationConfig.id + '-worker',
|
|
86
86
|
port: parentPort,
|
|
87
87
|
handlers: {
|
|
88
88
|
async start () {
|
package/lib/worker/main.js
CHANGED
|
@@ -2,8 +2,6 @@ import {
|
|
|
2
2
|
buildPinoFormatters,
|
|
3
3
|
buildPinoTimestamp,
|
|
4
4
|
disablePinoDirectWrite,
|
|
5
|
-
ensureLoggableError,
|
|
6
|
-
executeWithTimeout,
|
|
7
5
|
getPrivateSymbol
|
|
8
6
|
} from '@platformatic/foundation'
|
|
9
7
|
import dotenv from 'dotenv'
|
|
@@ -30,21 +28,6 @@ class ForwardingEventEmitter extends EventEmitter {
|
|
|
30
28
|
}
|
|
31
29
|
}
|
|
32
30
|
|
|
33
|
-
function handleUnhandled (app, type, err) {
|
|
34
|
-
const label =
|
|
35
|
-
workerData.worker.count > 1
|
|
36
|
-
? `worker ${workerData.worker.index} of the application "${workerData.applicationConfig.id}"`
|
|
37
|
-
: `application "${workerData.applicationConfig.id}"`
|
|
38
|
-
|
|
39
|
-
globalThis.platformatic.logger.error({ err: ensureLoggableError(err) }, `The ${label} threw an ${type}.`)
|
|
40
|
-
|
|
41
|
-
executeWithTimeout(app?.stop(), 1000)
|
|
42
|
-
.catch()
|
|
43
|
-
.finally(() => {
|
|
44
|
-
process.exit(1)
|
|
45
|
-
})
|
|
46
|
-
}
|
|
47
|
-
|
|
48
31
|
function patchLogging () {
|
|
49
32
|
disablePinoDirectWrite()
|
|
50
33
|
|
|
@@ -113,16 +96,16 @@ async function main () {
|
|
|
113
96
|
events: new ForwardingEventEmitter()
|
|
114
97
|
})
|
|
115
98
|
|
|
116
|
-
const
|
|
99
|
+
const runtimeConfig = workerData.config
|
|
117
100
|
|
|
118
|
-
await performPreloading(
|
|
101
|
+
await performPreloading(runtimeConfig, workerData.applicationConfig)
|
|
119
102
|
|
|
120
|
-
const
|
|
103
|
+
const applicationConfig = workerData.applicationConfig
|
|
121
104
|
|
|
122
105
|
// Load env file and mixin env vars from application config
|
|
123
106
|
let envfile
|
|
124
|
-
if (
|
|
125
|
-
envfile = resolve(workerData.dirname,
|
|
107
|
+
if (applicationConfig.envfile) {
|
|
108
|
+
envfile = resolve(workerData.dirname, applicationConfig.envfile)
|
|
126
109
|
} else {
|
|
127
110
|
envfile = resolve(workerData.applicationConfig.path, '.env')
|
|
128
111
|
}
|
|
@@ -133,20 +116,20 @@ async function main () {
|
|
|
133
116
|
path: envfile
|
|
134
117
|
})
|
|
135
118
|
|
|
136
|
-
if (
|
|
137
|
-
Object.assign(process.env,
|
|
119
|
+
if (runtimeConfig.env) {
|
|
120
|
+
Object.assign(process.env, runtimeConfig.env)
|
|
138
121
|
}
|
|
139
|
-
if (
|
|
140
|
-
Object.assign(process.env,
|
|
122
|
+
if (applicationConfig.env) {
|
|
123
|
+
Object.assign(process.env, applicationConfig.env)
|
|
141
124
|
}
|
|
142
125
|
|
|
143
|
-
const { threadDispatcher } = await setDispatcher(
|
|
126
|
+
const { threadDispatcher } = await setDispatcher(runtimeConfig)
|
|
144
127
|
|
|
145
128
|
// If the application is an entrypoint and runtime server config is defined, use it.
|
|
146
129
|
let serverConfig = null
|
|
147
|
-
if (
|
|
148
|
-
serverConfig =
|
|
149
|
-
} else if (
|
|
130
|
+
if (runtimeConfig.server && applicationConfig.entrypoint) {
|
|
131
|
+
serverConfig = runtimeConfig.server
|
|
132
|
+
} else if (applicationConfig.useHttp) {
|
|
150
133
|
serverConfig = {
|
|
151
134
|
port: 0,
|
|
152
135
|
hostname: '127.0.0.1',
|
|
@@ -169,48 +152,32 @@ async function main () {
|
|
|
169
152
|
const res = await fetch(url)
|
|
170
153
|
const [{ devtoolsFrontendUrl }] = await res.json()
|
|
171
154
|
|
|
172
|
-
console.log(`For ${
|
|
155
|
+
console.log(`For ${applicationConfig.id} debugger open the following in chrome: "${devtoolsFrontendUrl}"`)
|
|
173
156
|
}
|
|
174
157
|
|
|
175
158
|
// Create the application
|
|
176
159
|
// Add idLabel to metrics config to determine which label name to use (defaults to applicationId)
|
|
177
|
-
const metricsConfig =
|
|
160
|
+
const metricsConfig = runtimeConfig.metrics
|
|
178
161
|
? {
|
|
179
|
-
...
|
|
180
|
-
idLabel:
|
|
162
|
+
...runtimeConfig.metrics,
|
|
163
|
+
idLabel: runtimeConfig.metrics.applicationLabel || 'applicationId'
|
|
181
164
|
}
|
|
182
|
-
:
|
|
165
|
+
: runtimeConfig.metrics
|
|
183
166
|
|
|
184
167
|
const controller = new Controller(
|
|
185
|
-
|
|
168
|
+
runtimeConfig,
|
|
169
|
+
applicationConfig,
|
|
186
170
|
workerData.worker.count > 1 ? workerData.worker.index : undefined,
|
|
187
|
-
application.telemetry,
|
|
188
|
-
config.logger,
|
|
189
171
|
serverConfig,
|
|
190
|
-
metricsConfig
|
|
191
|
-
!!config.managementApi,
|
|
192
|
-
!!config.watch
|
|
172
|
+
metricsConfig
|
|
193
173
|
)
|
|
194
174
|
|
|
195
|
-
if (config.exitOnUnhandledErrors) {
|
|
196
|
-
process.on('uncaughtException', handleUnhandled.bind(null, controller, 'uncaught exception'))
|
|
197
|
-
process.on('unhandledRejection', handleUnhandled.bind(null, controller, 'unhandled rejection'))
|
|
198
|
-
|
|
199
|
-
process.on('newListener', event => {
|
|
200
|
-
if (event === 'uncaughtException' || event === 'unhandledRejection') {
|
|
201
|
-
globalThis.platformatic.logger.warn(
|
|
202
|
-
`A listener has been added for the "process.${event}" event. This listener will be never triggered as Watt default behavior will kill the process before.\n To disable this behavior, set "exitOnUnhandledErrors" to false in the runtime config.`
|
|
203
|
-
)
|
|
204
|
-
}
|
|
205
|
-
})
|
|
206
|
-
}
|
|
207
|
-
|
|
208
175
|
await controller.init()
|
|
209
176
|
|
|
210
|
-
if (
|
|
177
|
+
if (applicationConfig.entrypoint && runtimeConfig.basePath) {
|
|
211
178
|
const meta = await controller.capability.getMeta()
|
|
212
179
|
if (!meta.gateway.wantsAbsoluteUrls) {
|
|
213
|
-
stripBasePath(
|
|
180
|
+
stripBasePath(runtimeConfig.basePath)
|
|
214
181
|
}
|
|
215
182
|
}
|
|
216
183
|
|
|
@@ -222,7 +189,7 @@ async function main () {
|
|
|
222
189
|
}
|
|
223
190
|
|
|
224
191
|
// Setup interaction with parent port
|
|
225
|
-
const itc = setupITC(controller,
|
|
192
|
+
const itc = setupITC(controller, applicationConfig, threadDispatcher, sharedContext)
|
|
226
193
|
globalThis[kITC] = itc
|
|
227
194
|
globalThis.platformatic.itc = itc
|
|
228
195
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/runtime",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"@fastify/express": "^4.0.0",
|
|
20
20
|
"@fastify/formbody": "^8.0.0",
|
|
21
21
|
"autocannon": "^8.0.0",
|
|
22
|
+
"atomic-sleep": "^1.0.0",
|
|
22
23
|
"c8": "^10.0.0",
|
|
23
24
|
"cleaner-spec-reporter": "^0.5.0",
|
|
24
25
|
"eslint": "9",
|
|
@@ -34,14 +35,14 @@
|
|
|
34
35
|
"typescript": "^5.5.4",
|
|
35
36
|
"undici-oidc-interceptor": "^0.5.0",
|
|
36
37
|
"why-is-node-running": "^2.2.2",
|
|
37
|
-
"@platformatic/composer": "3.
|
|
38
|
-
"@platformatic/db": "3.
|
|
39
|
-
"@platformatic/gateway": "3.
|
|
40
|
-
"@platformatic/
|
|
41
|
-
"@platformatic/
|
|
42
|
-
"@platformatic/sql-
|
|
43
|
-
"@platformatic/
|
|
44
|
-
"@platformatic/wattpm-pprof-capture": "3.
|
|
38
|
+
"@platformatic/composer": "3.9.0",
|
|
39
|
+
"@platformatic/db": "3.9.0",
|
|
40
|
+
"@platformatic/gateway": "3.9.0",
|
|
41
|
+
"@platformatic/sql-graphql": "3.9.0",
|
|
42
|
+
"@platformatic/node": "3.9.0",
|
|
43
|
+
"@platformatic/sql-mapper": "3.9.0",
|
|
44
|
+
"@platformatic/service": "3.9.0",
|
|
45
|
+
"@platformatic/wattpm-pprof-capture": "3.9.0"
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
47
48
|
"@fastify/accepts": "^5.0.0",
|
|
@@ -71,12 +72,12 @@
|
|
|
71
72
|
"undici": "^7.0.0",
|
|
72
73
|
"undici-thread-interceptor": "^0.14.0",
|
|
73
74
|
"ws": "^8.16.0",
|
|
74
|
-
"@platformatic/foundation": "3.
|
|
75
|
-
"@platformatic/
|
|
76
|
-
"@platformatic/itc": "3.
|
|
77
|
-
"@platformatic/
|
|
78
|
-
"@platformatic/metrics": "3.
|
|
79
|
-
"@platformatic/telemetry": "3.
|
|
75
|
+
"@platformatic/foundation": "3.9.0",
|
|
76
|
+
"@platformatic/generators": "3.9.0",
|
|
77
|
+
"@platformatic/itc": "3.9.0",
|
|
78
|
+
"@platformatic/basic": "3.9.0",
|
|
79
|
+
"@platformatic/metrics": "3.9.0",
|
|
80
|
+
"@platformatic/telemetry": "3.9.0"
|
|
80
81
|
},
|
|
81
82
|
"engines": {
|
|
82
83
|
"node": ">=22.19.0"
|
package/schema.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.
|
|
2
|
+
"$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.9.0.json",
|
|
3
3
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
4
|
"title": "Platformatic Runtime Config",
|
|
5
5
|
"type": "object",
|
|
@@ -923,8 +923,7 @@
|
|
|
923
923
|
{
|
|
924
924
|
"type": "string"
|
|
925
925
|
}
|
|
926
|
-
]
|
|
927
|
-
"default": 1
|
|
926
|
+
]
|
|
928
927
|
},
|
|
929
928
|
"workersRestartDelay": {
|
|
930
929
|
"anyOf": [
|
|
@@ -1828,6 +1827,72 @@
|
|
|
1828
1827
|
],
|
|
1829
1828
|
"additionalProperties": false
|
|
1830
1829
|
},
|
|
1830
|
+
"verticalScaler": {
|
|
1831
|
+
"type": "object",
|
|
1832
|
+
"properties": {
|
|
1833
|
+
"enabled": {
|
|
1834
|
+
"type": "boolean",
|
|
1835
|
+
"default": true
|
|
1836
|
+
},
|
|
1837
|
+
"maxTotalWorkers": {
|
|
1838
|
+
"type": "number",
|
|
1839
|
+
"minimum": 1
|
|
1840
|
+
},
|
|
1841
|
+
"minWorkers": {
|
|
1842
|
+
"type": "number",
|
|
1843
|
+
"minimum": 1
|
|
1844
|
+
},
|
|
1845
|
+
"maxWorkers": {
|
|
1846
|
+
"type": "number",
|
|
1847
|
+
"minimum": 1
|
|
1848
|
+
},
|
|
1849
|
+
"scaleUpELU": {
|
|
1850
|
+
"type": "number",
|
|
1851
|
+
"minimum": 0,
|
|
1852
|
+
"maximum": 1
|
|
1853
|
+
},
|
|
1854
|
+
"scaleDownELU": {
|
|
1855
|
+
"type": "number",
|
|
1856
|
+
"minimum": 0,
|
|
1857
|
+
"maximum": 1
|
|
1858
|
+
},
|
|
1859
|
+
"minELUDiff": {
|
|
1860
|
+
"type": "number",
|
|
1861
|
+
"minimum": 0,
|
|
1862
|
+
"maximum": 1
|
|
1863
|
+
},
|
|
1864
|
+
"timeWindowSec": {
|
|
1865
|
+
"type": "number",
|
|
1866
|
+
"minimum": 0
|
|
1867
|
+
},
|
|
1868
|
+
"cooldownSec": {
|
|
1869
|
+
"type": "number",
|
|
1870
|
+
"minimum": 0
|
|
1871
|
+
},
|
|
1872
|
+
"scaleIntervalSec": {
|
|
1873
|
+
"type": "number",
|
|
1874
|
+
"minimum": 0
|
|
1875
|
+
},
|
|
1876
|
+
"applications": {
|
|
1877
|
+
"type": "object",
|
|
1878
|
+
"additionalProperties": {
|
|
1879
|
+
"type": "object",
|
|
1880
|
+
"properties": {
|
|
1881
|
+
"minWorkers": {
|
|
1882
|
+
"type": "number",
|
|
1883
|
+
"minimum": 1
|
|
1884
|
+
},
|
|
1885
|
+
"maxWorkers": {
|
|
1886
|
+
"type": "number",
|
|
1887
|
+
"minimum": 1
|
|
1888
|
+
}
|
|
1889
|
+
},
|
|
1890
|
+
"additionalProperties": false
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
},
|
|
1894
|
+
"additionalProperties": false
|
|
1895
|
+
},
|
|
1831
1896
|
"inspectorOptions": {
|
|
1832
1897
|
"type": "object",
|
|
1833
1898
|
"properties": {
|