@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.
- package/config.d.ts +71 -4
- package/index.d.ts +6 -1
- package/index.js +1 -1
- package/lib/config.js +118 -72
- package/lib/dynamic-workers-scaler.js +218 -0
- package/lib/errors.js +21 -0
- package/lib/logger.js +4 -2
- package/lib/prom-server.js +2 -4
- package/lib/runtime.js +448 -476
- package/lib/scaling-algorithm.js +26 -31
- package/lib/worker/controller.js +15 -8
- package/lib/worker/health-signals.js +80 -0
- package/lib/worker/main.js +8 -5
- package/lib/worker/messaging.js +0 -6
- package/lib/worker/round-robin-map.js +39 -41
- package/lib/worker/symbols.js +4 -1
- package/package.json +15 -15
- package/schema.json +180 -22
package/config.d.ts
CHANGED
|
@@ -20,7 +20,15 @@ export type PlatformaticRuntimeConfig = {
|
|
|
20
20
|
id: string;
|
|
21
21
|
config?: string;
|
|
22
22
|
useHttp?: boolean;
|
|
23
|
-
workers?:
|
|
23
|
+
workers?:
|
|
24
|
+
| number
|
|
25
|
+
| string
|
|
26
|
+
| {
|
|
27
|
+
static?: number;
|
|
28
|
+
minimum?: number;
|
|
29
|
+
maximum?: number;
|
|
30
|
+
[k: string]: unknown;
|
|
31
|
+
};
|
|
24
32
|
health?: {
|
|
25
33
|
enabled?: boolean | string;
|
|
26
34
|
interval?: number | string;
|
|
@@ -76,7 +84,20 @@ export type PlatformaticRuntimeConfig = {
|
|
|
76
84
|
web?: {
|
|
77
85
|
[k: string]: unknown;
|
|
78
86
|
}[];
|
|
79
|
-
workers?:
|
|
87
|
+
workers?:
|
|
88
|
+
| number
|
|
89
|
+
| string
|
|
90
|
+
| {
|
|
91
|
+
static?: number;
|
|
92
|
+
dynamic?: boolean;
|
|
93
|
+
minimum?: number;
|
|
94
|
+
maximum?: number;
|
|
95
|
+
total?: number;
|
|
96
|
+
maxMemory?: number;
|
|
97
|
+
cooldown?: number;
|
|
98
|
+
gracePeriod?: number;
|
|
99
|
+
[k: string]: unknown;
|
|
100
|
+
};
|
|
80
101
|
workersRestartDelay?: number | string;
|
|
81
102
|
logger?: {
|
|
82
103
|
level: (
|
|
@@ -285,6 +306,37 @@ export type PlatformaticRuntimeConfig = {
|
|
|
285
306
|
};
|
|
286
307
|
plugins?: string[];
|
|
287
308
|
timeout?: number | string;
|
|
309
|
+
/**
|
|
310
|
+
* Configuration for exporting metrics to an OTLP endpoint
|
|
311
|
+
*/
|
|
312
|
+
otlpExporter?: {
|
|
313
|
+
/**
|
|
314
|
+
* Enable or disable OTLP metrics export
|
|
315
|
+
*/
|
|
316
|
+
enabled?: boolean | string;
|
|
317
|
+
/**
|
|
318
|
+
* OTLP endpoint URL (e.g., http://collector:4318/v1/metrics)
|
|
319
|
+
*/
|
|
320
|
+
endpoint: string;
|
|
321
|
+
/**
|
|
322
|
+
* Interval in milliseconds between metric pushes
|
|
323
|
+
*/
|
|
324
|
+
interval?: number | string;
|
|
325
|
+
/**
|
|
326
|
+
* Additional HTTP headers for authentication
|
|
327
|
+
*/
|
|
328
|
+
headers?: {
|
|
329
|
+
[k: string]: string;
|
|
330
|
+
};
|
|
331
|
+
/**
|
|
332
|
+
* Service name for OTLP resource attributes
|
|
333
|
+
*/
|
|
334
|
+
serviceName?: string;
|
|
335
|
+
/**
|
|
336
|
+
* Service version for OTLP resource attributes
|
|
337
|
+
*/
|
|
338
|
+
serviceVersion?: string;
|
|
339
|
+
};
|
|
288
340
|
};
|
|
289
341
|
telemetry?: {
|
|
290
342
|
enabled?: boolean | string;
|
|
@@ -368,13 +420,28 @@ export type PlatformaticRuntimeConfig = {
|
|
|
368
420
|
maxTotalMemory?: number;
|
|
369
421
|
minWorkers?: number;
|
|
370
422
|
maxWorkers?: number;
|
|
423
|
+
cooldownSec?: number;
|
|
424
|
+
gracePeriod?: number;
|
|
425
|
+
/**
|
|
426
|
+
* @deprecated
|
|
427
|
+
*/
|
|
371
428
|
scaleUpELU?: number;
|
|
429
|
+
/**
|
|
430
|
+
* @deprecated
|
|
431
|
+
*/
|
|
372
432
|
scaleDownELU?: number;
|
|
433
|
+
/**
|
|
434
|
+
* @deprecated
|
|
435
|
+
*/
|
|
373
436
|
timeWindowSec?: number;
|
|
437
|
+
/**
|
|
438
|
+
* @deprecated
|
|
439
|
+
*/
|
|
374
440
|
scaleDownTimeWindowSec?: number;
|
|
375
|
-
|
|
441
|
+
/**
|
|
442
|
+
* @deprecated
|
|
443
|
+
*/
|
|
376
444
|
scaleIntervalSec?: number;
|
|
377
|
-
gracePeriod?: number;
|
|
378
445
|
applications?: {
|
|
379
446
|
[k: string]: {
|
|
380
447
|
minWorkers?: number;
|
package/index.d.ts
CHANGED
|
@@ -55,8 +55,11 @@ export module symbols {
|
|
|
55
55
|
export declare const kWorkerId: unique symbol
|
|
56
56
|
export declare const kITC: unique symbol
|
|
57
57
|
export declare const kHealthCheckTimer: unique symbol
|
|
58
|
-
export declare const
|
|
58
|
+
export declare const kHealthMetricsTimer: unique symbol
|
|
59
|
+
export declare const kLastHealthCheckELU: unique symbol
|
|
60
|
+
export declare const kLastVerticalScalerELU: unique symbol
|
|
59
61
|
export declare const kWorkerStatus: unique symbol
|
|
62
|
+
export declare const kWorkerHealthSignals: unique symbol
|
|
60
63
|
export declare const kStderrMarker: string
|
|
61
64
|
export declare const kInterceptors: unique symbol
|
|
62
65
|
export declare const kWorkersBroadcast: unique symbol
|
|
@@ -89,6 +92,8 @@ export function create (
|
|
|
89
92
|
context?: ConfigurationOptions
|
|
90
93
|
): Promise<Runtime>
|
|
91
94
|
|
|
95
|
+
export declare function prepareApplication (config: RuntimeConfiguration, application: object): object
|
|
96
|
+
|
|
92
97
|
export declare function transform (config: RuntimeConfiguration): Promise<RuntimeConfiguration>
|
|
93
98
|
|
|
94
99
|
export declare function loadApplicationsCommands (): Promise<ApplicationsCommands>
|
package/index.js
CHANGED
|
@@ -168,7 +168,7 @@ export async function create (configOrRoot, sourceOrConfig, context) {
|
|
|
168
168
|
return runtime
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
export { transform, wrapInRuntimeConfig } from './lib/config.js'
|
|
171
|
+
export { prepareApplication, transform, wrapInRuntimeConfig } from './lib/config.js'
|
|
172
172
|
export * as errors from './lib/errors.js'
|
|
173
173
|
export { RuntimeGenerator as Generator, WrappedGenerator } from './lib/generator.js'
|
|
174
174
|
export { Runtime } from './lib/runtime.js'
|
package/lib/config.js
CHANGED
|
@@ -45,6 +45,42 @@ function raiseInvalidWorkersError (location, received, hint) {
|
|
|
45
45
|
throw new InvalidArgumentError(`${location} workers must be a positive integer; received "${received}"${extra}`)
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function parseWorkers (config, prefix, defaultWorkers = 1) {
|
|
49
|
+
if (typeof config.workers !== 'undefined') {
|
|
50
|
+
// Number
|
|
51
|
+
if (typeof config.workers !== 'object') {
|
|
52
|
+
const coerced = coercePositiveInteger(config.workers)
|
|
53
|
+
|
|
54
|
+
if (coerced === null) {
|
|
55
|
+
const raw = config.workers
|
|
56
|
+
const hint = typeof raw === 'string' && /\{.*\}/.test(raw) ? 'check your environment variable' : ''
|
|
57
|
+
raiseInvalidWorkersError(prefix, config.workers, hint)
|
|
58
|
+
} else {
|
|
59
|
+
config.workers = { static: coerced, dynamic: false }
|
|
60
|
+
}
|
|
61
|
+
// Object
|
|
62
|
+
} else {
|
|
63
|
+
for (const key of ['minimum', 'maximum', 'static']) {
|
|
64
|
+
if (typeof config.workers[key] === 'undefined') {
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const coerced = coercePositiveInteger(config.workers[key])
|
|
69
|
+
if (coerced === null) {
|
|
70
|
+
const raw = config.workers
|
|
71
|
+
const hint = typeof raw === 'string' && /\{.*\}/.test(raw) ? 'check your environment variable' : ''
|
|
72
|
+
raiseInvalidWorkersError(`${prefix} ${key}`, config.workers, hint)
|
|
73
|
+
} else {
|
|
74
|
+
config.workers[key] = coerced
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// No value, inherit from runtime
|
|
79
|
+
} else {
|
|
80
|
+
config.workers = { static: defaultWorkers }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
48
84
|
export function pprofCapturePreloadPath () {
|
|
49
85
|
const require = createRequire(import.meta.url)
|
|
50
86
|
|
|
@@ -162,6 +198,64 @@ export function parseInspectorOptions (config, inspect, inspectBreak) {
|
|
|
162
198
|
config.watch = false
|
|
163
199
|
}
|
|
164
200
|
|
|
201
|
+
export async function prepareApplication (config, application, defaultWorkers) {
|
|
202
|
+
// We need to have absolute paths here, ot the `loadConfig` will fail
|
|
203
|
+
// Make sure we don't resolve if env var was not replaced
|
|
204
|
+
if (application.path && !isAbsolute(application.path) && !application.path.match(/^\{.*\}$/)) {
|
|
205
|
+
application.path = resolvePath(config[kMetadata].root, application.path)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (application.path && application.config) {
|
|
209
|
+
application.config = resolvePath(application.path, application.config)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
let pkg
|
|
214
|
+
|
|
215
|
+
if (application.config) {
|
|
216
|
+
const config = await loadConfiguration(application.config)
|
|
217
|
+
pkg = await loadConfigurationModule(application.path, config)
|
|
218
|
+
|
|
219
|
+
application.type = extractModuleFromSchemaUrl(config, true).module
|
|
220
|
+
application.skipTelemetryHooks = pkg.skipTelemetryHooks
|
|
221
|
+
} else {
|
|
222
|
+
const { moduleName, capability } = await importCapabilityAndConfig(application.path)
|
|
223
|
+
pkg = capability
|
|
224
|
+
|
|
225
|
+
application.type = moduleName
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
application.skipTelemetryHooks = pkg.skipTelemetryHooks
|
|
229
|
+
|
|
230
|
+
// This is needed to work around Rust bug on dylibs:
|
|
231
|
+
// https://github.com/rust-lang/rust/issues/91979
|
|
232
|
+
// https://github.com/rollup/rollup/issues/5761
|
|
233
|
+
const _require = createRequire(application.path)
|
|
234
|
+
for (const m of pkg.modulesToLoad ?? []) {
|
|
235
|
+
const toLoad = _require.resolve(m)
|
|
236
|
+
loadModule(_require, toLoad).catch(() => {})
|
|
237
|
+
}
|
|
238
|
+
} catch (err) {
|
|
239
|
+
// This should not happen, it happens on running some unit tests if we prepare the runtime
|
|
240
|
+
// when not all the applications configs are available. Given that we are running this only
|
|
241
|
+
// to ddetermine the type of the application, it's safe to ignore this error and default to unknown
|
|
242
|
+
application.type = 'unknown'
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Validate and coerce per-service workers
|
|
246
|
+
parseWorkers(application, `Service "${application.id}"`, defaultWorkers)
|
|
247
|
+
|
|
248
|
+
application.entrypoint = application.id === config.entrypoint
|
|
249
|
+
application.dependencies ??= []
|
|
250
|
+
application.localUrl = `http://${application.id}.plt.local`
|
|
251
|
+
|
|
252
|
+
if (typeof application.watch === 'undefined') {
|
|
253
|
+
application.watch = config.watch
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return application
|
|
257
|
+
}
|
|
258
|
+
|
|
165
259
|
export async function transform (config, _, context) {
|
|
166
260
|
const production = context?.isProduction ?? context?.production
|
|
167
261
|
const applications = [...(config.applications ?? []), ...(config.services ?? []), ...(config.web ?? [])]
|
|
@@ -173,6 +267,27 @@ export async function transform (config, _, context) {
|
|
|
173
267
|
config.watch = !production
|
|
174
268
|
}
|
|
175
269
|
|
|
270
|
+
// Migrate the old verticalScaler property, only applied if the new settings are not set, otherwise workers takes precedence
|
|
271
|
+
// TODO: Remove in the next major version
|
|
272
|
+
if (config.verticalScaler) {
|
|
273
|
+
config.workers ??= {}
|
|
274
|
+
config.workers.dynamic ??= config.verticalScaler.enabled
|
|
275
|
+
config.workers.minimum ??= config.verticalScaler.minWorkers
|
|
276
|
+
config.workers.maximum ??= config.verticalScaler.maxWorkers
|
|
277
|
+
config.workers.total ??= config.verticalScaler.maxTotalWorkers
|
|
278
|
+
config.workers.maxMemory ??= config.verticalScaler.maxTotalMemory
|
|
279
|
+
|
|
280
|
+
if (typeof config.workers.cooldown === 'undefined' && typeof config.verticalScaler.cooldownSec === 'number') {
|
|
281
|
+
config.workers.cooldown = config.verticalScaler.cooldownSec * 1000
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (typeof config.workers.gracePeriod === 'undefined' && typeof config.verticalScaler.gracePeriod === 'number') {
|
|
285
|
+
config.workers.gracePeriod = config.verticalScaler.gracePeriod
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
config.verticalScaler = undefined
|
|
289
|
+
}
|
|
290
|
+
|
|
176
291
|
if (config.autoload) {
|
|
177
292
|
const { exclude = [], mappings = {} } = config.autoload
|
|
178
293
|
let { path } = config.autoload
|
|
@@ -215,80 +330,11 @@ export async function transform (config, _, context) {
|
|
|
215
330
|
let hasValidEntrypoint = false
|
|
216
331
|
|
|
217
332
|
// Root-level workers
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (coerced === null) {
|
|
221
|
-
const raw = config.workers
|
|
222
|
-
const hint = typeof raw === 'string' && /\{.*\}/.test(raw) ? 'check your environment variable' : ''
|
|
223
|
-
raiseInvalidWorkersError('Runtime', config.workers, hint)
|
|
224
|
-
}
|
|
225
|
-
config.workers = coerced
|
|
226
|
-
}
|
|
333
|
+
parseWorkers(config, 'Runtime')
|
|
334
|
+
const defaultWorkers = config.workers.static
|
|
227
335
|
|
|
228
336
|
for (let i = 0; i < applications.length; ++i) {
|
|
229
|
-
const application = applications[i]
|
|
230
|
-
|
|
231
|
-
// We need to have absolute paths here, ot the `loadConfig` will fail
|
|
232
|
-
// Make sure we don't resolve if env var was not replaced
|
|
233
|
-
if (application.path && !isAbsolute(application.path) && !application.path.match(/^\{.*\}$/)) {
|
|
234
|
-
application.path = resolvePath(config[kMetadata].root, application.path)
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (application.path && application.config) {
|
|
238
|
-
application.config = resolvePath(application.path, application.config)
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
try {
|
|
242
|
-
let pkg
|
|
243
|
-
|
|
244
|
-
if (application.config) {
|
|
245
|
-
const config = await loadConfiguration(application.config)
|
|
246
|
-
pkg = await loadConfigurationModule(application.path, config)
|
|
247
|
-
|
|
248
|
-
application.type = extractModuleFromSchemaUrl(config, true).module
|
|
249
|
-
application.skipTelemetryHooks = pkg.skipTelemetryHooks
|
|
250
|
-
} else {
|
|
251
|
-
const { moduleName, capability } = await importCapabilityAndConfig(application.path)
|
|
252
|
-
pkg = capability
|
|
253
|
-
|
|
254
|
-
application.type = moduleName
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
application.skipTelemetryHooks = pkg.skipTelemetryHooks
|
|
258
|
-
|
|
259
|
-
// This is needed to work around Rust bug on dylibs:
|
|
260
|
-
// https://github.com/rust-lang/rust/issues/91979
|
|
261
|
-
// https://github.com/rollup/rollup/issues/5761
|
|
262
|
-
const _require = createRequire(application.path)
|
|
263
|
-
for (const m of pkg.modulesToLoad ?? []) {
|
|
264
|
-
const toLoad = _require.resolve(m)
|
|
265
|
-
loadModule(_require, toLoad).catch(() => {})
|
|
266
|
-
}
|
|
267
|
-
} catch (err) {
|
|
268
|
-
// This should not happen, it happens on running some unit tests if we prepare the runtime
|
|
269
|
-
// when not all the applications configs are available. Given that we are running this only
|
|
270
|
-
// to ddetermine the type of the application, it's safe to ignore this error and default to unknown
|
|
271
|
-
application.type = 'unknown'
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Validate and coerce per-service workers
|
|
275
|
-
if (typeof application.workers !== 'undefined') {
|
|
276
|
-
const coerced = coercePositiveInteger(application.workers)
|
|
277
|
-
if (coerced === null) {
|
|
278
|
-
const raw = config.application?.[i]?.workers
|
|
279
|
-
const hint = typeof raw === 'string' && /\{.*\}/.test(raw) ? 'check your environment variable' : ''
|
|
280
|
-
raiseInvalidWorkersError(`Service "${application.id}"`, application.workers, hint)
|
|
281
|
-
}
|
|
282
|
-
application.workers = coerced
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
application.entrypoint = application.id === config.entrypoint
|
|
286
|
-
application.dependencies ??= []
|
|
287
|
-
application.localUrl = `http://${application.id}.plt.local`
|
|
288
|
-
|
|
289
|
-
if (typeof application.watch === 'undefined') {
|
|
290
|
-
application.watch = config.watch
|
|
291
|
-
}
|
|
337
|
+
const application = await prepareApplication(config, applications[i], defaultWorkers)
|
|
292
338
|
|
|
293
339
|
if (application.entrypoint) {
|
|
294
340
|
hasValidEntrypoint = true
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { features } from '@platformatic/foundation'
|
|
2
|
+
import { availableParallelism } from 'node:os'
|
|
3
|
+
import { getMemoryInfo } from './metrics.js'
|
|
4
|
+
import { ScalingAlgorithm, scaleUpELUThreshold } from './scaling-algorithm.js'
|
|
5
|
+
import { kApplicationId, kId, kLastVerticalScalerELU, kWorkerStartTime, kWorkerStatus } from './worker/symbols.js'
|
|
6
|
+
|
|
7
|
+
const healthCheckInterval = 1000
|
|
8
|
+
export const kOriginalWorkers = Symbol('plt.runtime.application.dynamicWorkersScalerOriginalWorkers')
|
|
9
|
+
|
|
10
|
+
const defaultCooldown = 60_000
|
|
11
|
+
const defaultGracePeriod = 30_000
|
|
12
|
+
const scaleIntervalPeriod = 60_000
|
|
13
|
+
|
|
14
|
+
export class DynamicWorkersScaler {
|
|
15
|
+
#status
|
|
16
|
+
#runtime
|
|
17
|
+
#algorithm
|
|
18
|
+
|
|
19
|
+
#maxTotalMemory
|
|
20
|
+
#maxTotalWorkers
|
|
21
|
+
#maxWorkers
|
|
22
|
+
#minWorkers
|
|
23
|
+
#cooldown
|
|
24
|
+
#gracePeriod
|
|
25
|
+
|
|
26
|
+
#initialUpdates
|
|
27
|
+
#memoryInfo
|
|
28
|
+
#healthCheckTimeout
|
|
29
|
+
#checkScalingInterval
|
|
30
|
+
#isScaling
|
|
31
|
+
#lastScaling
|
|
32
|
+
|
|
33
|
+
constructor (runtime, config) {
|
|
34
|
+
this.#runtime = runtime
|
|
35
|
+
|
|
36
|
+
this.#maxTotalMemory = config.maxMemory // This is defaulted in start()
|
|
37
|
+
this.#maxTotalWorkers = config.total ?? availableParallelism()
|
|
38
|
+
this.#maxWorkers = config.maximum ?? this.#maxTotalWorkers
|
|
39
|
+
this.#minWorkers = config.minimum ?? 1
|
|
40
|
+
this.#cooldown = config.cooldown ?? defaultCooldown
|
|
41
|
+
this.#gracePeriod = config.gracePeriod ?? defaultGracePeriod
|
|
42
|
+
|
|
43
|
+
this.#algorithm = new ScalingAlgorithm({ maxTotalWorkers: this.#maxTotalWorkers })
|
|
44
|
+
|
|
45
|
+
this.#isScaling = false
|
|
46
|
+
this.#lastScaling = 0
|
|
47
|
+
this.#initialUpdates = []
|
|
48
|
+
this.#status = 'init'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getConfig () {
|
|
52
|
+
return {
|
|
53
|
+
maxTotalMemory: this.#maxTotalMemory,
|
|
54
|
+
maxTotalWorkers: this.#maxTotalWorkers,
|
|
55
|
+
maxWorkers: this.#maxWorkers,
|
|
56
|
+
minWorkers: this.#minWorkers,
|
|
57
|
+
cooldown: this.#cooldown,
|
|
58
|
+
gracePeriod: this.#gracePeriod
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async start () {
|
|
63
|
+
this.#memoryInfo = await getMemoryInfo()
|
|
64
|
+
this.#maxTotalMemory ??= this.#memoryInfo.total * 0.9
|
|
65
|
+
|
|
66
|
+
this.#checkScalingInterval = setInterval(this.#checkScaling.bind(this), scaleIntervalPeriod)
|
|
67
|
+
this.#healthCheckTimeout = setTimeout(this.#chechHealth.bind(this), healthCheckInterval)
|
|
68
|
+
|
|
69
|
+
if (this.#initialUpdates.length > 0) {
|
|
70
|
+
await this.#runtime.updateApplicationsResources(this.#initialUpdates)
|
|
71
|
+
this.#initialUpdates = []
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.#status = 'started'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
stop () {
|
|
78
|
+
clearTimeout(this.#healthCheckTimeout)
|
|
79
|
+
clearInterval(this.#checkScalingInterval)
|
|
80
|
+
this.#status = 'stopped'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async add (application) {
|
|
84
|
+
const config = {}
|
|
85
|
+
|
|
86
|
+
if (application.entrypoint && !features.node.reusePort) {
|
|
87
|
+
this.#runtime.logger.warn(
|
|
88
|
+
`The "${application.id}" application cannot be scaled because it is an entrypoint and the "reusePort" feature is not available in your OS.`
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
config.minWorkers = 1
|
|
92
|
+
config.maxWorkers = 1
|
|
93
|
+
} else if (application.workers.dynamic === false) {
|
|
94
|
+
this.#runtime.logger.warn(
|
|
95
|
+
`The "${application.id}" application cannot be scaled because it has a fixed number of workers (${application.workers.static}).`
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
config.minWorkers = application.workers.static
|
|
99
|
+
config.maxWorkers = application.workers.static
|
|
100
|
+
} else {
|
|
101
|
+
config.minWorkers = application.workers.minimum
|
|
102
|
+
config.maxWorkers = application.workers.maximum
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
config.minWorkers ??= this.#minWorkers
|
|
106
|
+
config.maxWorkers ??= this.#maxWorkers
|
|
107
|
+
|
|
108
|
+
if (config.minWorkers > 1) {
|
|
109
|
+
const update = { application: application.id, workers: config.minWorkers }
|
|
110
|
+
|
|
111
|
+
if (!this.#status === 'started') {
|
|
112
|
+
await this.#runtime.updateApplicationsResources([update])
|
|
113
|
+
} else {
|
|
114
|
+
this.#initialUpdates.push(update)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.#algorithm.addApplication(application.id, config)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
remove (application) {
|
|
122
|
+
this.#algorithm.removeApplication(application.id)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async #chechHealth () {
|
|
126
|
+
let shouldCheckForScaling = false
|
|
127
|
+
|
|
128
|
+
const now = Date.now()
|
|
129
|
+
|
|
130
|
+
const workers = await this.#runtime.getWorkers(true)
|
|
131
|
+
|
|
132
|
+
for (const { raw: worker } of Object.values(workers)) {
|
|
133
|
+
if (worker[kWorkerStatus] !== 'started' || worker[kWorkerStartTime] + this.#gracePeriod > now) {
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const health = await this.#runtime.getWorkerHealth(worker, { previousELU: worker[kLastVerticalScalerELU] })
|
|
139
|
+
|
|
140
|
+
if (!health) {
|
|
141
|
+
continue
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
worker[kLastVerticalScalerELU] = health.currentELU
|
|
145
|
+
|
|
146
|
+
this.#algorithm.addWorkerHealthInfo({
|
|
147
|
+
workerId: worker[kId],
|
|
148
|
+
applicationId: worker[kApplicationId],
|
|
149
|
+
elu: health.elu,
|
|
150
|
+
heapUsed: health.heapUsed,
|
|
151
|
+
heapTotal: health.heapTotal
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
if (health.elu > scaleUpELUThreshold) {
|
|
155
|
+
shouldCheckForScaling = true
|
|
156
|
+
}
|
|
157
|
+
} catch (err) {
|
|
158
|
+
this.logger.error({ err }, 'Failed to get health for worker')
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (shouldCheckForScaling) {
|
|
163
|
+
await this.#checkScaling()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
this.#healthCheckTimeout.refresh()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async #checkScaling () {
|
|
170
|
+
const isInCooldown = Date.now() < this.#lastScaling + this.#cooldown
|
|
171
|
+
if (this.#isScaling || isInCooldown) {
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
this.#isScaling = true
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const workersInfo = await this.#runtime.getWorkers()
|
|
179
|
+
const mem = await getMemoryInfo({ scope: this.#memoryInfo.scope })
|
|
180
|
+
|
|
181
|
+
const appsWorkersInfo = {}
|
|
182
|
+
for (const worker of Object.values(workersInfo)) {
|
|
183
|
+
if (worker.status === 'exited') {
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const applicationId = worker.application
|
|
188
|
+
appsWorkersInfo[applicationId] ??= 0
|
|
189
|
+
appsWorkersInfo[applicationId]++
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const availableMemory = this.#maxTotalMemory - mem.used
|
|
193
|
+
const recommendations = this.#algorithm.getRecommendations(appsWorkersInfo, { availableMemory })
|
|
194
|
+
|
|
195
|
+
if (recommendations.length > 0) {
|
|
196
|
+
await this.#applyRecommendations(recommendations)
|
|
197
|
+
this.#lastScaling = Date.now()
|
|
198
|
+
}
|
|
199
|
+
} catch (err) {
|
|
200
|
+
this.#runtime.logger.error({ err }, 'Failed to scale applications')
|
|
201
|
+
} finally {
|
|
202
|
+
this.#isScaling = false
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async #applyRecommendations (recommendations) {
|
|
207
|
+
const resourcesUpdates = []
|
|
208
|
+
|
|
209
|
+
for (const recommendation of recommendations) {
|
|
210
|
+
const { applicationId, workersCount, direction } = recommendation
|
|
211
|
+
this.#runtime.logger.info(`Scaling ${direction} the "${applicationId}" app to ${workersCount} workers`)
|
|
212
|
+
|
|
213
|
+
resourcesUpdates.push({ application: applicationId, workers: workersCount })
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return this.#runtime.updateApplicationsResources(resourcesUpdates)
|
|
217
|
+
}
|
|
218
|
+
}
|
package/lib/errors.js
CHANGED
|
@@ -127,3 +127,24 @@ export const MissingPprofCapture = createError(
|
|
|
127
127
|
`${ERROR_PREFIX}_MISSING_PPROF_CAPTURE`,
|
|
128
128
|
'Please install @platformatic/wattpm-pprof-capture'
|
|
129
129
|
)
|
|
130
|
+
|
|
131
|
+
export const GetHeapStatisticUnavailable = createError(
|
|
132
|
+
`${ERROR_PREFIX}_GET_HEAP_STATISTIC_UNAVAILABLE`,
|
|
133
|
+
'The getHeapStatistics method is not available in your Node version'
|
|
134
|
+
)
|
|
135
|
+
export const FailedToSendHealthSignalsError = createError(
|
|
136
|
+
`${ERROR_PREFIX}_FAILED_TO_SEND_HEALTH_SIGNALS`,
|
|
137
|
+
'Cannot send health signals from application "%s": %s'
|
|
138
|
+
)
|
|
139
|
+
export const HealthSignalMustBeObjectError = createError(
|
|
140
|
+
`${ERROR_PREFIX}_HEALTH_SIGNAL_MUST_BE_OBJECT`,
|
|
141
|
+
'Health signal must be an object'
|
|
142
|
+
)
|
|
143
|
+
export const HealthSignalTypeMustBeStringError = createError(
|
|
144
|
+
`${ERROR_PREFIX}_HEALTH_SIGNAL_TYPE_MUST_BE_STRING`,
|
|
145
|
+
'Health signal type must be a string, received "%s"'
|
|
146
|
+
)
|
|
147
|
+
export const CannotRemoveEntrypointError = createError(
|
|
148
|
+
`${ERROR_PREFIX}_CANNOT_REMOVE_ENTRYPOINT`,
|
|
149
|
+
'Cannot remove the entrypoint application.'
|
|
150
|
+
)
|
package/lib/logger.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildPinoFormatters, buildPinoTimestamp } from '@platformatic/foundation'
|
|
1
|
+
import { buildPinoFormatters, buildPinoTimestamp, usePrettyPrint } from '@platformatic/foundation'
|
|
2
2
|
import { isatty } from 'node:tty'
|
|
3
3
|
import pino from 'pino'
|
|
4
4
|
import pretty from 'pino-pretty'
|
|
@@ -27,8 +27,10 @@ export async function createLogger (config) {
|
|
|
27
27
|
|
|
28
28
|
if (config.logger.transport) {
|
|
29
29
|
cliStream = pino.transport(config.logger.transport)
|
|
30
|
+
} else if ((process.env.FORCE_TTY || isatty(1)) && usePrettyPrint()) {
|
|
31
|
+
cliStream = pretty({ customPrettifiers })
|
|
30
32
|
} else {
|
|
31
|
-
cliStream =
|
|
33
|
+
cliStream = pino.destination(1)
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
if (loggerConfig.formatters) {
|
package/lib/prom-server.js
CHANGED
|
@@ -21,19 +21,17 @@ const DEFAULT_LIVENESS_FAIL_BODY = 'ERR'
|
|
|
21
21
|
|
|
22
22
|
async function checkReadiness (runtime) {
|
|
23
23
|
const workers = await runtime.getWorkers()
|
|
24
|
+
const applications = await runtime.getApplicationsIds()
|
|
24
25
|
|
|
25
26
|
// Make sure there is at least one started worker
|
|
26
|
-
const applications = new Set()
|
|
27
27
|
const started = new Set()
|
|
28
28
|
for (const worker of Object.values(workers)) {
|
|
29
|
-
applications.add(worker.application)
|
|
30
|
-
|
|
31
29
|
if (worker.status === 'started') {
|
|
32
30
|
started.add(worker.application)
|
|
33
31
|
}
|
|
34
32
|
}
|
|
35
33
|
|
|
36
|
-
if (started.size !== applications.
|
|
34
|
+
if (started.size !== applications.length) {
|
|
37
35
|
return { status: false }
|
|
38
36
|
}
|
|
39
37
|
|