@platformatic/runtime 3.71.1-alpha.0 → 4.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config.d.ts +602 -673
- package/index.d.ts +22 -27
- package/index.js +290 -30
- package/lib/config.js +270 -367
- package/lib/errors.js +27 -13
- package/lib/generator.js +249 -127
- package/lib/logger.js +3 -3
- package/lib/management-api.js +22 -65
- package/lib/management-handlers.js +3 -5
- package/lib/memory.js +29 -0
- package/lib/policies.js +27 -0
- package/lib/predictive-scaling.js +910 -0
- package/lib/predictive-worker-scaler.js +378 -0
- package/lib/runtime.js +1081 -572
- package/lib/scheduler.js +6 -41
- package/lib/schema.js +77 -15
- package/lib/upgrade.js +16 -1
- package/lib/versions/v4.0.0.js +31 -0
- package/lib/worker/controller.js +89 -109
- package/lib/worker/interceptors.js +34 -34
- package/lib/worker/itc.js +248 -66
- package/lib/worker/main.js +50 -98
- package/lib/worker/messaging.js +42 -1
- package/lib/worker/round-robin-map.js +10 -3
- package/lib/worker/symbols.js +6 -2
- package/package.json +29 -20
- package/schema.json +252 -433
- package/lib/scaling-algorithm.js +0 -256
- package/lib/worker-scaler.js +0 -249
package/lib/config.js
CHANGED
|
@@ -1,103 +1,109 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
loadConfigurationModule,
|
|
8
|
-
loadModule,
|
|
9
|
-
omitProperties,
|
|
10
|
-
runtimeUnwrappablePropertiesList
|
|
11
|
-
} from '@platformatic/foundation'
|
|
12
|
-
import { realpathSync } from 'node:fs'
|
|
13
|
-
import { readdir, readFile } from 'node:fs/promises'
|
|
14
|
-
import { createRequire } from 'node:module'
|
|
15
|
-
import { isAbsolute, join, resolve as resolvePath } from 'node:path'
|
|
1
|
+
import { createDirectory, kMetadata, loadModule } from '@platformatic/foundation'
|
|
2
|
+
import { loadAdditionalApplications } from '@platformatic/foundation/loader'
|
|
3
|
+
import { createRequire, findPackageJSON } from 'node:module'
|
|
4
|
+
import { availableParallelism } from 'node:os'
|
|
5
|
+
import { dirname, resolve as resolvePath } from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
16
7
|
|
|
17
8
|
import {
|
|
18
|
-
|
|
9
|
+
ApplicationsPortsOverlapError,
|
|
19
10
|
InspectAndInspectBrkError,
|
|
20
11
|
InspectorHostError,
|
|
21
12
|
InspectorPortError,
|
|
22
13
|
InvalidArgumentError,
|
|
23
|
-
|
|
14
|
+
MissingDependencyError
|
|
24
15
|
} from './errors.js'
|
|
25
|
-
|
|
26
|
-
|
|
16
|
+
|
|
17
|
+
// The runtime package's own entry point, which is where the bundled capability copies live.
|
|
18
|
+
const runtimeScopePath = fileURLToPath(new URL('../index.js', import.meta.url))
|
|
27
19
|
|
|
28
20
|
// Validate and coerce workers values early to avoid runtime hangs when invalid
|
|
29
|
-
function coercePositiveInteger (value) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
21
|
+
function coercePositiveInteger (value, location) {
|
|
22
|
+
const count = typeof value === 'string' ? Number(value) : value
|
|
23
|
+
if (!Number.isInteger(count) || count < 1) {
|
|
24
|
+
const hint = typeof value === 'string' && /\{.*\}/.test(value) ? ' (check your environment variable)' : ''
|
|
25
|
+
throw new InvalidArgumentError(`${location} workers must be a positive integer; received "${value}"${hint}`)
|
|
33
26
|
}
|
|
34
|
-
|
|
35
|
-
// Trim to handle accidental spaces
|
|
36
|
-
const trimmed = value.trim()
|
|
37
|
-
if (trimmed.length === 0) return null
|
|
38
|
-
const num = Number(trimmed)
|
|
39
|
-
if (!Number.isFinite(num) || !Number.isInteger(num) || num < 1) return null
|
|
40
|
-
return num
|
|
41
|
-
}
|
|
42
|
-
return null
|
|
27
|
+
return count
|
|
43
28
|
}
|
|
44
29
|
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
throw new InvalidArgumentError(`${location} workers must be a positive integer; received "${received}"${extra}`)
|
|
48
|
-
}
|
|
30
|
+
function coerceWorkers (workers, prefix) {
|
|
31
|
+
if (workers === undefined) return {}
|
|
49
32
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const coerced = coercePositiveInteger(config.workers)
|
|
55
|
-
|
|
56
|
-
if (coerced === null) {
|
|
57
|
-
const raw = config.workers
|
|
58
|
-
const hint = typeof raw === 'string' && /\{.*\}/.test(raw) ? 'check your environment variable' : ''
|
|
59
|
-
raiseInvalidWorkersError(prefix, config.workers, hint)
|
|
60
|
-
} else {
|
|
61
|
-
config.workers = { static: coerced, dynamic: false }
|
|
62
|
-
}
|
|
63
|
-
// Object
|
|
64
|
-
} else {
|
|
65
|
-
for (const key of ['minimum', 'maximum', 'static']) {
|
|
66
|
-
if (typeof config.workers[key] === 'undefined') {
|
|
67
|
-
continue
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const coerced = coercePositiveInteger(config.workers[key])
|
|
71
|
-
if (coerced === null) {
|
|
72
|
-
const raw = config.workers
|
|
73
|
-
const hint = typeof raw === 'string' && /\{.*\}/.test(raw) ? 'check your environment variable' : ''
|
|
74
|
-
raiseInvalidWorkersError(`${prefix} ${key}`, config.workers, hint)
|
|
75
|
-
} else {
|
|
76
|
-
config.workers[key] = coerced
|
|
77
|
-
}
|
|
78
|
-
}
|
|
33
|
+
if (typeof workers !== 'object') {
|
|
34
|
+
return {
|
|
35
|
+
static: coercePositiveInteger(workers, prefix),
|
|
36
|
+
dynamic: false
|
|
79
37
|
}
|
|
80
|
-
} else {
|
|
81
|
-
config.workers = {}
|
|
82
38
|
}
|
|
83
39
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
config.workers[key] = defaultWorkers[key]
|
|
40
|
+
for (const key of ['minimum', 'maximum', 'static']) {
|
|
41
|
+
if (workers[key] !== undefined) {
|
|
42
|
+
workers[key] = coercePositiveInteger(workers[key], `${prefix} ${key}`)
|
|
88
43
|
}
|
|
89
44
|
}
|
|
45
|
+
return workers
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseRuntimeWorkers (config) {
|
|
49
|
+
const workers = coerceWorkers(config.workers, 'Runtime')
|
|
50
|
+
workers.total ??= availableParallelism()
|
|
51
|
+
workers.static ??= 1
|
|
52
|
+
workers.dynamic ??= false
|
|
53
|
+
|
|
54
|
+
// Ajv does not apply property defaults inside the workers schema's anyOf branches.
|
|
55
|
+
workers.eluThreshold ??= 0.8
|
|
56
|
+
workers.processIntervalMs ??= 10000
|
|
57
|
+
workers.maxScaleUpStep ??= 1
|
|
58
|
+
workers.redistributionMs ??= 10000
|
|
59
|
+
workers.alphaUp ??= 0.2
|
|
60
|
+
workers.alphaDown ??= 0.1
|
|
61
|
+
workers.betaUp ??= 0.1
|
|
62
|
+
workers.betaDown ??= 0.1
|
|
63
|
+
workers.cooldowns ??= {}
|
|
64
|
+
workers.cooldowns.scaleUpAfterScaleUpMs ??= 5000
|
|
65
|
+
workers.cooldowns.scaleUpAfterScaleDownMs ??= 5000
|
|
66
|
+
workers.cooldowns.scaleDownAfterScaleUpMs ??= 30000
|
|
67
|
+
workers.cooldowns.scaleDownAfterScaleDownMs ??= 20000
|
|
68
|
+
|
|
69
|
+
if (workers.maximum < workers.minimum) {
|
|
70
|
+
throw new InvalidArgumentError(
|
|
71
|
+
`Workers minimum (${workers.minimum}) must not exceed maximum (${workers.maximum})`
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
if (workers.dynamic) {
|
|
75
|
+
workers.static = workers.minimum ?? 1
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
config.workers = workers
|
|
79
|
+
}
|
|
90
80
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
81
|
+
function parseApplicationWorkers (applicationConfig, config) {
|
|
82
|
+
const workers = coerceWorkers(applicationConfig.workers, `Service "${applicationConfig.id}"`)
|
|
83
|
+
for (const key of ['minimum', 'maximum', 'static', 'dynamic']) {
|
|
84
|
+
if (workers[key] === undefined && config.workers[key] !== undefined) {
|
|
85
|
+
workers[key] = config.workers[key]
|
|
86
|
+
}
|
|
96
87
|
}
|
|
97
88
|
|
|
98
|
-
if (
|
|
99
|
-
|
|
89
|
+
if (workers.maximum < workers.minimum) {
|
|
90
|
+
throw new InvalidArgumentError(
|
|
91
|
+
`Workers minimum (${workers.minimum}) must not exceed maximum (${workers.maximum})`
|
|
92
|
+
)
|
|
100
93
|
}
|
|
94
|
+
if (workers.dynamic) {
|
|
95
|
+
for (const key of ['eluThreshold', 'heapThresholdMb']) {
|
|
96
|
+
if (workers[key] === undefined && config.workers[key] !== undefined) {
|
|
97
|
+
workers[key] = config.workers[key]
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
workers.minimum ??= 1
|
|
101
|
+
// An implicit maximum must accommodate the application's required minimum.
|
|
102
|
+
workers.maximum ??= Math.max(workers.minimum, config.workers.total)
|
|
103
|
+
workers.static = workers.minimum
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
applicationConfig.workers = workers
|
|
101
107
|
}
|
|
102
108
|
|
|
103
109
|
export function pprofCapturePreloadPath () {
|
|
@@ -130,62 +136,6 @@ export function autoDetectPprofCapture (config) {
|
|
|
130
136
|
return config
|
|
131
137
|
}
|
|
132
138
|
|
|
133
|
-
export async function wrapInRuntimeConfig (config, context) {
|
|
134
|
-
let applicationId = 'main'
|
|
135
|
-
try {
|
|
136
|
-
const packageJson = JSON.parse(await readFile(join(config[kMetadata].root, 'package.json'), 'utf-8'))
|
|
137
|
-
applicationId = packageJson?.name ?? 'main'
|
|
138
|
-
|
|
139
|
-
if (applicationId.startsWith('@')) {
|
|
140
|
-
applicationId = applicationId.split('/')[1]
|
|
141
|
-
}
|
|
142
|
-
} catch (err) {
|
|
143
|
-
// on purpose, the package.json might be missing
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Carry over only the server properties that the user actually set, so
|
|
147
|
-
// we do not end up with a `{ hostname: undefined, ... }` object that then
|
|
148
|
-
// gets populated by schema defaults or trips truthy checks downstream.
|
|
149
|
-
const server = {}
|
|
150
|
-
if (config.server) {
|
|
151
|
-
for (const key of ['hostname', 'port', 'http2', 'https']) {
|
|
152
|
-
if (config.server[key] !== undefined) {
|
|
153
|
-
server[key] = config.server[key]
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
const production = context?.isProduction ?? context?.production
|
|
158
|
-
|
|
159
|
-
const runtimeConfig = config.runtime ?? {}
|
|
160
|
-
|
|
161
|
-
// Important: do not change the order of the properties in this object
|
|
162
|
-
/* c8 ignore next */
|
|
163
|
-
const wrapped = {
|
|
164
|
-
$schema: schema.$id,
|
|
165
|
-
...(Object.keys(server).length > 0 ? { server } : {}),
|
|
166
|
-
watch: !production,
|
|
167
|
-
...omitProperties(runtimeConfig, runtimeUnwrappablePropertiesList),
|
|
168
|
-
entrypoint: applicationId,
|
|
169
|
-
applications: [
|
|
170
|
-
{
|
|
171
|
-
id: applicationId,
|
|
172
|
-
path: config[kMetadata].root,
|
|
173
|
-
config: config[kMetadata].path,
|
|
174
|
-
...(runtimeConfig.application ?? {})
|
|
175
|
-
}
|
|
176
|
-
]
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return loadConfiguration(wrapped, context?.schema ?? schema, {
|
|
180
|
-
validationOptions,
|
|
181
|
-
transform,
|
|
182
|
-
upgrade,
|
|
183
|
-
replaceEnv: true,
|
|
184
|
-
root: config[kMetadata].root,
|
|
185
|
-
...context
|
|
186
|
-
})
|
|
187
|
-
}
|
|
188
|
-
|
|
189
139
|
export function parseInspectorOptions (config, inspect, inspectBreak) {
|
|
190
140
|
const hasInspect = inspect != null
|
|
191
141
|
const hasInspectBrk = inspectBreak != null
|
|
@@ -228,297 +178,226 @@ export function parseInspectorOptions (config, inspect, inspectBreak) {
|
|
|
228
178
|
config.watch = false
|
|
229
179
|
}
|
|
230
180
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
181
|
+
/*
|
|
182
|
+
The one entry point for applications added while the runtime is running, shared by the HTTP route
|
|
183
|
+
and the management ITC handler so the two cannot drift.
|
|
184
|
+
|
|
185
|
+
An added application is evaluated exactly as boot evaluates one -- its configuration
|
|
186
|
+
file is found in its own directory, its environment is resolved main-side, and it arrives with
|
|
187
|
+
resolvedConfig. Skipping that leaves the worker to look for a config file name that is never written,
|
|
188
|
+
which fails as an unhelpful "unable to initialize the worker".
|
|
189
|
+
*/
|
|
190
|
+
export async function prepareAddedApplications (config, entries, existingIds = []) {
|
|
191
|
+
const metadata = config[kMetadata]
|
|
192
|
+
|
|
193
|
+
const { applications } = await loadAdditionalApplications({
|
|
194
|
+
configPath: metadata.path,
|
|
195
|
+
entries,
|
|
196
|
+
existingIds,
|
|
197
|
+
rootEnvBlock: config.env,
|
|
198
|
+
command: metadata.loader.command,
|
|
199
|
+
mode: metadata.loader.mode,
|
|
200
|
+
production: metadata.loader.production,
|
|
201
|
+
realEnv: metadata.loader.realEnv,
|
|
202
|
+
customEnvFile: metadata.loader.customEnvFile,
|
|
203
|
+
// The runtime is the fallback scope for capability resolution, the same as at boot.
|
|
204
|
+
runtimeScope: runtimeScopePath
|
|
205
|
+
})
|
|
237
206
|
|
|
238
|
-
|
|
239
|
-
|
|
207
|
+
const prepared = []
|
|
208
|
+
|
|
209
|
+
for (const application of applications) {
|
|
210
|
+
prepared.push(await prepareRuntimeApplication(config, application))
|
|
240
211
|
}
|
|
241
212
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
// Attempting to detect capability here would cause slow glob operations on the cwd
|
|
245
|
-
if (application.url && !application.path) {
|
|
246
|
-
application.type = 'unknown'
|
|
247
|
-
} else {
|
|
248
|
-
try {
|
|
249
|
-
let pkg
|
|
213
|
+
return prepared
|
|
214
|
+
}
|
|
250
215
|
|
|
251
|
-
|
|
252
|
-
const config = await loadConfiguration(application.config)
|
|
253
|
-
pkg = await loadConfigurationModule(application.path, config)
|
|
216
|
+
const hostnameWildcards = new Set(['0.0.0.0', '::', '[::]'])
|
|
254
217
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
218
|
+
// The listener an application declares in its own configuration, when it can be determined without
|
|
219
|
+
// starting it. A port coming from an environment variable which was not replaced, an ephemeral port
|
|
220
|
+
// and a missing server block all yield null, in which case only the start time check applies.
|
|
221
|
+
function declaredListener (applicationConfig) {
|
|
222
|
+
const server = applicationConfig?.server
|
|
260
223
|
|
|
261
|
-
|
|
262
|
-
|
|
224
|
+
if (!server) {
|
|
225
|
+
return null
|
|
226
|
+
}
|
|
263
227
|
|
|
264
|
-
|
|
228
|
+
const port = Number(server.port)
|
|
265
229
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
// https://github.com/rollup/rollup/issues/5761
|
|
269
|
-
const _require = createRequire(application.path)
|
|
270
|
-
for (const m of pkg.modulesToLoad ?? []) {
|
|
271
|
-
const toLoad = _require.resolve(m)
|
|
272
|
-
loadModule(_require, toLoad).catch(() => {})
|
|
273
|
-
}
|
|
274
|
-
} catch (err) {
|
|
275
|
-
// This should not happen, it happens on running some unit tests if we prepare the runtime
|
|
276
|
-
// when not all the applications configs are available. Given that we are running this only
|
|
277
|
-
// to ddetermine the type of the application, it's safe to ignore this error and default to unknown
|
|
278
|
-
application.type = 'unknown'
|
|
279
|
-
}
|
|
230
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
231
|
+
return null
|
|
280
232
|
}
|
|
281
233
|
|
|
282
|
-
|
|
283
|
-
|
|
234
|
+
return { port, hostname: server.hostname, perWorker: server.portAssignment === 'perWorkerIncrement' }
|
|
235
|
+
}
|
|
284
236
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
237
|
+
// Two declared listeners can only be compared when both hostnames are known. When a hostname is
|
|
238
|
+
// omitted the capability picks its own default, so unless the other side is a wildcard - which
|
|
239
|
+
// overlaps with anything - the comparison is left to the start time check.
|
|
240
|
+
function declaredListenersOverlap (first, second) {
|
|
241
|
+
const hostname = first.hostname?.toLowerCase()
|
|
242
|
+
const otherHostname = second.hostname?.toLowerCase()
|
|
288
243
|
|
|
289
|
-
|
|
290
|
-
|
|
244
|
+
// A wildcard overlaps with any other hostname, known or not
|
|
245
|
+
if (hostnameWildcards.has(hostname) || hostnameWildcards.has(otherHostname)) {
|
|
246
|
+
return true
|
|
291
247
|
}
|
|
292
248
|
|
|
293
|
-
|
|
294
|
-
|
|
249
|
+
// Both applications rely on the same capability default, so they do collide
|
|
250
|
+
if (typeof hostname === 'undefined' && typeof otherHostname === 'undefined') {
|
|
251
|
+
return true
|
|
295
252
|
}
|
|
296
253
|
|
|
297
|
-
|
|
254
|
+
// Only one of the two defaults is unknown, so nothing can be concluded here
|
|
255
|
+
if (typeof hostname === 'undefined' || typeof otherHostname === 'undefined') {
|
|
256
|
+
return false
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return hostname === otherHostname
|
|
298
260
|
}
|
|
299
261
|
|
|
300
|
-
function
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
} catch {
|
|
304
|
-
return resolvePath(path)
|
|
305
|
-
}
|
|
262
|
+
function describeDeclaredPorts (listener) {
|
|
263
|
+
const { port, last } = listener
|
|
264
|
+
return port === last ? `port ${port}` : `ports ${port}-${last}, one per worker`
|
|
306
265
|
}
|
|
307
266
|
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
267
|
+
// Rejects applications whose declared ports overlap before any of them is started, so that the
|
|
268
|
+
// failure does not depend on which worker happens to report its URL first. This is best effort:
|
|
269
|
+
// applications whose port cannot be determined here are checked when their workers start.
|
|
270
|
+
function verifyApplicationsPorts (applications) {
|
|
271
|
+
const listeners = []
|
|
272
|
+
|
|
273
|
+
for (const application of applications) {
|
|
274
|
+
// The validated capability configuration is kept on the application (resolvedConfig), so the
|
|
275
|
+
// declared listener is read from it directly rather than being stashed on a symbol during a
|
|
276
|
+
// separate config-loading pass, as it once was.
|
|
277
|
+
const listener = declaredListener(application.resolvedConfig)
|
|
278
|
+
|
|
279
|
+
// Dynamic scaling changes how many ports the application ends up using, so leave it to the start time check
|
|
280
|
+
if (!listener || application.workers?.dynamic) {
|
|
281
|
+
continue
|
|
318
282
|
}
|
|
319
283
|
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
return canonicalPath(existingPath) === canonicalPath(entryPath) ? null : `the path "${existing.path}"`
|
|
284
|
+
const workers = listener.perWorker ? (application.workers?.static ?? 1) : 1
|
|
285
|
+
listeners.push({ ...listener, id: application.id, last: listener.port + workers - 1 })
|
|
323
286
|
}
|
|
324
287
|
|
|
325
|
-
|
|
326
|
-
|
|
288
|
+
for (let i = 0; i < listeners.length; i++) {
|
|
289
|
+
for (let j = i + 1; j < listeners.length; j++) {
|
|
290
|
+
const first = listeners[i]
|
|
291
|
+
const second = listeners[j]
|
|
327
292
|
|
|
328
|
-
|
|
329
|
-
}
|
|
293
|
+
const port = Math.max(first.port, second.port)
|
|
330
294
|
|
|
331
|
-
|
|
295
|
+
if (port > Math.min(first.last, second.last) || !declaredListenersOverlap(first, second)) {
|
|
296
|
+
continue
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
throw new ApplicationsPortsOverlapError(
|
|
300
|
+
first.id,
|
|
301
|
+
describeDeclaredPorts(first),
|
|
302
|
+
second.id,
|
|
303
|
+
describeDeclaredPorts(second),
|
|
304
|
+
port
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
332
308
|
}
|
|
333
309
|
|
|
334
|
-
|
|
335
|
-
|
|
310
|
+
// Everything an application needs regardless of how its capability was determined, taken from the
|
|
311
|
+
// loader's envelope before any worker exists rather than by loading the application's config file.
|
|
312
|
+
export function finalizeApplication (config, application) {
|
|
313
|
+
// Validate and coerce per-service workers
|
|
314
|
+
parseApplicationWorkers(application, config)
|
|
336
315
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
316
|
+
application.dependencies ??= []
|
|
317
|
+
application.localUrl = `http://${application.id}.plt.local`
|
|
340
318
|
|
|
341
|
-
if (typeof
|
|
342
|
-
|
|
319
|
+
if (typeof application.watch === 'undefined') {
|
|
320
|
+
application.watch = config.watch
|
|
343
321
|
}
|
|
344
322
|
|
|
345
|
-
if (typeof
|
|
346
|
-
|
|
323
|
+
if (typeof application.management === 'undefined' && config.management) {
|
|
324
|
+
application.management = config.management
|
|
347
325
|
}
|
|
348
326
|
|
|
349
|
-
return
|
|
327
|
+
return application
|
|
350
328
|
}
|
|
351
329
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
if (config.verticalScaler.cooldownSec) {
|
|
375
|
-
config.workers.cooldown ??= config.verticalScaler.cooldownSec * 1000
|
|
376
|
-
}
|
|
377
|
-
if (config.verticalScaler.gracePeriod) {
|
|
378
|
-
config.workers.gracePeriod ??= config.verticalScaler.gracePeriod
|
|
379
|
-
}
|
|
380
|
-
if (config.verticalScaler.scaleUpELU) {
|
|
381
|
-
config.workers.scaleUpELU ??= config.verticalScaler.scaleUpELU
|
|
382
|
-
}
|
|
383
|
-
if (config.verticalScaler.scaleDownELU) {
|
|
384
|
-
config.workers.scaleDownELU ??= config.verticalScaler.scaleDownELU
|
|
330
|
+
/*
|
|
331
|
+
The preparation step. The loader resolved the path, selected the capability and imported its schema
|
|
332
|
+
subpath — which carries skipTracingHooks and modulesToLoad — all main-side, before any worker
|
|
333
|
+
existed. So there is nothing to discover here, and in particular no application config file to
|
|
334
|
+
re-read: that is the whole point of evaluating configuration exactly once per load.
|
|
335
|
+
*/
|
|
336
|
+
export async function prepareRuntimeApplication (config, application) {
|
|
337
|
+
// resolvedConfig replaces the config file path in workerData: the worker receives the validated
|
|
338
|
+
// capability payload as data and never re-reads a file.
|
|
339
|
+
application.resolvedConfig = application.config ?? {}
|
|
340
|
+
application.config = undefined
|
|
341
|
+
|
|
342
|
+
/*
|
|
343
|
+
A module application names an npm package as its capability instead of describing one in a file.
|
|
344
|
+
The package is resolved from the runtime root, and its own directory (sourcePath) is kept
|
|
345
|
+
separate from the application's writable root (path) — which the module treats as a working
|
|
346
|
+
directory and which need not exist yet. Keyed on moduleApplication, not module: a regular
|
|
347
|
+
application also carries a module (its detected capability), and must not be resolved this way.
|
|
348
|
+
*/
|
|
349
|
+
if (application.moduleApplication) {
|
|
350
|
+
if (!application.path) {
|
|
351
|
+
throw new InvalidArgumentError(`Application "${application.id}" must define path when module is set`)
|
|
385
352
|
}
|
|
386
353
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
appConfig = { id: appId, workers: {} }
|
|
392
|
-
applications.push(appConfig)
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const scaleConfig = config.verticalScaler.applications[appId]
|
|
396
|
-
const workersConfig = appConfig.workers
|
|
397
|
-
|
|
398
|
-
workersConfig.minimum ??= scaleConfig.minWorkers ?? config.workers.minimum
|
|
399
|
-
workersConfig.maximum ??= scaleConfig.maxWorkers ?? config.workers.maximum
|
|
400
|
-
|
|
401
|
-
if (scaleConfig.scaleUpELU) {
|
|
402
|
-
workersConfig.scaleUpELU ??= scaleConfig.scaleUpELU
|
|
403
|
-
}
|
|
404
|
-
if (scaleConfig.scaleDownELU) {
|
|
405
|
-
workersConfig.scaleDownELU ??= scaleConfig.scaleDownELU
|
|
406
|
-
}
|
|
407
|
-
}
|
|
354
|
+
try {
|
|
355
|
+
application.sourcePath = dirname(findPackageJSON(application.module, resolvePath(config[kMetadata].root, 'noop.js')))
|
|
356
|
+
} catch (error) {
|
|
357
|
+
throw new MissingDependencyError(application.module, { cause: error })
|
|
408
358
|
}
|
|
409
359
|
|
|
410
|
-
|
|
360
|
+
application.moduleRoot = config[kMetadata].root
|
|
361
|
+
await createDirectory(application.path)
|
|
411
362
|
}
|
|
412
363
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
let { path } = config.autoload
|
|
416
|
-
|
|
417
|
-
// Capture these before the loop, as config is shadowed in its body
|
|
418
|
-
const root = config[kMetadata].root
|
|
419
|
-
const resolvedApplicationsPath = resolvePath(root, config.resolvedApplicationsBasePath ?? 'external')
|
|
420
|
-
|
|
421
|
-
path = resolvePath(root, path)
|
|
422
|
-
const entries = await readdir(path, { withFileTypes: true })
|
|
423
|
-
|
|
424
|
-
for (let i = 0; i < entries.length; ++i) {
|
|
425
|
-
const entry = entries[i]
|
|
426
|
-
|
|
427
|
-
if (exclude.includes(entry.name) || !entry.isDirectory()) {
|
|
428
|
-
continue
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const mapping = mappings[entry.name] ?? {}
|
|
432
|
-
const id = mapping.id ?? entry.name
|
|
433
|
-
const entryPath = join(path, entry.name)
|
|
434
|
-
|
|
435
|
-
let config
|
|
436
|
-
const configFilename = mapping.config ?? (await findConfigurationFile(entryPath))
|
|
437
|
-
|
|
438
|
-
if (typeof configFilename === 'string') {
|
|
439
|
-
config = join(entryPath, configFilename)
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const application = { id, config, path: entryPath, ...mapping }
|
|
443
|
-
const existingApplicationId = applications.findIndex(application => application.id === id)
|
|
444
|
-
|
|
445
|
-
if (existingApplicationId !== -1) {
|
|
446
|
-
const existing = applications[existingApplicationId]
|
|
364
|
+
application.type = application.module ?? 'unknown'
|
|
365
|
+
application.skipTracingHooks = application.capabilityMetadata?.skipTracingHooks ?? false
|
|
447
366
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
// is the mesh hostname, the injected PLT_<ID>_URL name, the metrics label and the argument of
|
|
451
|
-
// "wattpm inject", so two different applications cannot share one.
|
|
452
|
-
if (isApplicationEnabled(existing, environment)) {
|
|
453
|
-
const source = conflictingApplicationSource(root, resolvedApplicationsPath, existing, entryPath)
|
|
367
|
+
// This is needed to work around a Rust bug on dylibs.
|
|
368
|
+
const modulesToLoad = application.capabilityMetadata?.modulesToLoad ?? []
|
|
454
369
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
}
|
|
458
|
-
}
|
|
370
|
+
if (modulesToLoad.length > 0 && application.path) {
|
|
371
|
+
const _require = createRequire(application.sourcePath ?? application.path)
|
|
459
372
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
373
|
+
for (const m of modulesToLoad) {
|
|
374
|
+
try {
|
|
375
|
+
loadModule(_require, _require.resolve(m)).catch(() => {})
|
|
376
|
+
} catch {
|
|
377
|
+
// A module the capability names but the application cannot resolve is not fatal here; the
|
|
378
|
+
// worker reports it with its own context when it actually needs it.
|
|
463
379
|
}
|
|
464
380
|
}
|
|
465
381
|
}
|
|
466
382
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
applications.splice(i, 1)
|
|
470
|
-
}
|
|
471
|
-
}
|
|
383
|
+
return finalizeApplication(config, application)
|
|
384
|
+
}
|
|
472
385
|
|
|
386
|
+
// The half of the transform the loader does not own: inspector options, worker counts, the
|
|
387
|
+
// per-application preparation, and the runtime-level normalizations. This is reached directly,
|
|
388
|
+
// having already expanded autoload and resolved enabled in the eval worker.
|
|
389
|
+
export async function finalizeConfiguration (config, applications, context, production, prepare) {
|
|
473
390
|
config.inspectorOptions = undefined
|
|
474
391
|
parseInspectorOptions(config, context?.inspect, context?.inspectBreak)
|
|
475
392
|
|
|
476
|
-
let hasValidEntrypoint = false
|
|
477
|
-
|
|
478
393
|
// Root-level workers
|
|
479
|
-
|
|
480
|
-
const defaultWorkers = config.workers
|
|
394
|
+
parseRuntimeWorkers(config)
|
|
481
395
|
|
|
482
396
|
for (let i = 0; i < applications.length; ++i) {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
if (application.entrypoint) {
|
|
486
|
-
hasValidEntrypoint = true
|
|
487
|
-
}
|
|
397
|
+
await prepare(config, applications[i])
|
|
488
398
|
}
|
|
489
399
|
|
|
490
|
-
|
|
491
|
-
if (!config.entrypoint) {
|
|
492
|
-
// If there is only one application, it becomes the entrypoint
|
|
493
|
-
if (applications.length === 1) {
|
|
494
|
-
applications[0].entrypoint = true
|
|
495
|
-
config.entrypoint = applications[0].id
|
|
496
|
-
hasValidEntrypoint = true
|
|
497
|
-
} else {
|
|
498
|
-
// Search if exactly application uses @platformatic/gateway
|
|
499
|
-
const gateways = []
|
|
500
|
-
|
|
501
|
-
for (const application of applications) {
|
|
502
|
-
if (!application.config) {
|
|
503
|
-
continue
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
if (application.type === '@platformatic/gateway') {
|
|
507
|
-
gateways.push(application.id)
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
if (gateways.length === 1) {
|
|
512
|
-
applications.find(s => s.id === gateways[0]).entrypoint = true
|
|
513
|
-
config.entrypoint = gateways[0]
|
|
514
|
-
hasValidEntrypoint = true
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
if (!hasValidEntrypoint && config.entrypoint && !context.allowMissingEntrypoint) {
|
|
520
|
-
throw new InvalidEntrypointError(config.entrypoint)
|
|
521
|
-
}
|
|
400
|
+
verifyApplicationsPorts(applications)
|
|
522
401
|
|
|
523
402
|
if (typeof config.metrics === 'boolean') {
|
|
524
403
|
config.metrics = {
|
|
@@ -557,3 +436,27 @@ export async function transform (config, _, context) {
|
|
|
557
436
|
|
|
558
437
|
return config
|
|
559
438
|
}
|
|
439
|
+
|
|
440
|
+
/*
|
|
441
|
+
The transform. What is absent is the point:
|
|
442
|
+
|
|
443
|
+
- no alias merging, because `services` and `web` do not exist;
|
|
444
|
+
- no autoload expansion and no `enabled` filtering, because both ran in the root eval worker,
|
|
445
|
+
which is the only place either runs. Leaving them reachable here would let a second expansion
|
|
446
|
+
re-merge entries and read the filesystem after the authoritative snapshot already exists;
|
|
447
|
+
- no verticalScaler migration, which the schema audit removes.
|
|
448
|
+
|
|
449
|
+
The `autoload` declaration survives as data beside the expanded list — `GET /metadata` reports it
|
|
450
|
+
and `applications:add --save` needs it to decide whether a new application belongs in a mapping
|
|
451
|
+
or as an explicit entry. It is carried, never re-executed.
|
|
452
|
+
*/
|
|
453
|
+
export async function transformConfiguration (config, _, context) {
|
|
454
|
+
const production = context?.isProduction ?? context?.production
|
|
455
|
+
const applications = config.applications ?? []
|
|
456
|
+
|
|
457
|
+
if (typeof config.watch === 'undefined') {
|
|
458
|
+
config.watch = !production
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return finalizeConfiguration(config, applications, context, production, prepareRuntimeApplication)
|
|
462
|
+
}
|