@platformatic/runtime 3.64.0 → 3.66.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/index.d.ts +49 -0
- package/index.js +2 -0
- package/lib/errors.js +1 -1
- package/lib/management-api.js +97 -28
- package/lib/runtime.js +119 -38
- package/lib/worker/controller.js +2 -2
- package/lib/worker/itc.js +8 -3
- package/lib/worker/main.js +16 -1
- package/package.json +18 -17
- package/schema.json +1 -1
package/index.d.ts
CHANGED
|
@@ -200,6 +200,40 @@ export interface RuntimeMetadata {
|
|
|
200
200
|
platformaticVersion: string
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
export interface WorkerLifecycleEvent {
|
|
204
|
+
application: string
|
|
205
|
+
worker: number
|
|
206
|
+
workersCount?: number
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface ProfileCapturedEvent extends WorkerLifecycleEvent {
|
|
210
|
+
id: string
|
|
211
|
+
type: 'cpu' | 'heap'
|
|
212
|
+
timestamp: number
|
|
213
|
+
sampleCount: number | null
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface RuntimeHealthSignal {
|
|
217
|
+
type: string
|
|
218
|
+
max?: number
|
|
219
|
+
mean?: number
|
|
220
|
+
p99?: number
|
|
221
|
+
timestamp?: number
|
|
222
|
+
[key: string]: unknown
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface WorkerCurrentHealth {
|
|
226
|
+
elu: number
|
|
227
|
+
heapUsed?: number | null
|
|
228
|
+
heapTotal?: number | null
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface HealthMetricsEvent extends WorkerLifecycleEvent {
|
|
232
|
+
id: string
|
|
233
|
+
currentHealth: WorkerCurrentHealth | null
|
|
234
|
+
healthSignals: RuntimeHealthSignal[]
|
|
235
|
+
}
|
|
236
|
+
|
|
203
237
|
export declare class ManagementClient {
|
|
204
238
|
constructor (allowedOperations?: string[])
|
|
205
239
|
|
|
@@ -400,6 +434,21 @@ export declare class Runtime extends EventEmitter {
|
|
|
400
434
|
// create()/start().
|
|
401
435
|
setApplicationConfigPatch (applicationId: string, patch: Array<Record<string, unknown>>): void
|
|
402
436
|
removeApplicationConfigPatch (applicationId: string): void
|
|
437
|
+
|
|
438
|
+
// Typed worker lifecycle events. Catch-all overloads preserve EventEmitter's
|
|
439
|
+
// open event surface so listeners for other runtime events still type-check.
|
|
440
|
+
on (event: 'application:worker:started', listener: (event: WorkerLifecycleEvent) => void): this
|
|
441
|
+
on (event: 'application:worker:exited', listener: (event: WorkerLifecycleEvent) => void): this
|
|
442
|
+
on (event: 'application:worker:profile:captured', listener: (event: ProfileCapturedEvent) => void): this
|
|
443
|
+
on (event: 'application:worker:health:metrics', listener: (event: HealthMetricsEvent) => void): this
|
|
444
|
+
on (event: string | symbol, listener: (...args: any[]) => void): this
|
|
445
|
+
|
|
446
|
+
off (event: 'application:worker:started', listener: (event: WorkerLifecycleEvent) => void): this
|
|
447
|
+
off (event: 'application:worker:exited', listener: (event: WorkerLifecycleEvent) => void): this
|
|
448
|
+
off (event: 'application:worker:profile:captured', listener: (event: ProfileCapturedEvent) => void): this
|
|
449
|
+
off (event: 'application:worker:health:metrics', listener: (event: HealthMetricsEvent) => void): this
|
|
450
|
+
off (event: string | symbol, listener: (...args: any[]) => void): this
|
|
451
|
+
|
|
403
452
|
getSchedulerJobs (): SchedulerJob[]
|
|
404
453
|
pauseSchedulerJob (name: string): Promise<SchedulerJob>
|
|
405
454
|
resumeSchedulerJob (name: string): Promise<SchedulerJob>
|
package/index.js
CHANGED
package/lib/errors.js
CHANGED
|
@@ -178,7 +178,7 @@ export const FailedToStopExtensionError = createError(
|
|
|
178
178
|
|
|
179
179
|
export const InvalidExtensionError = createError(
|
|
180
180
|
`${ERROR_PREFIX}_INVALID_EXTENSION`,
|
|
181
|
-
'The extension "%s" must export a setup function as its default export'
|
|
181
|
+
'The extension "%s" must export a setup function as its default export or as a named "setup" export'
|
|
182
182
|
)
|
|
183
183
|
|
|
184
184
|
export const ReservedITCHandlerNameError = createError(
|
package/lib/management-api.js
CHANGED
|
@@ -18,6 +18,53 @@ import { prepareApplication } from './config.js'
|
|
|
18
18
|
|
|
19
19
|
const PLATFORMATIC_TMP_DIR = join(tmpdir(), 'platformatic', 'runtimes')
|
|
20
20
|
|
|
21
|
+
const loggerLevels = ['fatal', 'error', 'warn', 'info', 'debug', 'trace']
|
|
22
|
+
|
|
23
|
+
/*
|
|
24
|
+
Endpoints which are frequently polled by clients (the CLI, the dashboard or any other
|
|
25
|
+
monitoring tool) do not log the request/response pair to avoid flooding the runtime logs.
|
|
26
|
+
*/
|
|
27
|
+
const quiet = { logLevel: 'warn' }
|
|
28
|
+
|
|
29
|
+
/*
|
|
30
|
+
The management API is closed after the runtime has been closed (see the "closed" event handler
|
|
31
|
+
in startManagementApi), so that the POST /stop endpoint can reply. By then the runtime logger
|
|
32
|
+
has been destroyed and writing to its destination throws.
|
|
33
|
+
|
|
34
|
+
To avoid that, the logger is never captured: it is resolved - and its children are recreated -
|
|
35
|
+
on each call, so that logging becomes a no-op as soon as the runtime replaces its logger with
|
|
36
|
+
the abstract one.
|
|
37
|
+
*/
|
|
38
|
+
function createRuntimeLoggerProxy (getParent, bindings, options) {
|
|
39
|
+
let parent
|
|
40
|
+
let logger
|
|
41
|
+
|
|
42
|
+
function resolve () {
|
|
43
|
+
const current = getParent()
|
|
44
|
+
|
|
45
|
+
if (current !== parent) {
|
|
46
|
+
parent = current
|
|
47
|
+
logger = bindings ? parent.child(bindings, options) : parent
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return logger
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const proxy = {
|
|
54
|
+
child (childBindings, childOptions) {
|
|
55
|
+
return createRuntimeLoggerProxy(resolve, childBindings, childOptions)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const level of loggerLevels) {
|
|
60
|
+
proxy[level] = function (...args) {
|
|
61
|
+
return resolve()[level](...args)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return proxy
|
|
66
|
+
}
|
|
67
|
+
|
|
21
68
|
export async function managementApiPlugin (app, opts) {
|
|
22
69
|
app.register(fastifyAccepts)
|
|
23
70
|
|
|
@@ -43,16 +90,16 @@ export async function managementApiPlugin (app, opts) {
|
|
|
43
90
|
return removed
|
|
44
91
|
}
|
|
45
92
|
|
|
46
|
-
app.get('/status', async () => {
|
|
93
|
+
app.get('/status', quiet, async () => {
|
|
47
94
|
const status = runtime.getRuntimeStatus()
|
|
48
95
|
return { status }
|
|
49
96
|
})
|
|
50
97
|
|
|
51
|
-
app.get('/metadata', async () => {
|
|
98
|
+
app.get('/metadata', quiet, async () => {
|
|
52
99
|
return runtime.getRuntimeMetadata()
|
|
53
100
|
})
|
|
54
101
|
|
|
55
|
-
app.get('/config', async request => {
|
|
102
|
+
app.get('/config', quiet, async request => {
|
|
56
103
|
const metadata = request.query.metadata === 'true'
|
|
57
104
|
const rawConfig = await runtime.getRuntimeConfig(metadata)
|
|
58
105
|
|
|
@@ -64,7 +111,7 @@ export async function managementApiPlugin (app, opts) {
|
|
|
64
111
|
return rawConfig
|
|
65
112
|
})
|
|
66
113
|
|
|
67
|
-
app.get('/env', async () => {
|
|
114
|
+
app.get('/env', quiet, async () => {
|
|
68
115
|
return { ...process.env, ...runtime.getRuntimeEnv() }
|
|
69
116
|
})
|
|
70
117
|
|
|
@@ -96,7 +143,7 @@ export async function managementApiPlugin (app, opts) {
|
|
|
96
143
|
return runtime.runSchedulerJob(request.params.name)
|
|
97
144
|
})
|
|
98
145
|
|
|
99
|
-
app.get('/applications', async () => {
|
|
146
|
+
app.get('/applications', quiet, async () => {
|
|
100
147
|
return runtime.getApplications()
|
|
101
148
|
})
|
|
102
149
|
|
|
@@ -144,9 +191,9 @@ export async function managementApiPlugin (app, opts) {
|
|
|
144
191
|
return deleteApplications(request.body, reply)
|
|
145
192
|
})
|
|
146
193
|
|
|
147
|
-
app.get('/applications/:id', async request => {
|
|
194
|
+
app.get('/applications/:id', quiet, async request => {
|
|
148
195
|
const { id } = request.params
|
|
149
|
-
app.log.debug('get application details'
|
|
196
|
+
app.log.debug({ id }, 'get application details')
|
|
150
197
|
return runtime.getApplicationDetails(id)
|
|
151
198
|
})
|
|
152
199
|
|
|
@@ -154,45 +201,45 @@ export async function managementApiPlugin (app, opts) {
|
|
|
154
201
|
return deleteApplications([request.params.id], reply)
|
|
155
202
|
})
|
|
156
203
|
|
|
157
|
-
app.get('/applications/:id/config', async request => {
|
|
204
|
+
app.get('/applications/:id/config', quiet, async request => {
|
|
158
205
|
const { id } = request.params
|
|
159
|
-
app.log.debug('get application config'
|
|
206
|
+
app.log.debug({ id }, 'get application config')
|
|
160
207
|
return runtime.getApplicationConfig(id)
|
|
161
208
|
})
|
|
162
209
|
|
|
163
|
-
app.get('/applications/:id/env', async request => {
|
|
210
|
+
app.get('/applications/:id/env', quiet, async request => {
|
|
164
211
|
const { id } = request.params
|
|
165
|
-
app.log.debug('get application
|
|
212
|
+
app.log.debug({ id }, 'get application env')
|
|
166
213
|
return runtime.getApplicationEnv(id)
|
|
167
214
|
})
|
|
168
215
|
|
|
169
|
-
app.get('/applications/:id/openapi-schema', async request => {
|
|
216
|
+
app.get('/applications/:id/openapi-schema', quiet, async request => {
|
|
170
217
|
const { id } = request.params
|
|
171
|
-
app.log.debug('get openapi-schema'
|
|
218
|
+
app.log.debug({ id }, 'get openapi-schema')
|
|
172
219
|
return runtime.getApplicationOpenapiSchema(id)
|
|
173
220
|
})
|
|
174
221
|
|
|
175
|
-
app.get('/applications/:id/graphql-schema', async request => {
|
|
222
|
+
app.get('/applications/:id/graphql-schema', quiet, async request => {
|
|
176
223
|
const { id } = request.params
|
|
177
|
-
app.log.debug('get graphql-schema'
|
|
224
|
+
app.log.debug({ id }, 'get graphql-schema')
|
|
178
225
|
return runtime.getApplicationGraphqlSchema(id)
|
|
179
226
|
})
|
|
180
227
|
|
|
181
228
|
app.post('/applications/:id/start', async request => {
|
|
182
229
|
const { id } = request.params
|
|
183
|
-
app.log.debug('start application'
|
|
230
|
+
app.log.debug({ id }, 'start application')
|
|
184
231
|
await runtime.startApplication(id)
|
|
185
232
|
})
|
|
186
233
|
|
|
187
234
|
app.post('/applications/:id/stop', async request => {
|
|
188
235
|
const { id } = request.params
|
|
189
|
-
app.log.debug('stop application'
|
|
236
|
+
app.log.debug({ id }, 'stop application')
|
|
190
237
|
await runtime.stopApplication(id)
|
|
191
238
|
})
|
|
192
239
|
|
|
193
|
-
app.all('/applications/:id/proxy/*', async (request, reply) => {
|
|
240
|
+
app.all('/applications/:id/proxy/*', quiet, async (request, reply) => {
|
|
194
241
|
const { id, '*': requestUrl } = request.params
|
|
195
|
-
app.log.debug(
|
|
242
|
+
app.log.debug({ id, requestUrl }, 'proxy request')
|
|
196
243
|
|
|
197
244
|
delete request.headers.connection
|
|
198
245
|
delete request.headers['content-length']
|
|
@@ -217,7 +264,7 @@ export async function managementApiPlugin (app, opts) {
|
|
|
217
264
|
|
|
218
265
|
app.post('/applications/:id/pprof/start', async (request, reply) => {
|
|
219
266
|
const { id } = request.params
|
|
220
|
-
app.log.debug('start profiling'
|
|
267
|
+
app.log.debug({ id }, 'start profiling')
|
|
221
268
|
|
|
222
269
|
const options = request.body || {}
|
|
223
270
|
const result = await runtime.startApplicationProfiling(id, options)
|
|
@@ -226,7 +273,7 @@ export async function managementApiPlugin (app, opts) {
|
|
|
226
273
|
|
|
227
274
|
app.post('/applications/:id/pprof/stop', async (request, reply) => {
|
|
228
275
|
const { id } = request.params
|
|
229
|
-
app.log.debug('stop profiling'
|
|
276
|
+
app.log.debug({ id }, 'stop profiling')
|
|
230
277
|
|
|
231
278
|
const options = request.body || {}
|
|
232
279
|
const profileData = await runtime.stopApplicationProfiling(id, options)
|
|
@@ -248,14 +295,14 @@ export async function managementApiPlugin (app, opts) {
|
|
|
248
295
|
|
|
249
296
|
app.post('/applications/:id/heap-snapshot', async (request, reply) => {
|
|
250
297
|
const { id } = request.params
|
|
251
|
-
app.log.debug('take heap snapshot'
|
|
298
|
+
app.log.debug({ id }, 'take heap snapshot')
|
|
252
299
|
|
|
253
300
|
const stream = await runtime.takeApplicationHeapSnapshot(id)
|
|
254
301
|
reply.type('application/octet-stream').send(stream)
|
|
255
302
|
return reply
|
|
256
303
|
})
|
|
257
304
|
|
|
258
|
-
app.get('/metrics',
|
|
305
|
+
app.get('/metrics', quiet, async (req, reply) => {
|
|
259
306
|
const config = await runtime.getRuntimeConfig()
|
|
260
307
|
|
|
261
308
|
if (config.metrics?.enabled === false) {
|
|
@@ -280,7 +327,7 @@ export async function managementApiPlugin (app, opts) {
|
|
|
280
327
|
})
|
|
281
328
|
|
|
282
329
|
// TODO: Remove in next major version - deprecated endpoint
|
|
283
|
-
app.get('/metrics/live', { websocket: true }, async socket => {
|
|
330
|
+
app.get('/metrics/live', { ...quiet, websocket: true }, async socket => {
|
|
284
331
|
const config = await runtime.getRuntimeConfig()
|
|
285
332
|
|
|
286
333
|
if (config.metrics?.enabled === false) {
|
|
@@ -326,7 +373,7 @@ export async function managementApiPlugin (app, opts) {
|
|
|
326
373
|
socket.on('close', cleanup)
|
|
327
374
|
})
|
|
328
375
|
|
|
329
|
-
app.get('/logs/live', { websocket: true }, async socket => {
|
|
376
|
+
app.get('/logs/live', { ...quiet, websocket: true }, async socket => {
|
|
330
377
|
runtime.addLoggerDestination(createWebSocketStream(socket))
|
|
331
378
|
})
|
|
332
379
|
|
|
@@ -394,7 +441,10 @@ export async function startManagementApi (runtime, config) {
|
|
|
394
441
|
socketPath = join(runtimePIDDir, 'socket')
|
|
395
442
|
}
|
|
396
443
|
|
|
397
|
-
const managementApi = fastify(
|
|
444
|
+
const managementApi = fastify({
|
|
445
|
+
loggerInstance: createRuntimeLoggerProxy(() => runtime.logger, { name: 'management-api' })
|
|
446
|
+
})
|
|
447
|
+
|
|
398
448
|
managementApi.register(fastifyWebsocket)
|
|
399
449
|
managementApi.register(managementApiPlugin, { runtime, prefix: '/api/v1' })
|
|
400
450
|
|
|
@@ -416,10 +466,29 @@ export async function startManagementApi (runtime, config) {
|
|
|
416
466
|
*/
|
|
417
467
|
for (let i = 0; i < 5; i++) {
|
|
418
468
|
try {
|
|
419
|
-
|
|
469
|
+
// Fastify always logs listen() at info. The management API binds a private unix
|
|
470
|
+
// socket/pipe, so that message is noise and also breaks helpers that treat the first
|
|
471
|
+
// "listening at" log as the application URL. Downgrade it to debug for this call.
|
|
472
|
+
const { log } = managementApi
|
|
473
|
+
const originalInfo = log.info
|
|
474
|
+
log.info = function (...args) {
|
|
475
|
+
const text = typeof args[0] === 'string' ? args[0] : args[1]
|
|
476
|
+
if (typeof text === 'string' && text.startsWith('Server listening at')) {
|
|
477
|
+
return log.debug(...args)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return originalInfo.apply(this, args)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
await managementApi.listen({ path: socketPath })
|
|
485
|
+
} finally {
|
|
486
|
+
log.info = originalInfo
|
|
487
|
+
}
|
|
488
|
+
|
|
420
489
|
break
|
|
421
490
|
} catch (e) {
|
|
422
|
-
if (i ===
|
|
491
|
+
if (i === 4) {
|
|
423
492
|
throw e
|
|
424
493
|
}
|
|
425
494
|
|
package/lib/runtime.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
executeInParallel,
|
|
6
6
|
executeWithTimeout,
|
|
7
7
|
features,
|
|
8
|
+
kEnvFileFallbackKeys,
|
|
8
9
|
kMetadata,
|
|
9
10
|
kTimeout,
|
|
10
11
|
parseMemorySize
|
|
@@ -146,6 +147,24 @@ function formatMetricValue (value) {
|
|
|
146
147
|
return `${value}`
|
|
147
148
|
}
|
|
148
149
|
|
|
150
|
+
// Resolves the setup function of a runtime extension out of its module namespace.
|
|
151
|
+
//
|
|
152
|
+
// The canonical form is a default export, but transpilers and bundlers emit
|
|
153
|
+
// faux ESM modules, in which the real exports are properties of `module.exports`
|
|
154
|
+
// and therefore only reachable via an additional `default` hop. The setup
|
|
155
|
+
// function can also be exported as a named `setup` export, at both levels.
|
|
156
|
+
function resolveExtensionSetup (imported) {
|
|
157
|
+
const candidates = [imported?.default, imported?.setup, imported?.default?.default, imported?.default?.setup]
|
|
158
|
+
|
|
159
|
+
for (const candidate of candidates) {
|
|
160
|
+
if (typeof candidate === 'function') {
|
|
161
|
+
return candidate
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
|
|
149
168
|
// Always run operations in parallel to avoid deadlocks when services have dependencies
|
|
150
169
|
const DEFAULT_CONCURRENCY = availableParallelism() * 2
|
|
151
170
|
const MAX_BOOTSTRAP_ATTEMPTS = 5
|
|
@@ -301,10 +320,6 @@ export class Runtime extends EventEmitter {
|
|
|
301
320
|
|
|
302
321
|
const config = this.#config
|
|
303
322
|
|
|
304
|
-
if (config.managementApi) {
|
|
305
|
-
this.#managementApi = await startManagementApi(this, config.managementApi)
|
|
306
|
-
}
|
|
307
|
-
|
|
308
323
|
if (config.metrics) {
|
|
309
324
|
// Use the configured application label name for metrics (defaults to 'applicationId')
|
|
310
325
|
this.#metricsLabelName = config.metrics.applicationLabel || 'applicationId'
|
|
@@ -321,12 +336,17 @@ export class Runtime extends EventEmitter {
|
|
|
321
336
|
collectProcessMetrics(this.#processMetricsRegistry)
|
|
322
337
|
}
|
|
323
338
|
|
|
324
|
-
// Create the logger before extensions and health/metrics servers
|
|
339
|
+
// Create the logger before the management API, the extensions and the health/metrics servers
|
|
340
|
+
// so that all of them can use it.
|
|
325
341
|
const [logger, destination, context] = await createLogger(config)
|
|
326
342
|
this.logger = logger
|
|
327
343
|
this.#loggerDestination = destination
|
|
328
344
|
this.#loggerContext = context
|
|
329
345
|
|
|
346
|
+
if (config.managementApi) {
|
|
347
|
+
this.#managementApi = await startManagementApi(this, config.managementApi)
|
|
348
|
+
}
|
|
349
|
+
|
|
330
350
|
await this.#startOpenTelemetryMetricsForwarder(config.metrics?.opentelemetry)
|
|
331
351
|
|
|
332
352
|
this.#createWorkersBroadcastChannel()
|
|
@@ -990,45 +1010,45 @@ export class Runtime extends EventEmitter {
|
|
|
990
1010
|
|
|
991
1011
|
try {
|
|
992
1012
|
// Bound the whole retrieval with a single timeout budget: resolving the
|
|
993
|
-
//
|
|
994
|
-
//
|
|
1013
|
+
// workers round-trips to them when ensureStarted is set, and the profile
|
|
1014
|
+
// pulls do too — both hang if a worker event loop is blocked. Attach
|
|
995
1015
|
// a noop handler so that a late settlement after the timeout does not
|
|
996
1016
|
// surface as an unhandled rejection.
|
|
997
|
-
const pull = (
|
|
998
|
-
const service = await this.#getApplicationById(id, ensureStarted)
|
|
999
|
-
const result = await sendViaITC(service, 'getLastProfile', {
|
|
1000
|
-
...options,
|
|
1001
|
-
includeTimestamp: true,
|
|
1002
|
-
includeSampleCount: true
|
|
1003
|
-
})
|
|
1004
|
-
return { service, result }
|
|
1005
|
-
})()
|
|
1017
|
+
const pull = this.#pullLastProfiles(id, options, ensureStarted)
|
|
1006
1018
|
pull.catch(() => {})
|
|
1007
1019
|
|
|
1008
1020
|
const outcome = await executeWithTimeout(pull, timeout, kTimeout)
|
|
1009
1021
|
|
|
1010
1022
|
if (outcome !== kTimeout) {
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1023
|
+
let best = null
|
|
1024
|
+
|
|
1025
|
+
for (const { service, result } of outcome) {
|
|
1026
|
+
// An older capture module which does not support includeTimestamp
|
|
1027
|
+
// returns the raw profile.
|
|
1028
|
+
const value = result instanceof Uint8Array
|
|
1029
|
+
? { profile: result, timestamp: null, sampleCount: null }
|
|
1030
|
+
: { sampleCount: null, ...result }
|
|
1031
|
+
|
|
1032
|
+
// A strictly newer live window supersedes the preserved overload
|
|
1033
|
+
// profile: prune it so the preserved copy naturally expires once the
|
|
1034
|
+
// worker is healthy again and its profiles are being consumed.
|
|
1035
|
+
if (value.timestamp != null) {
|
|
1036
|
+
const key = `${service[kApplicationId]}:${service[kWorkerId]}:${type}`
|
|
1037
|
+
const preserved = this.#lastOverloadProfiles.get(key)
|
|
1038
|
+
|
|
1039
|
+
if (preserved && preserved.timestamp < value.timestamp) {
|
|
1040
|
+
this.#lastOverloadProfiles.delete(key)
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// For an application-level id the newest window across the workers
|
|
1045
|
+
// wins, mirroring the preserved overload profile fallback below.
|
|
1046
|
+
if (!best || (value.timestamp != null && (best.timestamp == null || value.timestamp > best.timestamp))) {
|
|
1047
|
+
best = value
|
|
1028
1048
|
}
|
|
1029
1049
|
}
|
|
1030
1050
|
|
|
1031
|
-
return { ...
|
|
1051
|
+
return { ...best, preserved: false }
|
|
1032
1052
|
}
|
|
1033
1053
|
|
|
1034
1054
|
// The worker event loop is not responding (e.g. it is hard-blocked).
|
|
@@ -1057,6 +1077,62 @@ export class Runtime extends EventEmitter {
|
|
|
1057
1077
|
throw error
|
|
1058
1078
|
}
|
|
1059
1079
|
|
|
1080
|
+
// Pulls the last profile from the addressed worker, or from every worker of
|
|
1081
|
+
// the application when no explicit worker index is given: the application
|
|
1082
|
+
// "last profile" is the newest window among its workers, so one arbitrary
|
|
1083
|
+
// worker cannot answer for all of them. Per-worker failures with fallback
|
|
1084
|
+
// codes are ignored as long as at least one worker yields a profile.
|
|
1085
|
+
async #pullLastProfiles (id, options, ensureStarted) {
|
|
1086
|
+
const pullOptions = { ...options, includeTimestamp: true, includeSampleCount: true }
|
|
1087
|
+
|
|
1088
|
+
const pullWorker = async service => {
|
|
1089
|
+
const result = await sendViaITC(service, 'getLastProfile', pullOptions)
|
|
1090
|
+
return { service, result }
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
if (/^.+:\d+$/.test(id)) {
|
|
1094
|
+
return [await pullWorker(await this.#getApplicationById(id, ensureStarted))]
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
if (!this.#applications.has(id)) {
|
|
1098
|
+
throw new ApplicationNotFoundError(id, this.getApplicationsIds().join(', '))
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
const keys = this.#workers.getKeys(id)
|
|
1102
|
+
|
|
1103
|
+
// No worker is currently registered: resolve the id as usual so that the
|
|
1104
|
+
// canonical error is raised.
|
|
1105
|
+
if (keys.length === 0) {
|
|
1106
|
+
return [await pullWorker(await this.#getApplicationById(id, ensureStarted))]
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
const settled = await Promise.allSettled(
|
|
1110
|
+
keys.map(async key => {
|
|
1111
|
+
const service = await this.#getWorkerByIdOrNext(id, key.split(':')[1], ensureStarted)
|
|
1112
|
+
return pullWorker(service)
|
|
1113
|
+
})
|
|
1114
|
+
)
|
|
1115
|
+
|
|
1116
|
+
const profiles = []
|
|
1117
|
+
let firstError
|
|
1118
|
+
|
|
1119
|
+
for (const outcome of settled) {
|
|
1120
|
+
if (outcome.status === 'fulfilled') {
|
|
1121
|
+
profiles.push(outcome.value)
|
|
1122
|
+
} else if (!kLastProfileFallbackCodes.has(outcome.reason?.code)) {
|
|
1123
|
+
throw outcome.reason
|
|
1124
|
+
} else {
|
|
1125
|
+
firstError ??= outcome.reason
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
if (profiles.length === 0) {
|
|
1130
|
+
throw firstError
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
return profiles
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1060
1136
|
// The final profile of an overload pause is pushed by the worker and
|
|
1061
1137
|
// preserved in the main thread (see the profile:overload listener), so that
|
|
1062
1138
|
// the evidence of what saturated a worker survives the worker being blocked
|
|
@@ -2433,7 +2509,10 @@ export class Runtime extends EventEmitter {
|
|
|
2433
2509
|
codeRangeSizeMb
|
|
2434
2510
|
},
|
|
2435
2511
|
inspectorOptions,
|
|
2436
|
-
dirname: this.#root
|
|
2512
|
+
dirname: this.#root,
|
|
2513
|
+
// Keys of the worker environment which only come from an env file of the runtime:
|
|
2514
|
+
// the env file of the application is allowed to override those.
|
|
2515
|
+
envFileFallbackKeys: this.#env[kEnvFileFallbackKeys] ?? []
|
|
2437
2516
|
},
|
|
2438
2517
|
argv: applicationConfig.arguments,
|
|
2439
2518
|
execArgv,
|
|
@@ -2746,7 +2825,7 @@ export class Runtime extends EventEmitter {
|
|
|
2746
2825
|
if (attempt === MAX_BOOTSTRAP_ATTEMPTS) {
|
|
2747
2826
|
const error = new RuntimeAbortedError({ cause: e })
|
|
2748
2827
|
error.message = `Unable to initialize the ${errorLabel}.`
|
|
2749
|
-
throw
|
|
2828
|
+
throw error
|
|
2750
2829
|
}
|
|
2751
2830
|
|
|
2752
2831
|
if (e.code !== 'PLT_RUNTIME_APPLICATION_WORKER_EXIT') {
|
|
@@ -4276,13 +4355,15 @@ export class Runtime extends EventEmitter {
|
|
|
4276
4355
|
for (const extension of extensions) {
|
|
4277
4356
|
const { path, options } = typeof extension === 'string' ? { path: extension } : extension
|
|
4278
4357
|
|
|
4279
|
-
let
|
|
4358
|
+
let imported
|
|
4280
4359
|
try {
|
|
4281
|
-
|
|
4360
|
+
imported = await import(pathToFileURL(path))
|
|
4282
4361
|
} catch (e) {
|
|
4283
4362
|
throw new FailedToLoadExtensionError(path, e.message, { cause: e })
|
|
4284
4363
|
}
|
|
4285
4364
|
|
|
4365
|
+
const setup = resolveExtensionSetup(imported)
|
|
4366
|
+
|
|
4286
4367
|
if (typeof setup !== 'function') {
|
|
4287
4368
|
throw new InvalidExtensionError(path)
|
|
4288
4369
|
}
|
package/lib/worker/controller.js
CHANGED
|
@@ -24,7 +24,7 @@ import { resolve } from 'node:path'
|
|
|
24
24
|
import { getActiveResourcesInfo } from 'node:process'
|
|
25
25
|
import { workerData } from 'node:worker_threads'
|
|
26
26
|
import { getGlobalDispatcher, setGlobalDispatcher } from 'undici'
|
|
27
|
-
import { ApplicationAlreadyStartedError, RuntimeNotStartedError } from '../errors.js'
|
|
27
|
+
import { ApplicationAlreadyStartedError, exitCodes, RuntimeNotStartedError } from '../errors.js'
|
|
28
28
|
import { getApplicationUrl } from '../utils.js'
|
|
29
29
|
import { markAsPlatformaticDispatcher, refreshGlobalDispatcher } from './interceptors.js'
|
|
30
30
|
|
|
@@ -43,7 +43,7 @@ function handleUnhandled (app, event, listeners, timeout, err, ...args) {
|
|
|
43
43
|
logger.error({ err: ensureLoggableError(err) }, `The ${label} threw an ${event} event.`)
|
|
44
44
|
|
|
45
45
|
// Give some time to the listeners, logger and ITC notifications to land before shutting down
|
|
46
|
-
setTimeout(() => process.exit(
|
|
46
|
+
setTimeout(() => process.exit(exitCodes.PROCESS_UNHANDLED_ERROR), timeout)
|
|
47
47
|
|
|
48
48
|
for (const listener of listeners) {
|
|
49
49
|
try {
|
package/lib/worker/itc.js
CHANGED
|
@@ -167,6 +167,10 @@ export async function setupITC (controller, application, dispatcher, sharedConte
|
|
|
167
167
|
}
|
|
168
168
|
})
|
|
169
169
|
|
|
170
|
+
// ITC handlers run concurrently, so later start/stop requests must wait for
|
|
171
|
+
// the actual in-flight start operation rather than a controller event.
|
|
172
|
+
let controllerStartPromise
|
|
173
|
+
|
|
170
174
|
const itc = new ITC({
|
|
171
175
|
name: controller.applicationConfig.id + '-worker',
|
|
172
176
|
port: parentPort,
|
|
@@ -175,14 +179,15 @@ export async function setupITC (controller, application, dispatcher, sharedConte
|
|
|
175
179
|
const status = controller.getStatus()
|
|
176
180
|
|
|
177
181
|
if (status === 'starting') {
|
|
178
|
-
await
|
|
182
|
+
await controllerStartPromise
|
|
179
183
|
} else {
|
|
180
184
|
// This gives a chance to a capability to perform custom logic
|
|
181
185
|
const events = getEvents()
|
|
182
186
|
events.emit('start')
|
|
183
187
|
|
|
184
188
|
try {
|
|
185
|
-
|
|
189
|
+
controllerStartPromise = controller.start()
|
|
190
|
+
await controllerStartPromise
|
|
186
191
|
} catch (e) {
|
|
187
192
|
await controller.stop(true)
|
|
188
193
|
|
|
@@ -222,7 +227,7 @@ export async function setupITC (controller, application, dispatcher, sharedConte
|
|
|
222
227
|
const status = controller.getStatus()
|
|
223
228
|
|
|
224
229
|
if (!force && status === 'starting') {
|
|
225
|
-
await
|
|
230
|
+
await controllerStartPromise
|
|
226
231
|
}
|
|
227
232
|
|
|
228
233
|
if (force || status.startsWith('start')) {
|
package/lib/worker/main.js
CHANGED
|
@@ -11,12 +11,14 @@ import { addPinoInstrumentation } from '@platformatic/telemetry'
|
|
|
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'
|
|
14
15
|
import { ServerResponse } from 'node:http'
|
|
15
16
|
import inspector from 'node:inspector'
|
|
16
17
|
import { hostname } from 'node:os'
|
|
17
18
|
import { join, resolve } from 'node:path'
|
|
18
19
|
import { setDefaultHighWaterMark } from 'node:stream'
|
|
19
20
|
import { pathToFileURL } from 'node:url'
|
|
21
|
+
import { parseEnv } from 'node:util'
|
|
20
22
|
import { threadId, workerData } from 'node:worker_threads'
|
|
21
23
|
import pino from 'pino'
|
|
22
24
|
import { install as installUndiciGlobals } from 'undici'
|
|
@@ -240,8 +242,21 @@ async function main () {
|
|
|
240
242
|
const logger = getLogger()
|
|
241
243
|
logger.debug({ envfile }, 'Loading envfile...')
|
|
242
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
|
+
|
|
243
252
|
try {
|
|
244
|
-
|
|
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
|
+
}
|
|
245
260
|
} catch {
|
|
246
261
|
// Ignore if the file doesn't exist, similar to dotenv behavior
|
|
247
262
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/runtime",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.66.0",
|
|
4
4
|
"description": "Platformatic runtime",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -39,14 +39,14 @@
|
|
|
39
39
|
"typescript": "^5.9.3",
|
|
40
40
|
"undici-oidc-interceptor": "^0.9.0",
|
|
41
41
|
"why-is-node-running": "^2.2.2",
|
|
42
|
-
"@platformatic/
|
|
43
|
-
"@platformatic/
|
|
44
|
-
"@platformatic/
|
|
45
|
-
"@platformatic/
|
|
46
|
-
"@platformatic/
|
|
47
|
-
"@platformatic/sql-
|
|
48
|
-
"@platformatic/wattpm-pprof-capture": "3.
|
|
49
|
-
"@platformatic/
|
|
42
|
+
"@platformatic/composer": "3.66.0",
|
|
43
|
+
"@platformatic/gateway": "3.66.0",
|
|
44
|
+
"@platformatic/service": "3.66.0",
|
|
45
|
+
"@platformatic/sql-graphql": "3.66.0",
|
|
46
|
+
"@platformatic/db": "3.66.0",
|
|
47
|
+
"@platformatic/sql-mapper": "3.66.0",
|
|
48
|
+
"@platformatic/wattpm-pprof-capture": "3.66.0",
|
|
49
|
+
"@platformatic/node": "3.66.0"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@fastify/accepts": "^5.0.0",
|
|
@@ -77,13 +77,13 @@
|
|
|
77
77
|
"undici": "^7.27.2",
|
|
78
78
|
"undici-thread-interceptor": "^1.3.1",
|
|
79
79
|
"ws": "^8.16.0",
|
|
80
|
-
"@platformatic/
|
|
81
|
-
"@platformatic/
|
|
82
|
-
"@platformatic/
|
|
83
|
-
"@platformatic/
|
|
84
|
-
"@platformatic/
|
|
85
|
-
"@platformatic/
|
|
86
|
-
"@platformatic/telemetry": "3.
|
|
80
|
+
"@platformatic/basic": "3.66.0",
|
|
81
|
+
"@platformatic/foundation": "3.66.0",
|
|
82
|
+
"@platformatic/itc": "3.66.0",
|
|
83
|
+
"@platformatic/metrics": "3.66.0",
|
|
84
|
+
"@platformatic/globals": "3.66.0",
|
|
85
|
+
"@platformatic/generators": "3.66.0",
|
|
86
|
+
"@platformatic/telemetry": "3.66.0"
|
|
87
87
|
},
|
|
88
88
|
"engines": {
|
|
89
89
|
"node": ">=22.19.0"
|
|
@@ -99,6 +99,7 @@
|
|
|
99
99
|
"gen-schema": "node lib/schema.js > schema.json",
|
|
100
100
|
"gen-types": "json2ts > config.d.ts < schema.json",
|
|
101
101
|
"build": "npm run gen-schema && npm run gen-types",
|
|
102
|
-
"lint": "eslint"
|
|
102
|
+
"lint": "eslint",
|
|
103
|
+
"lint:fix": "eslint --fix"
|
|
103
104
|
}
|
|
104
105
|
}
|
package/schema.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.
|
|
2
|
+
"$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.66.0.json",
|
|
3
3
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
4
|
"title": "Platformatic Runtime Config",
|
|
5
5
|
"type": "object",
|