@platformatic/runtime 4.0.0-new-config.2 → 4.0.0-new-config.5
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 +594 -596
- package/index.d.ts +11 -5
- package/index.js +161 -85
- package/lib/config.js +47 -251
- package/lib/errors.js +8 -0
- package/lib/generator.js +199 -35
- package/lib/logger.js +3 -3
- package/lib/management-api.js +4 -60
- package/lib/policies.js +27 -0
- package/lib/runtime.js +315 -239
- package/lib/schema.js +76 -0
- package/lib/worker/controller.js +54 -49
- package/lib/worker/interceptors.js +34 -34
- package/lib/worker/itc.js +49 -2
- package/lib/worker/main.js +28 -53
- package/lib/worker/symbols.js +1 -1
- package/package.json +18 -18
- package/schema.json +87 -44
package/lib/config.js
CHANGED
|
@@ -1,28 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
extractModuleFromSchemaUrl,
|
|
4
|
-
findConfigurationFile,
|
|
5
|
-
kMetadata,
|
|
6
|
-
loadConfiguration,
|
|
7
|
-
loadConfigurationModule,
|
|
8
|
-
loadModule,
|
|
9
|
-
omitProperties,
|
|
10
|
-
runtimeUnwrappablePropertiesList
|
|
11
|
-
} from '@platformatic/foundation'
|
|
12
|
-
import { readdir, readFile } from 'node:fs/promises'
|
|
13
|
-
import { createRequire } from 'node:module'
|
|
1
|
+
import { createDirectory, kMetadata, loadModule } from '@platformatic/foundation'
|
|
14
2
|
import { loadAdditionalApplications } from '@platformatic/foundation/lib/v4/index.js'
|
|
15
|
-
import {
|
|
3
|
+
import { createRequire, findPackageJSON } from 'node:module'
|
|
4
|
+
import { dirname, resolve as resolvePath } from 'node:path'
|
|
16
5
|
import { fileURLToPath } from 'node:url'
|
|
17
6
|
|
|
18
7
|
import {
|
|
19
8
|
InspectAndInspectBrkError,
|
|
20
9
|
InspectorHostError,
|
|
21
10
|
InspectorPortError,
|
|
22
|
-
InvalidArgumentError
|
|
11
|
+
InvalidArgumentError,
|
|
12
|
+
MissingDependencyError
|
|
23
13
|
} from './errors.js'
|
|
24
|
-
import { schema } from './schema.js'
|
|
25
|
-
import { upgrade } from './upgrade.js'
|
|
26
14
|
|
|
27
15
|
// The runtime package's own entry point, which is where the bundled capability copies live.
|
|
28
16
|
const runtimeScopePath = fileURLToPath(new URL('../index.js', import.meta.url))
|
|
@@ -83,6 +71,10 @@ function parseWorkers (config, prefix, defaultWorkers = { static: 1, dynamic: fa
|
|
|
83
71
|
config.workers = {}
|
|
84
72
|
}
|
|
85
73
|
|
|
74
|
+
// What this entry asked for, before the defaults are folded in.
|
|
75
|
+
const declaredMinimum = config.workers.minimum
|
|
76
|
+
const declaredStatic = config.workers.static
|
|
77
|
+
|
|
86
78
|
// Fill missing values from defaults
|
|
87
79
|
for (const key of ['minimum', 'maximum', 'static', 'dynamic']) {
|
|
88
80
|
if (typeof config.workers[key] === 'undefined' && typeof defaultWorkers[key] !== 'undefined') {
|
|
@@ -97,6 +89,16 @@ function parseWorkers (config, prefix, defaultWorkers = { static: 1, dynamic: fa
|
|
|
97
89
|
config.workers.maximum = t
|
|
98
90
|
}
|
|
99
91
|
|
|
92
|
+
/*
|
|
93
|
+
`static` is how many workers the application starts with, and an entry that declared its own
|
|
94
|
+
`minimum` has already said. The runtime-wide default -- which is 1 unless the project set one --
|
|
95
|
+
must not override it: `{ dynamic: true, minimum: 2 }` otherwise starts a single worker and
|
|
96
|
+
leaves the autoscaler to climb to the floor the entry asked for.
|
|
97
|
+
*/
|
|
98
|
+
if (typeof declaredStatic === 'undefined' && typeof declaredMinimum !== 'undefined') {
|
|
99
|
+
config.workers.static = declaredMinimum
|
|
100
|
+
}
|
|
101
|
+
|
|
100
102
|
if (typeof config.workers.static === 'undefined') {
|
|
101
103
|
config.workers.static = config.workers.minimum
|
|
102
104
|
}
|
|
@@ -132,49 +134,6 @@ export function autoDetectPprofCapture (config) {
|
|
|
132
134
|
return config
|
|
133
135
|
}
|
|
134
136
|
|
|
135
|
-
export async function wrapInRuntimeConfig (config, context) {
|
|
136
|
-
let applicationId = 'main'
|
|
137
|
-
try {
|
|
138
|
-
const packageJson = JSON.parse(await readFile(join(config[kMetadata].root, 'package.json'), 'utf-8'))
|
|
139
|
-
applicationId = packageJson?.name ?? 'main'
|
|
140
|
-
|
|
141
|
-
if (applicationId.startsWith('@')) {
|
|
142
|
-
applicationId = applicationId.split('/')[1]
|
|
143
|
-
}
|
|
144
|
-
} catch (err) {
|
|
145
|
-
// on purpose, the package.json might be missing
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const production = context?.isProduction ?? context?.production
|
|
149
|
-
|
|
150
|
-
const runtimeConfig = config.runtime ?? {}
|
|
151
|
-
|
|
152
|
-
// Important: do not change the order of the properties in this object
|
|
153
|
-
/* c8 ignore next */
|
|
154
|
-
const wrapped = {
|
|
155
|
-
$schema: schema.$id,
|
|
156
|
-
watch: !production,
|
|
157
|
-
...omitProperties(runtimeConfig, runtimeUnwrappablePropertiesList),
|
|
158
|
-
applications: [
|
|
159
|
-
{
|
|
160
|
-
id: applicationId,
|
|
161
|
-
path: config[kMetadata].root,
|
|
162
|
-
config: config[kMetadata].path,
|
|
163
|
-
...(runtimeConfig.application ?? {})
|
|
164
|
-
}
|
|
165
|
-
]
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return loadConfiguration(wrapped, context?.schema ?? schema, {
|
|
169
|
-
validationOptions,
|
|
170
|
-
transform,
|
|
171
|
-
upgrade,
|
|
172
|
-
replaceEnv: true,
|
|
173
|
-
root: config[kMetadata].root,
|
|
174
|
-
...context
|
|
175
|
-
})
|
|
176
|
-
}
|
|
177
|
-
|
|
178
137
|
export function parseInspectorOptions (config, inspect, inspectBreak) {
|
|
179
138
|
const hasInspect = inspect != null
|
|
180
139
|
const hasInspectBrk = inspectBreak != null
|
|
@@ -229,16 +188,6 @@ export function parseInspectorOptions (config, inspect, inspectBreak) {
|
|
|
229
188
|
export async function prepareAddedApplications (config, entries, existingIds = []) {
|
|
230
189
|
const metadata = config[kMetadata]
|
|
231
190
|
|
|
232
|
-
if (!metadata?.v4) {
|
|
233
|
-
const prepared = []
|
|
234
|
-
|
|
235
|
-
for (const entry of entries) {
|
|
236
|
-
prepared.push(await prepareApplication(config, entry, config.workers))
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
return prepared
|
|
240
|
-
}
|
|
241
|
-
|
|
242
191
|
const { applications } = await loadAdditionalApplications({
|
|
243
192
|
configPath: metadata.path,
|
|
244
193
|
entries,
|
|
@@ -247,6 +196,8 @@ export async function prepareAddedApplications (config, entries, existingIds = [
|
|
|
247
196
|
command: metadata.v4.command,
|
|
248
197
|
mode: metadata.v4.mode,
|
|
249
198
|
production: metadata.v4.production,
|
|
199
|
+
realEnv: metadata.v4.realEnv,
|
|
200
|
+
customEnvFile: metadata.v4.customEnvFile,
|
|
250
201
|
// The runtime is the fallback scope for capability resolution, the same as at boot.
|
|
251
202
|
runtimeScope: runtimeScopePath
|
|
252
203
|
})
|
|
@@ -260,60 +211,6 @@ export async function prepareAddedApplications (config, entries, existingIds = [
|
|
|
260
211
|
return prepared
|
|
261
212
|
}
|
|
262
213
|
|
|
263
|
-
export async function prepareApplication (config, application, defaultWorkers) {
|
|
264
|
-
// We need to have absolute paths here, ot the `loadConfig` will fail
|
|
265
|
-
// Make sure we don't resolve if env var was not replaced
|
|
266
|
-
if (application.path && !isAbsolute(application.path) && !application.path.match(/^\{.*\}$/)) {
|
|
267
|
-
application.path = resolvePath(config[kMetadata].root, application.path)
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
if (application.path && application.config) {
|
|
271
|
-
application.config = resolvePath(application.path, application.config)
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Skip capability detection for external services (url without path)
|
|
275
|
-
// These services will have their path resolved later in runtime.js #setupApplication
|
|
276
|
-
// Attempting to detect capability here would cause slow glob operations on the cwd
|
|
277
|
-
if (application.url && !application.path) {
|
|
278
|
-
application.type = 'unknown'
|
|
279
|
-
} else {
|
|
280
|
-
try {
|
|
281
|
-
let pkg
|
|
282
|
-
|
|
283
|
-
if (application.config) {
|
|
284
|
-
const applicationConfig = await loadConfiguration(application.config)
|
|
285
|
-
pkg = await loadConfigurationModule(application.path, applicationConfig)
|
|
286
|
-
|
|
287
|
-
application.type = extractModuleFromSchemaUrl(applicationConfig, true).module
|
|
288
|
-
application.skipTelemetryHooks = pkg.skipTelemetryHooks
|
|
289
|
-
} else {
|
|
290
|
-
const { moduleName, capability } = await importCapabilityAndConfig(application.path)
|
|
291
|
-
pkg = capability
|
|
292
|
-
|
|
293
|
-
application.type = moduleName
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
application.skipTelemetryHooks = pkg.skipTelemetryHooks
|
|
297
|
-
|
|
298
|
-
// This is needed to work around Rust bug on dylibs:
|
|
299
|
-
// https://github.com/rust-lang/rust/issues/91979
|
|
300
|
-
// https://github.com/rollup/rollup/issues/5761
|
|
301
|
-
const _require = createRequire(application.path)
|
|
302
|
-
for (const m of pkg.modulesToLoad ?? []) {
|
|
303
|
-
const toLoad = _require.resolve(m)
|
|
304
|
-
loadModule(_require, toLoad).catch(() => {})
|
|
305
|
-
}
|
|
306
|
-
} catch (err) {
|
|
307
|
-
// This should not happen, it happens on running some unit tests if we prepare the runtime
|
|
308
|
-
// when not all the applications configs are available. Given that we are running this only
|
|
309
|
-
// to ddetermine the type of the application, it's safe to ignore this error and default to unknown
|
|
310
|
-
application.type = 'unknown'
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return finalizeApplication(config, application, defaultWorkers)
|
|
315
|
-
}
|
|
316
|
-
|
|
317
214
|
// Everything an application needs regardless of how its capability was determined. v3 discovers
|
|
318
215
|
// that by loading the application's config file; v4 has it from the loader's envelope before any
|
|
319
216
|
// worker exists.
|
|
@@ -337,7 +234,7 @@ export function finalizeApplication (config, application, defaultWorkers) {
|
|
|
337
234
|
|
|
338
235
|
/*
|
|
339
236
|
The v4 preparation. The loader resolved the path, selected the capability and imported its schema
|
|
340
|
-
subpath — which carries
|
|
237
|
+
subpath — which carries skipTracingHooks and modulesToLoad — all main-side, before any worker
|
|
341
238
|
existed. So there is nothing to discover here, and in particular no application config file to
|
|
342
239
|
re-read: that is the whole point of evaluating configuration exactly once per load.
|
|
343
240
|
*/
|
|
@@ -346,14 +243,37 @@ export async function prepareV4Application (config, application, defaultWorkers)
|
|
|
346
243
|
// capability payload as data and never re-reads a file.
|
|
347
244
|
application.resolvedConfig = application.config ?? {}
|
|
348
245
|
application.config = undefined
|
|
246
|
+
|
|
247
|
+
/*
|
|
248
|
+
A module application names an npm package as its capability instead of describing one in a file.
|
|
249
|
+
The package is resolved from the runtime root, and its own directory (sourcePath) is kept
|
|
250
|
+
separate from the application's writable root (path) — which the module treats as a working
|
|
251
|
+
directory and which need not exist yet. Keyed on moduleApplication, not module: a regular
|
|
252
|
+
application also carries a module (its detected capability), and must not be resolved this way.
|
|
253
|
+
*/
|
|
254
|
+
if (application.moduleApplication) {
|
|
255
|
+
if (!application.path) {
|
|
256
|
+
throw new InvalidArgumentError(`Application "${application.id}" must define path when module is set`)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
application.sourcePath = dirname(findPackageJSON(application.module, resolvePath(config[kMetadata].root, 'noop.js')))
|
|
261
|
+
} catch (error) {
|
|
262
|
+
throw new MissingDependencyError(application.module, { cause: error })
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
application.moduleRoot = config[kMetadata].root
|
|
266
|
+
await createDirectory(application.path)
|
|
267
|
+
}
|
|
268
|
+
|
|
349
269
|
application.type = application.module ?? 'unknown'
|
|
350
|
-
application.
|
|
270
|
+
application.skipTracingHooks = application.capabilityMetadata?.skipTracingHooks ?? false
|
|
351
271
|
|
|
352
272
|
// This is needed to work around a Rust bug on dylibs, as in the v3 path.
|
|
353
273
|
const modulesToLoad = application.capabilityMetadata?.modulesToLoad ?? []
|
|
354
274
|
|
|
355
275
|
if (modulesToLoad.length > 0 && application.path) {
|
|
356
|
-
const _require = createRequire(application.path)
|
|
276
|
+
const _require = createRequire(application.sourcePath ?? application.path)
|
|
357
277
|
|
|
358
278
|
for (const m of modulesToLoad) {
|
|
359
279
|
try {
|
|
@@ -368,130 +288,6 @@ export async function prepareV4Application (config, application, defaultWorkers)
|
|
|
368
288
|
return finalizeApplication(config, application, defaultWorkers)
|
|
369
289
|
}
|
|
370
290
|
|
|
371
|
-
function isApplicationEnabled (application, environment) {
|
|
372
|
-
const { enabled } = application
|
|
373
|
-
|
|
374
|
-
if (typeof enabled === 'undefined') {
|
|
375
|
-
return true
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
if (typeof enabled === 'string') {
|
|
379
|
-
return enabled !== 'false'
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
if (typeof enabled === 'object' && enabled !== null) {
|
|
383
|
-
return enabled[environment] ?? true
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
return enabled
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
export async function transform (config, _, context) {
|
|
390
|
-
const production = context?.isProduction ?? context?.production
|
|
391
|
-
const environment = production ? 'production' : 'development'
|
|
392
|
-
const applications = [...(config.applications ?? []), ...(config.services ?? []), ...(config.web ?? [])]
|
|
393
|
-
|
|
394
|
-
const watchType = typeof config.watch
|
|
395
|
-
if (watchType === 'string') {
|
|
396
|
-
config.watch = config.watch === 'true'
|
|
397
|
-
} else if (watchType === 'undefined') {
|
|
398
|
-
config.watch = !production
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Migrate the old verticalScaler property, only applied if the new settings are not set, otherwise workers takes precedence
|
|
402
|
-
// TODO: Remove in the next major version
|
|
403
|
-
if (config.verticalScaler) {
|
|
404
|
-
config.workers ??= {}
|
|
405
|
-
config.workers.total ??= config.verticalScaler.maxTotalWorkers
|
|
406
|
-
config.workers.dynamic ??= config.verticalScaler.enabled
|
|
407
|
-
config.workers.minimum ??= config.verticalScaler.minWorkers ?? 1
|
|
408
|
-
config.workers.maximum ??= config.verticalScaler.maxWorkers ?? config.verticalScaler.maxTotalWorkers ?? 1
|
|
409
|
-
config.workers.maxMemory ??= config.verticalScaler.maxTotalMemory
|
|
410
|
-
|
|
411
|
-
if (config.verticalScaler.cooldownSec) {
|
|
412
|
-
config.workers.cooldown ??= config.verticalScaler.cooldownSec * 1000
|
|
413
|
-
}
|
|
414
|
-
if (config.verticalScaler.gracePeriod) {
|
|
415
|
-
config.workers.gracePeriod ??= config.verticalScaler.gracePeriod
|
|
416
|
-
}
|
|
417
|
-
if (config.verticalScaler.scaleUpELU) {
|
|
418
|
-
config.workers.scaleUpELU ??= config.verticalScaler.scaleUpELU
|
|
419
|
-
}
|
|
420
|
-
if (config.verticalScaler.scaleDownELU) {
|
|
421
|
-
config.workers.scaleDownELU ??= config.verticalScaler.scaleDownELU
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
if (config.verticalScaler.applications) {
|
|
425
|
-
for (const appId in config.verticalScaler.applications) {
|
|
426
|
-
let appConfig = applications.find((app) => app.id === appId)
|
|
427
|
-
if (!appConfig) {
|
|
428
|
-
appConfig = { id: appId, workers: {} }
|
|
429
|
-
applications.push(appConfig)
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const scaleConfig = config.verticalScaler.applications[appId]
|
|
433
|
-
const workersConfig = appConfig.workers ??= {}
|
|
434
|
-
|
|
435
|
-
workersConfig.minimum ??= scaleConfig.minWorkers ?? config.workers.minimum
|
|
436
|
-
workersConfig.maximum ??= scaleConfig.maxWorkers ?? config.workers.maximum
|
|
437
|
-
|
|
438
|
-
if (scaleConfig.scaleUpELU) {
|
|
439
|
-
workersConfig.scaleUpELU ??= scaleConfig.scaleUpELU
|
|
440
|
-
}
|
|
441
|
-
if (scaleConfig.scaleDownELU) {
|
|
442
|
-
workersConfig.scaleDownELU ??= scaleConfig.scaleDownELU
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
config.verticalScaler = undefined
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
if (config.autoload) {
|
|
451
|
-
const { exclude = [], mappings = {} } = config.autoload
|
|
452
|
-
let { path } = config.autoload
|
|
453
|
-
|
|
454
|
-
path = resolvePath(config[kMetadata].root, path)
|
|
455
|
-
const entries = await readdir(path, { withFileTypes: true })
|
|
456
|
-
|
|
457
|
-
for (let i = 0; i < entries.length; ++i) {
|
|
458
|
-
const entry = entries[i]
|
|
459
|
-
|
|
460
|
-
if (exclude.includes(entry.name) || !entry.isDirectory()) {
|
|
461
|
-
continue
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
const mapping = mappings[entry.name] ?? {}
|
|
465
|
-
const id = mapping.id ?? entry.name
|
|
466
|
-
const entryPath = join(path, entry.name)
|
|
467
|
-
|
|
468
|
-
let config
|
|
469
|
-
const configFilename = mapping.config ?? (await findConfigurationFile(entryPath))
|
|
470
|
-
|
|
471
|
-
if (typeof configFilename === 'string') {
|
|
472
|
-
config = join(entryPath, configFilename)
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
const application = { id, config, path: entryPath, ...mapping }
|
|
476
|
-
const existingApplicationId = applications.findIndex(application => application.id === id)
|
|
477
|
-
|
|
478
|
-
if (existingApplicationId !== -1) {
|
|
479
|
-
applications[existingApplicationId] = { ...application, ...applications[existingApplicationId] }
|
|
480
|
-
} else {
|
|
481
|
-
applications.push(application)
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
for (let i = applications.length - 1; i >= 0; --i) {
|
|
487
|
-
if (!isApplicationEnabled(applications[i], environment)) {
|
|
488
|
-
applications.splice(i, 1)
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
return finalizeConfiguration(config, applications, context, production, prepareApplication)
|
|
493
|
-
}
|
|
494
|
-
|
|
495
291
|
// The half of the transform the loader does not own: inspector options, worker counts, the
|
|
496
292
|
// per-application preparation, and the runtime-level normalizations. v4 reaches this directly,
|
|
497
293
|
// having already expanded autoload and resolved enabled in the eval worker.
|
package/lib/errors.js
CHANGED
|
@@ -103,6 +103,10 @@ export const NoConfigFileFoundError = createError(
|
|
|
103
103
|
"No config file found for application '%s'"
|
|
104
104
|
)
|
|
105
105
|
export const MissingDependencyError = createError(`${ERROR_PREFIX}_MISSING_DEPENDENCY`, 'Missing dependency: "%s"')
|
|
106
|
+
export const InvalidApplicationModuleError = createError(
|
|
107
|
+
`${ERROR_PREFIX}_INVALID_APPLICATION_MODULE`,
|
|
108
|
+
'Application module "%s" must export a create function'
|
|
109
|
+
)
|
|
106
110
|
export const InspectAndInspectBrkError = createError(
|
|
107
111
|
`${ERROR_PREFIX}_INSPECT_AND_INSPECT_BRK`,
|
|
108
112
|
'--inspect and --inspect-brk cannot be used together'
|
|
@@ -232,3 +236,7 @@ export const WorkerInterceptorJoinTimeoutError = createError(
|
|
|
232
236
|
`${ERROR_PREFIX}_WORKER_INTERCEPTOR_JOIN_TIMEOUT`,
|
|
233
237
|
'The %s failed to join the mesh network in %dms.'
|
|
234
238
|
)
|
|
239
|
+
export const MixedServingStateError = createError(
|
|
240
|
+
`${ERROR_PREFIX}_MIXED_SERVING_STATE`,
|
|
241
|
+
'The workers of the application "%s" disagree about how it serves: %s. One application is one thing; workers that disagree about whether it serves HTTP would have the mesh route a share of requests to a worker that destroys them.'
|
|
242
|
+
)
|