@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/schema.js
CHANGED
|
@@ -33,6 +33,12 @@ const runtimeLogger = {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/*
|
|
37
|
+
The root logger is not the application one: it carries `captureStdio` and the `pino` key mapping,
|
|
38
|
+
which only the runtime reads. Naming them apart is what lets a person write the type of either.
|
|
39
|
+
*/
|
|
40
|
+
runtimeLogger.title = 'RuntimeLoggerOptions'
|
|
41
|
+
|
|
36
42
|
schemaComponents.runtimeProperties.logger = runtimeLogger
|
|
37
43
|
|
|
38
44
|
schemaComponents.runtimeProperties.verticalScaler.properties.applications = {
|
|
@@ -71,6 +77,76 @@ const platformaticRuntimeSchema = {
|
|
|
71
77
|
|
|
72
78
|
export const schema = platformaticRuntimeSchema
|
|
73
79
|
|
|
80
|
+
/*
|
|
81
|
+
The keys v4 does not implement. Validating a v4 configuration against the v3 schema accepted them
|
|
82
|
+
and then ignored them, which is the worst of both: `envfile: './deploy.env'` at the root looked
|
|
83
|
+
like it was doing something. They survive on the v3 schema, which still serves v3 projects, and
|
|
84
|
+
inside migrate's legacy reader.
|
|
85
|
+
|
|
86
|
+
Root `envfile` is removed, not renamed — an entry may still declare one, and that property lives
|
|
87
|
+
on the application schema rather than here.
|
|
88
|
+
|
|
89
|
+
`$schema` is different in kind: machine writers of the plain-object form still stamp it, and the
|
|
90
|
+
loader reads it for version detection and strips it before validation. The schema refusing it is
|
|
91
|
+
what makes the strip load-bearing rather than decorative — a stamp that reached AJV would mean the
|
|
92
|
+
loader had skipped the step that checks the file is not a v3 one.
|
|
93
|
+
|
|
94
|
+
`verticalScaler` is the deprecated spelling of `workers`, kept on the v3 schema with a transform
|
|
95
|
+
that migrates it. v4 has one spelling: a configuration that still says `verticalScaler` is told
|
|
96
|
+
so by the schema rather than being quietly rewritten, which is the only way the two cannot
|
|
97
|
+
disagree about which of them a project meant.
|
|
98
|
+
|
|
99
|
+
`services` and `web` are the same story: v3's spellings of `applications`, folded by the shared
|
|
100
|
+
transform. Alone they fail the pipeline's topology check by name; beside an `applications` key
|
|
101
|
+
they would validate and be folded silently, so the schema refuses them instead.
|
|
102
|
+
*/
|
|
103
|
+
const removedInV4 = ['envfile', 'strictEnv', '$schema', 'verticalScaler', 'services', 'web']
|
|
104
|
+
|
|
105
|
+
const { ...v4Properties } = platformaticRuntimeSchema.properties
|
|
106
|
+
|
|
107
|
+
for (const key of removedInV4) {
|
|
108
|
+
delete v4Properties[key]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/*
|
|
112
|
+
v3's entry anyOf required an id beside the path or url, because v3 derived ids only for
|
|
113
|
+
autoloaded applications. v4 derives them at every position -- explicit entries included, from the
|
|
114
|
+
package name and then the directory -- so requiring one would refuse an entry the loader handles.
|
|
115
|
+
What a v4 entry genuinely needs is a place: a path or a url.
|
|
116
|
+
|
|
117
|
+
Copied rather than edited, because `applications.items` is shared with the v3 schema, where the
|
|
118
|
+
requirement still holds.
|
|
119
|
+
*/
|
|
120
|
+
const v4EntryItems = {
|
|
121
|
+
...v4Properties.applications.items,
|
|
122
|
+
anyOf: [{ required: ['path'] }, { required: ['url'] }]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
v4Properties.applications = { ...v4Properties.applications, items: v4EntryItems }
|
|
126
|
+
|
|
127
|
+
/*
|
|
128
|
+
The singular shorthand: one application with runtime options, Level 1b. It is an application
|
|
129
|
+
entry whose identity and place the loader both supply -- the id derives from the package name
|
|
130
|
+
and the path defaults to the configuration file's own directory -- so it keeps the entry's
|
|
131
|
+
properties and drops the anyOf entirely.
|
|
132
|
+
*/
|
|
133
|
+
const { anyOf: _entryRequirements, ...applicationShorthand } = v4EntryItems
|
|
134
|
+
|
|
135
|
+
v4Properties.application = applicationShorthand
|
|
136
|
+
|
|
137
|
+
export const v4Schema = {
|
|
138
|
+
...platformaticRuntimeSchema,
|
|
139
|
+
$id: `https://schemas.platformatic.dev/@platformatic/runtime/${version}-v4.json`,
|
|
140
|
+
properties: v4Properties,
|
|
141
|
+
/*
|
|
142
|
+
The v3 anyOf listed the removed spellings, and it is decorative on the v4 path anyway -- by the
|
|
143
|
+
time either validation pass runs, normalization has already defaulted `applications`. The gate
|
|
144
|
+
that actually asks the question is the pipeline's topology check, before that default; this
|
|
145
|
+
matches it so the schema and the loader tell the same story.
|
|
146
|
+
*/
|
|
147
|
+
anyOf: [{ required: ['autoload'] }, { required: ['applications'] }, { required: ['application'] }]
|
|
148
|
+
}
|
|
149
|
+
|
|
74
150
|
if (import.meta.main) {
|
|
75
151
|
console.log(JSON.stringify(platformaticRuntimeSchema, null, 2))
|
|
76
152
|
}
|
package/lib/worker/controller.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
+
import { configurationFileNames } from '@platformatic/foundation/lib/v4/index.js'
|
|
1
2
|
import {
|
|
2
3
|
ensureLoggableError,
|
|
3
4
|
FileWatcher,
|
|
4
5
|
kHandledError,
|
|
5
|
-
|
|
6
|
-
loadConfiguration,
|
|
7
|
-
loadConfigurationModule,
|
|
8
|
-
mirrorGlobalDispatcherForBuiltinFetch
|
|
6
|
+
loadConfigurationModule
|
|
9
7
|
} from '@platformatic/foundation'
|
|
10
8
|
import {
|
|
11
9
|
getLogger,
|
|
@@ -20,14 +18,18 @@ import {
|
|
|
20
18
|
import { importCapabilityPackage } from '@platformatic/basic'
|
|
21
19
|
import debounce from 'debounce'
|
|
22
20
|
import { EventEmitter } from 'node:events'
|
|
23
|
-
import { existsSync } from 'node:fs'
|
|
24
21
|
import { resolve } from 'node:path'
|
|
25
22
|
import { getActiveResourcesInfo } from 'node:process'
|
|
26
23
|
import { workerData } from 'node:worker_threads'
|
|
27
|
-
import { getGlobalDispatcher
|
|
28
|
-
import {
|
|
24
|
+
import { getGlobalDispatcher } from 'undici'
|
|
25
|
+
import {
|
|
26
|
+
ApplicationAlreadyStartedError,
|
|
27
|
+
exitCodes,
|
|
28
|
+
InvalidApplicationModuleError,
|
|
29
|
+
RuntimeNotStartedError
|
|
30
|
+
} from '../errors.js'
|
|
29
31
|
import { getApplicationUrl } from '../utils.js'
|
|
30
|
-
import {
|
|
32
|
+
import { installGlobalDispatcher, refreshGlobalDispatcher } from './interceptors.js'
|
|
31
33
|
|
|
32
34
|
function fetchApplicationUrl (application, key) {
|
|
33
35
|
if (!key.endsWith('_URL') || !application.id) {
|
|
@@ -84,9 +86,10 @@ export class Controller extends EventEmitter {
|
|
|
84
86
|
applicationId: this.applicationId,
|
|
85
87
|
workerId: this.workerId,
|
|
86
88
|
directory: this.applicationConfig.path,
|
|
89
|
+
sourcePath: this.applicationConfig.sourcePath,
|
|
87
90
|
dependencies: this.applicationConfig.dependencies,
|
|
88
91
|
isProduction: this.applicationConfig.isProduction,
|
|
89
|
-
|
|
92
|
+
tracingConfig: this.applicationConfig.tracing,
|
|
90
93
|
loggerConfig: runtimeConfig.logger,
|
|
91
94
|
metricsConfig,
|
|
92
95
|
worker: workerData?.worker,
|
|
@@ -133,45 +136,40 @@ export class Controller extends EventEmitter {
|
|
|
133
136
|
with workers: 4 evaluated user code five times and could reach five different answers.
|
|
134
137
|
|
|
135
138
|
The capability is imported through the canonical resolution order, application-scoped first,
|
|
136
|
-
so the copy that runs here is the copy whose schema validated the payload main-side.
|
|
137
|
-
|
|
138
|
-
/*
|
|
139
|
-
v4: the configuration was evaluated exactly once, main-side, and this worker receives the
|
|
140
|
-
validated capability payload as data. There is no file to re-read and no schema to
|
|
141
|
-
rediscover — which is the point, since re-parsing per worker meant an application with
|
|
142
|
-
workers: 4 evaluated user code five times and could reach five different answers.
|
|
143
|
-
|
|
144
|
-
The capability is imported through the canonical resolution order, application-scoped
|
|
145
|
-
first, so the copy that runs here is the copy whose schema validated the payload.
|
|
139
|
+
so the copy that runs here is the copy whose schema validated the payload main-side. When the
|
|
140
|
+
entry names an npm module, that module is the capability, resolved the same way.
|
|
146
141
|
*/
|
|
147
142
|
if (appConfig.resolvedConfig) {
|
|
148
|
-
const pkg = await importCapabilityPackage(appConfig.path, appConfig.module
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
} else {
|
|
152
|
-
// Before returning the base application, check if there is any file we recognize
|
|
153
|
-
// and the user just forgot to specify in the configuration.
|
|
154
|
-
if (!appConfig.config) {
|
|
155
|
-
const candidate = listRecognizedConfigurationFiles().find(f => existsSync(resolve(appConfig.path, f)))
|
|
143
|
+
const pkg = await importCapabilityPackage(appConfig.path, appConfig.module, {
|
|
144
|
+
runtimeScope: import.meta.filename
|
|
145
|
+
})
|
|
156
146
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
147
|
+
// A module application names its capability directly. If that package exports no `create`,
|
|
148
|
+
// name it rather than letting the call below throw a bare "pkg.create is not a function".
|
|
149
|
+
if (appConfig.module && typeof pkg.create !== 'function') {
|
|
150
|
+
throw new InvalidApplicationModuleError(appConfig.module)
|
|
160
151
|
}
|
|
161
152
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
this
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
153
|
+
/*
|
|
154
|
+
`resolved` says this object has already been through the loader: its environment was
|
|
155
|
+
layered main-side, it holds no placeholders, and it has been validated against this
|
|
156
|
+
capability's schema. It is what tells the capability apart from an embedder handing over
|
|
157
|
+
an object nobody has checked, which still gets validated on the way in.
|
|
158
|
+
*/
|
|
159
|
+
this.capability = await pkg.create(appConfig.path, appConfig.resolvedConfig, {
|
|
160
|
+
...this.#context,
|
|
161
|
+
resolved: true
|
|
162
|
+
})
|
|
163
|
+
} else {
|
|
164
|
+
/*
|
|
165
|
+
No payload at all, which the v4 loader does not produce -- `prepareV4Application` gives
|
|
166
|
+
every entry a `resolvedConfig`, an empty object where there is nothing to say. What is
|
|
167
|
+
left here is an embedder constructing a Controller by hand, and the bundled base
|
|
168
|
+
capability is the answer for that: there is no configuration file to look for, because v4
|
|
169
|
+
decides that main-side and hands the result over.
|
|
170
|
+
*/
|
|
171
|
+
const pkg = await loadConfigurationModule(resolve(import.meta.dirname, '../..'), {}, '@platformatic/basic')
|
|
172
|
+
this.capability = await pkg.create(appConfig.path, {}, this.#context)
|
|
175
173
|
}
|
|
176
174
|
|
|
177
175
|
this.#updateDispatcher()
|
|
@@ -323,11 +321,20 @@ export class Controller extends EventEmitter {
|
|
|
323
321
|
return
|
|
324
322
|
}
|
|
325
323
|
|
|
324
|
+
/*
|
|
325
|
+
The application's own configuration file is not the worker's to watch. The runtime evaluates
|
|
326
|
+
configuration once, main-side, and watches every file that evaluation read -- so a change to
|
|
327
|
+
one is a configuration change and the runtime reloads for it.
|
|
328
|
+
|
|
329
|
+
Watching it here as well meant both fired for a single edit: the runtime tore itself down and
|
|
330
|
+
rebuilt while this worker restarted the same application, and the rebuilt runtime could not
|
|
331
|
+
bind its management socket because the one it replaced had not released it yet.
|
|
332
|
+
*/
|
|
326
333
|
const fileWatcher = new FileWatcher({
|
|
327
334
|
path: watch.path,
|
|
328
335
|
/* c8 ignore next 2 */
|
|
329
336
|
allowToWatch: watch?.allow,
|
|
330
|
-
watchIgnore: watch?.ignore || []
|
|
337
|
+
watchIgnore: [...(watch?.ignore || []), ...configurationFileNames]
|
|
331
338
|
})
|
|
332
339
|
|
|
333
340
|
fileWatcher.on('update', this.#debouncedRestart)
|
|
@@ -358,8 +365,8 @@ export class Controller extends EventEmitter {
|
|
|
358
365
|
}
|
|
359
366
|
|
|
360
367
|
#updateDispatcher () {
|
|
361
|
-
const
|
|
362
|
-
const telemetryId =
|
|
368
|
+
const tracingConfig = this.#context.tracingConfig
|
|
369
|
+
const telemetryId = tracingConfig?.applicationName
|
|
363
370
|
|
|
364
371
|
const interceptor = dispatch => {
|
|
365
372
|
return function InterceptedDispatch (opts, handler) {
|
|
@@ -375,9 +382,7 @@ export class Controller extends EventEmitter {
|
|
|
375
382
|
|
|
376
383
|
const dispatcher = getGlobalDispatcher().compose(interceptor)
|
|
377
384
|
|
|
378
|
-
|
|
379
|
-
setGlobalDispatcher(dispatcher)
|
|
380
|
-
mirrorGlobalDispatcherForBuiltinFetch(dispatcher)
|
|
385
|
+
installGlobalDispatcher(dispatcher)
|
|
381
386
|
}
|
|
382
387
|
|
|
383
388
|
#setupHandlers (timeout) {
|
|
@@ -6,14 +6,14 @@ import {
|
|
|
6
6
|
getOnHttpCacheMiss,
|
|
7
7
|
getOnHttpCacheRequest
|
|
8
8
|
} from '@platformatic/globals'
|
|
9
|
-
import { createTelemetryThreadInterceptorHooks } from '@platformatic/
|
|
9
|
+
import { createTelemetryThreadInterceptorHooks } from '@platformatic/tracing'
|
|
10
10
|
import { createRequire } from 'node:module'
|
|
11
11
|
import { join } from 'node:path'
|
|
12
12
|
import { pathToFileURL } from 'node:url'
|
|
13
|
-
import {
|
|
14
|
-
import { Agent, Client, Pool } from 'undici'
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
13
|
+
import { workerData } from 'node:worker_threads'
|
|
14
|
+
import { Agent, Client, Pool, setGlobalDispatcher } from 'undici'
|
|
15
|
+
import { createInterceptor } from 'undici-thread-interceptor'
|
|
16
|
+
import { createTargetPermissionHook } from '../policies.js'
|
|
17
17
|
import { RemoteCacheStore, httpCacheInterceptor } from './http-cache.js'
|
|
18
18
|
|
|
19
19
|
const kPlatformaticGlobalDispatcher = Symbol.for('platformatic.undici.globalDispatcher')
|
|
@@ -43,8 +43,11 @@ export function refreshGlobalDispatcher () {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export async function setDispatcher (runtimeConfig) {
|
|
46
|
-
const
|
|
47
|
-
const
|
|
46
|
+
const tracingHooks = runtimeConfig.tracing ? createTelemetryThreadInterceptorHooks() : {}
|
|
47
|
+
const threadDispatcher = createThreadInterceptor(runtimeConfig, tracingHooks)
|
|
48
|
+
const threadInterceptor = threadDispatcher
|
|
49
|
+
|
|
50
|
+
await threadDispatcher.ready
|
|
48
51
|
|
|
49
52
|
let cacheInterceptor = null
|
|
50
53
|
if (runtimeConfig.httpCache) {
|
|
@@ -65,30 +68,29 @@ export async function setDispatcher (runtimeConfig) {
|
|
|
65
68
|
[threadInterceptor, ...userInterceptors, cacheInterceptor].filter(Boolean)
|
|
66
69
|
)
|
|
67
70
|
|
|
68
|
-
|
|
69
|
-
mirrorGlobalDispatcherForBuiltinFetch(dispatcher, createLegacyDispatcher(dispatcher))
|
|
71
|
+
installGlobalDispatcher(dispatcher)
|
|
70
72
|
return dispatcher
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
composeRuntimeDispatcher = installDispatcher
|
|
74
76
|
installDispatcher(new Agent(dispatcherOpts))
|
|
75
77
|
|
|
76
|
-
return {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const currentDispatcher = globalThis[Symbol.for('undici.globalDispatcher.2')]
|
|
82
|
-
|
|
83
|
-
if (legacyDispatcher && legacyDispatcher !== currentDispatcher && legacyDispatcher.constructor?.name === 'Dispatcher1Wrapper') {
|
|
84
|
-
try {
|
|
85
|
-
return new legacyDispatcher.constructor(dispatcher)
|
|
86
|
-
} catch {
|
|
87
|
-
return dispatcher
|
|
78
|
+
return {
|
|
79
|
+
threadDispatcher: {
|
|
80
|
+
interceptor: threadDispatcher,
|
|
81
|
+
server: null,
|
|
82
|
+
serverHooks: tracingHooks.serverHooks
|
|
88
83
|
}
|
|
89
84
|
}
|
|
85
|
+
}
|
|
90
86
|
|
|
91
|
-
|
|
87
|
+
export function installGlobalDispatcher (dispatcher) {
|
|
88
|
+
markAsPlatformaticDispatcher(dispatcher)
|
|
89
|
+
setGlobalDispatcher(dispatcher)
|
|
90
|
+
mirrorGlobalDispatcherForBuiltinFetch(
|
|
91
|
+
dispatcher,
|
|
92
|
+
globalThis[Symbol.for('undici.globalDispatcher.1')]
|
|
93
|
+
)
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
export async function updateUndiciInterceptors (undiciConfig) {
|
|
@@ -215,19 +217,17 @@ async function getDispatcherOpts (undiciConfig) {
|
|
|
215
217
|
return dispatcherOpts
|
|
216
218
|
}
|
|
217
219
|
|
|
218
|
-
function createThreadInterceptor (runtimeConfig) {
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
const telemetryHooks = telemetry ? createTelemetryThreadInterceptorHooks() : {}
|
|
222
|
-
|
|
223
|
-
const threadDispatcher = wire({
|
|
224
|
-
// Specifying the domain is critical to avoid flooding the DNS
|
|
225
|
-
// with requests for a domain that's never going to exist.
|
|
220
|
+
function createThreadInterceptor (runtimeConfig, tracingHooks) {
|
|
221
|
+
const threadDispatcher = createInterceptor({
|
|
222
|
+
meshId: workerData.meshId,
|
|
226
223
|
domain: '.plt.local',
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
224
|
+
connectTimeout: runtimeConfig.applicationTimeout,
|
|
225
|
+
bootstrapTimeout: runtimeConfig.applicationTimeout,
|
|
226
|
+
allowTarget: createTargetPermissionHook({
|
|
227
|
+
...runtimeConfig,
|
|
228
|
+
applicationId: workerData.applicationConfig.id
|
|
229
|
+
}),
|
|
230
|
+
...tracingHooks
|
|
231
231
|
})
|
|
232
232
|
return threadDispatcher
|
|
233
233
|
}
|
package/lib/worker/itc.js
CHANGED
|
@@ -2,6 +2,7 @@ import { ensureLoggableError, executeInParallel, executeWithTimeout, kTimeout }
|
|
|
2
2
|
import { getEvents, getLogger, getMessaging, updateGlobals } from '@platformatic/globals'
|
|
3
3
|
import { ITC, initializeITCTelemetry } from '@platformatic/itc'
|
|
4
4
|
import { Unpromise } from '@watchable/unpromise'
|
|
5
|
+
import { createServer } from 'undici-thread-interceptor'
|
|
5
6
|
import { once } from 'node:events'
|
|
6
7
|
import { createRequire } from 'node:module'
|
|
7
8
|
import { Duplex } from 'node:stream'
|
|
@@ -114,6 +115,7 @@ async function safeHandleInITC (worker, fn) {
|
|
|
114
115
|
async function closeITC (dispatcher, itc, messaging) {
|
|
115
116
|
try {
|
|
116
117
|
await dispatcher.interceptor.close()
|
|
118
|
+
await dispatcher.server?.close()
|
|
117
119
|
itc.close()
|
|
118
120
|
messaging.close()
|
|
119
121
|
} finally {
|
|
@@ -208,7 +210,28 @@ export async function setupITC (controller, application, dispatcher, sharedConte
|
|
|
208
210
|
}
|
|
209
211
|
}
|
|
210
212
|
|
|
211
|
-
|
|
213
|
+
const dispatchTarget = await controller.capability.getDispatchTarget()
|
|
214
|
+
if (dispatchTarget == null) {
|
|
215
|
+
await new Promise(() => {})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const serverTarget = dispatcher.serverHooks?.run
|
|
219
|
+
? wrapDispatchTarget(dispatchTarget, dispatcher.serverHooks.run)
|
|
220
|
+
: dispatchTarget
|
|
221
|
+
|
|
222
|
+
dispatcher.server = createServer({
|
|
223
|
+
meshId: workerData.meshId,
|
|
224
|
+
serverId: workerData.worker.id,
|
|
225
|
+
domain: `${application.id}.plt.local`,
|
|
226
|
+
server: serverTarget,
|
|
227
|
+
bootstrapTimeout: workerData.config.applicationTimeout,
|
|
228
|
+
metadata: {
|
|
229
|
+
applicationId: application.id,
|
|
230
|
+
workerId: workerData.worker.id
|
|
231
|
+
},
|
|
232
|
+
...dispatcher.serverHooks
|
|
233
|
+
})
|
|
234
|
+
await dispatcher.server.ready
|
|
212
235
|
|
|
213
236
|
const scheduledTasks =
|
|
214
237
|
typeof controller.capability.getScheduledTasks === 'function'
|
|
@@ -259,7 +282,8 @@ export async function setupITC (controller, application, dispatcher, sharedConte
|
|
|
259
282
|
},
|
|
260
283
|
|
|
261
284
|
async removeFromMesh () {
|
|
262
|
-
|
|
285
|
+
await dispatcher.server?.close()
|
|
286
|
+
dispatcher.server = null
|
|
263
287
|
},
|
|
264
288
|
|
|
265
289
|
inject (injectParams) {
|
|
@@ -291,6 +315,12 @@ export async function setupITC (controller, application, dispatcher, sharedConte
|
|
|
291
315
|
return controller.capability.getInfo()
|
|
292
316
|
},
|
|
293
317
|
|
|
318
|
+
// How this worker serves, which only the worker can answer: for a worker-classified
|
|
319
|
+
// capability it depends on what the application's factory returned in this worker.
|
|
320
|
+
getServingState () {
|
|
321
|
+
return controller.capability?.getServingState?.() ?? 'inactive'
|
|
322
|
+
},
|
|
323
|
+
|
|
294
324
|
async getApplicationConfig () {
|
|
295
325
|
const current = await controller.capability.getConfig()
|
|
296
326
|
// Remove all undefined keys from the config
|
|
@@ -498,3 +528,20 @@ export async function setupITC (controller, application, dispatcher, sharedConte
|
|
|
498
528
|
itc.listen()
|
|
499
529
|
return itc
|
|
500
530
|
}
|
|
531
|
+
|
|
532
|
+
function wrapDispatchTarget (target, run) {
|
|
533
|
+
if (typeof target.inject === 'function') {
|
|
534
|
+
return {
|
|
535
|
+
inject: (req, callback) => run(req, () => target.inject(req, callback)),
|
|
536
|
+
server: target.server,
|
|
537
|
+
emit: target.emit?.bind(target),
|
|
538
|
+
listenerCount: target.listenerCount?.bind(target)
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (typeof target.emit === 'function') {
|
|
543
|
+
return target
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return target
|
|
547
|
+
}
|
package/lib/worker/main.js
CHANGED
|
@@ -7,18 +7,16 @@ import {
|
|
|
7
7
|
parseMemorySize
|
|
8
8
|
} from '@platformatic/foundation'
|
|
9
9
|
import { getITC, getLogger, updateGlobals } from '@platformatic/globals'
|
|
10
|
-
import { addPinoInstrumentation } from '@platformatic/
|
|
10
|
+
import { addPinoInstrumentation } from '@platformatic/tracing'
|
|
11
11
|
import { Buffer } from 'node:buffer'
|
|
12
12
|
import { subscribe } from 'node:diagnostics_channel'
|
|
13
13
|
import { EventEmitter } from 'node:events'
|
|
14
|
-
import { readFile } from 'node:fs/promises'
|
|
15
14
|
import { ServerResponse } from 'node:http'
|
|
16
15
|
import inspector from 'node:inspector'
|
|
17
16
|
import { hostname } from 'node:os'
|
|
18
|
-
import { join
|
|
17
|
+
import { join } from 'node:path'
|
|
19
18
|
import { setDefaultHighWaterMark } from 'node:stream'
|
|
20
19
|
import { pathToFileURL } from 'node:url'
|
|
21
|
-
import { parseEnv } from 'node:util'
|
|
22
20
|
import { threadId, workerData } from 'node:worker_threads'
|
|
23
21
|
import pino from 'pino'
|
|
24
22
|
import { install as installUndiciGlobals } from 'undici'
|
|
@@ -100,7 +98,7 @@ function createLogger () {
|
|
|
100
98
|
pinoOptions.timestamp = buildPinoTimestamp(pinoOptions.timestamp)
|
|
101
99
|
}
|
|
102
100
|
|
|
103
|
-
if (workerData.config.logger?.openTelemetryExporter && workerData.applicationConfig.
|
|
101
|
+
if (workerData.config.logger?.openTelemetryExporter && workerData.applicationConfig.tracing?.enabled !== false) {
|
|
104
102
|
addPinoInstrumentation(pinoOptions)
|
|
105
103
|
}
|
|
106
104
|
|
|
@@ -231,44 +229,19 @@ async function main () {
|
|
|
231
229
|
|
|
232
230
|
await performPreloading(runtimeConfig, applicationConfig)
|
|
233
231
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
// Note that process.loadEnvFile is not used here as it never overrides an already defined
|
|
246
|
-
// variable. The worker environment is seeded from the environment the runtime resolved, which
|
|
247
|
-
// might contain values coming from an env file of the runtime. Those are only defaults, so the
|
|
248
|
-
// env file of this application, which is more specific, is allowed to override them. Real
|
|
249
|
-
// environment variables are never overridden.
|
|
250
|
-
const envFileFallbackKeys = new Set(workerData.envFileFallbackKeys ?? [])
|
|
251
|
-
|
|
252
|
-
try {
|
|
253
|
-
const applicationEnv = parseEnv(await readFile(envfile, 'utf-8'))
|
|
254
|
-
|
|
255
|
-
for (const [key, value] of Object.entries(applicationEnv)) {
|
|
256
|
-
if (!(key in process.env) || envFileFallbackKeys.has(key)) {
|
|
257
|
-
process.env[key] = value
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
} catch {
|
|
261
|
-
// Ignore if the file doesn't exist, similar to dotenv behavior
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (runtimeConfig.env) {
|
|
265
|
-
Object.assign(process.env, runtimeConfig.env)
|
|
266
|
-
}
|
|
267
|
-
if (applicationConfig.env) {
|
|
268
|
-
Object.assign(process.env, applicationConfig.env)
|
|
269
|
-
}
|
|
232
|
+
/*
|
|
233
|
+
Nothing is layered onto process.env here, and that absence is load-bearing. The loader resolved
|
|
234
|
+
this worker's entire environment main-side -- env-file chains, both env blocks, the injected
|
|
235
|
+
topology URLs, with the real environment authoritative over all of them -- and the worker was
|
|
236
|
+
*spawned* with that result. Re-reading a .env or re-applying an env block on top re-decides
|
|
237
|
+
what was already decided, with the opposite precedence: v3 treated blocks as pins over the real
|
|
238
|
+
environment, and v4 declares the inversion as a breaking change. The suppression of a block
|
|
239
|
+
value by the real environment is reported at boot, main-side, where the ladder is resolved.
|
|
240
|
+
*/
|
|
241
|
+
getLogger().debug('Using the worker environment resolved by the loader.')
|
|
270
242
|
|
|
271
243
|
const { threadDispatcher } = await setDispatcher(runtimeConfig)
|
|
244
|
+
updateGlobals({ undiciThreadInterceptor: threadDispatcher.interceptor })
|
|
272
245
|
|
|
273
246
|
const inspectorOptions = workerData.inspectorOptions
|
|
274
247
|
|
|
@@ -304,17 +277,7 @@ async function main () {
|
|
|
304
277
|
metricsConfig
|
|
305
278
|
)
|
|
306
279
|
|
|
307
|
-
await controller.init(cleanup)
|
|
308
|
-
|
|
309
|
-
if (runtimeConfig.basePath) {
|
|
310
|
-
const meta = await controller.capability.getMeta()
|
|
311
|
-
if (!meta.gateway?.wantsAbsoluteUrls) {
|
|
312
|
-
stripBasePath(runtimeConfig.basePath)
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
280
|
const sharedContext = new SharedContext()
|
|
317
|
-
// Limit the amount of methods a user can call
|
|
318
281
|
updateGlobals({
|
|
319
282
|
sharedContext: {
|
|
320
283
|
get: () => sharedContext.get(),
|
|
@@ -322,10 +285,20 @@ async function main () {
|
|
|
322
285
|
}
|
|
323
286
|
})
|
|
324
287
|
|
|
325
|
-
// Setup interaction with parent
|
|
288
|
+
// Setup interaction with the parent before loading the application so plugins
|
|
289
|
+
// can register messaging handlers during their initialization.
|
|
326
290
|
const itc = await setupITC(controller, applicationConfig, threadDispatcher, sharedContext)
|
|
327
291
|
updateGlobals({ itc })
|
|
328
292
|
|
|
293
|
+
await controller.init(cleanup)
|
|
294
|
+
|
|
295
|
+
if (runtimeConfig.basePath) {
|
|
296
|
+
const meta = await controller.capability.getMeta()
|
|
297
|
+
if (!meta.gateway?.wantsAbsoluteUrls) {
|
|
298
|
+
stripBasePath(runtimeConfig.basePath)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
329
302
|
// Setup management client for privileged applications
|
|
330
303
|
if (applicationConfig.management) {
|
|
331
304
|
const mgmtEnabled =
|
|
@@ -385,7 +358,9 @@ function stripBasePath (basePath) {
|
|
|
385
358
|
request.url = '/' + request.url
|
|
386
359
|
}
|
|
387
360
|
|
|
388
|
-
response
|
|
361
|
+
if (response) {
|
|
362
|
+
response[kBasePath] = basePath
|
|
363
|
+
}
|
|
389
364
|
}
|
|
390
365
|
})
|
|
391
366
|
|
package/lib/worker/symbols.js
CHANGED
|
@@ -10,9 +10,9 @@ export const kWorkerHealthSignals = Symbol.for('plt.runtime.worker.healthSignals
|
|
|
10
10
|
export const kWorkerStartTime = Symbol.for('plt.runtime.worker.startTime')
|
|
11
11
|
export const kWorkerUrl = Symbol.for('plt.runtime.worker.url')
|
|
12
12
|
export const kWorkerPortOffset = Symbol.for('plt.runtime.worker.portOffset')
|
|
13
|
+
export const kWorkerServerOptions = Symbol.for('plt.runtime.worker.serverOptions')
|
|
13
14
|
export const kLastHealthCheckELU = Symbol.for('plt.runtime.worker.lastHealthCheckELU')
|
|
14
15
|
export const kLastWorkerScalerELU = Symbol.for('plt.runtime.worker.lastWorkerScalerELU')
|
|
15
|
-
export const kInterceptorReadyPromise = Symbol.for('plt.runtime.worker.interceptorReadyPromise')
|
|
16
16
|
export const kIsSubprocessHost = Symbol.for('plt.runtime.worker.isSubprocessHost')
|
|
17
17
|
|
|
18
18
|
// This string marker should be safe to use since it belongs to Unicode private area
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/runtime",
|
|
3
|
-
"version": "4.0.0-new-config.
|
|
3
|
+
"version": "4.0.0-new-config.5",
|
|
4
4
|
"description": "Platformatic runtime",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -39,14 +39,14 @@
|
|
|
39
39
|
"typescript": "~6.0.3",
|
|
40
40
|
"undici-oidc-interceptor": "^0.9.0",
|
|
41
41
|
"why-is-node-running": "^2.2.2",
|
|
42
|
-
"@platformatic/composer": "4.0.0-new-config.
|
|
43
|
-
"@platformatic/db": "4.0.0-new-config.
|
|
44
|
-
"@platformatic/gateway": "4.0.0-new-config.
|
|
45
|
-
"@platformatic/node": "4.0.0-new-config.
|
|
46
|
-
"@platformatic/
|
|
47
|
-
"@platformatic/sql-graphql": "4.0.0-new-config.
|
|
48
|
-
"@platformatic/
|
|
49
|
-
"@platformatic/
|
|
42
|
+
"@platformatic/composer": "4.0.0-new-config.5",
|
|
43
|
+
"@platformatic/db": "4.0.0-new-config.5",
|
|
44
|
+
"@platformatic/gateway": "4.0.0-new-config.5",
|
|
45
|
+
"@platformatic/node": "4.0.0-new-config.5",
|
|
46
|
+
"@platformatic/service": "4.0.0-new-config.5",
|
|
47
|
+
"@platformatic/sql-graphql": "4.0.0-new-config.5",
|
|
48
|
+
"@platformatic/sql-mapper": "4.0.0-new-config.5",
|
|
49
|
+
"@platformatic/wattpm-pprof-capture": "4.0.0-new-config.5"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@fastify/accepts": "^5.0.0",
|
|
@@ -75,16 +75,16 @@
|
|
|
75
75
|
"semver": "^7.6.3",
|
|
76
76
|
"sonic-boom": "^4.2.0",
|
|
77
77
|
"systeminformation": "^5.27.11",
|
|
78
|
-
"undici": "^
|
|
79
|
-
"undici-thread-interceptor": "^1.
|
|
78
|
+
"undici": "^8.5.0",
|
|
79
|
+
"undici-thread-interceptor": "^2.1.1",
|
|
80
80
|
"ws": "^8.16.0",
|
|
81
|
-
"@platformatic/basic": "4.0.0-new-config.
|
|
82
|
-
"@platformatic/foundation": "4.0.0-new-config.
|
|
83
|
-
"@platformatic/globals": "4.0.0-new-config.
|
|
84
|
-
"@platformatic/generators": "4.0.0-new-config.
|
|
85
|
-
"@platformatic/itc": "4.0.0-new-config.
|
|
86
|
-
"@platformatic/
|
|
87
|
-
"@platformatic/
|
|
81
|
+
"@platformatic/basic": "4.0.0-new-config.5",
|
|
82
|
+
"@platformatic/foundation": "4.0.0-new-config.5",
|
|
83
|
+
"@platformatic/globals": "4.0.0-new-config.5",
|
|
84
|
+
"@platformatic/generators": "4.0.0-new-config.5",
|
|
85
|
+
"@platformatic/itc": "4.0.0-new-config.5",
|
|
86
|
+
"@platformatic/tracing": "4.0.0-new-config.5",
|
|
87
|
+
"@platformatic/metrics": "4.0.0-new-config.5"
|
|
88
88
|
},
|
|
89
89
|
"engines": {
|
|
90
90
|
"node": ">=22.19.0"
|