@platformatic/runtime 3.55.0 → 3.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config.d.ts +43 -0
- package/index.d.ts +12 -1
- package/index.js +1 -0
- package/lib/config.js +3 -11
- package/lib/errors.js +4 -0
- package/lib/generator.js +39 -25
- package/lib/loopback-messaging.js +78 -0
- package/lib/management-api.js +3 -3
- package/lib/prom-server.js +30 -1
- package/lib/runtime.js +32 -8
- package/lib/utils.js +12 -3
- package/lib/worker/controller.js +44 -12
- package/lib/worker/health-signals.js +4 -2
- package/lib/worker/http-cache.js +5 -5
- package/lib/worker/interceptors.js +79 -19
- package/lib/worker/itc.js +16 -6
- package/lib/worker/main.js +22 -16
- package/lib/worker/management.js +3 -2
- package/lib/worker/messaging.js +23 -15
- package/lib/worker/shared-context.js +5 -3
- package/lib/worker/symbols.js +1 -2
- package/package.json +18 -17
- package/schema.json +126 -1
package/config.d.ts
CHANGED
|
@@ -333,6 +333,33 @@ export type PlatformaticRuntimeConfig = {
|
|
|
333
333
|
enabled?: boolean | string;
|
|
334
334
|
hostname?: string;
|
|
335
335
|
endpoint?: string;
|
|
336
|
+
https?: {
|
|
337
|
+
allowHTTP1?: boolean;
|
|
338
|
+
key:
|
|
339
|
+
| string
|
|
340
|
+
| {
|
|
341
|
+
path?: string;
|
|
342
|
+
}
|
|
343
|
+
| (
|
|
344
|
+
| string
|
|
345
|
+
| {
|
|
346
|
+
path?: string;
|
|
347
|
+
}
|
|
348
|
+
)[];
|
|
349
|
+
cert:
|
|
350
|
+
| string
|
|
351
|
+
| {
|
|
352
|
+
path?: string;
|
|
353
|
+
}
|
|
354
|
+
| (
|
|
355
|
+
| string
|
|
356
|
+
| {
|
|
357
|
+
path?: string;
|
|
358
|
+
}
|
|
359
|
+
)[];
|
|
360
|
+
requestCert?: boolean;
|
|
361
|
+
rejectUnauthorized?: boolean;
|
|
362
|
+
};
|
|
336
363
|
auth?: {
|
|
337
364
|
username: string;
|
|
338
365
|
password: string;
|
|
@@ -471,6 +498,14 @@ export type PlatformaticRuntimeConfig = {
|
|
|
471
498
|
* The path to write the traces to. Only for file exporter.
|
|
472
499
|
*/
|
|
473
500
|
path?: string;
|
|
501
|
+
/**
|
|
502
|
+
* The OTLP transport protocol to use. Only for the otlp exporter. Defaults to http.
|
|
503
|
+
*/
|
|
504
|
+
protocol?: "http" | "grpc";
|
|
505
|
+
/**
|
|
506
|
+
* Alias for protocol. Only for the otlp exporter. Defaults to http.
|
|
507
|
+
*/
|
|
508
|
+
transport?: "http" | "grpc";
|
|
474
509
|
[k: string]: unknown;
|
|
475
510
|
};
|
|
476
511
|
additionalProperties?: never;
|
|
@@ -496,6 +531,14 @@ export type PlatformaticRuntimeConfig = {
|
|
|
496
531
|
* The path to write the traces to. Only for file exporter.
|
|
497
532
|
*/
|
|
498
533
|
path?: string;
|
|
534
|
+
/**
|
|
535
|
+
* The OTLP transport protocol to use. Only for the otlp exporter. Defaults to http.
|
|
536
|
+
*/
|
|
537
|
+
protocol?: "http" | "grpc";
|
|
538
|
+
/**
|
|
539
|
+
* Alias for protocol. Only for the otlp exporter. Defaults to http.
|
|
540
|
+
*/
|
|
541
|
+
transport?: "http" | "grpc";
|
|
499
542
|
[k: string]: unknown;
|
|
500
543
|
};
|
|
501
544
|
additionalProperties?: never;
|
package/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FastifyError } from '@fastify/error'
|
|
2
2
|
import { Configuration, ConfigurationOptions, logFatalError, parseArgs } from '@platformatic/foundation'
|
|
3
3
|
import { BaseGenerator } from '@platformatic/generators'
|
|
4
|
+
import { PlatformaticGlobal } from '@platformatic/globals'
|
|
4
5
|
import { JSONSchemaType } from 'ajv'
|
|
5
6
|
import * as colorette from 'colorette'
|
|
6
7
|
import { Logger } from 'pino'
|
|
@@ -27,6 +28,12 @@ export interface ApplicationsCommands {
|
|
|
27
28
|
help: Record<string, string | (() => string)>
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
export interface LoopbackMessagingOptions {
|
|
32
|
+
logger?: Logger
|
|
33
|
+
mount?: boolean
|
|
34
|
+
runtimeConfig?: { messagingTimeout?: number; [key: string]: unknown }
|
|
35
|
+
}
|
|
36
|
+
|
|
30
37
|
export namespace errors {
|
|
31
38
|
export const RuntimeExitedError: () => FastifyError
|
|
32
39
|
export const UnknownRuntimeAPICommandError: (command: string) => FastifyError
|
|
@@ -61,7 +68,6 @@ export namespace symbols {
|
|
|
61
68
|
export const kWorkerStatus: unique symbol
|
|
62
69
|
export const kWorkerHealthSignals: unique symbol
|
|
63
70
|
export const kStderrMarker: string
|
|
64
|
-
export const kInterceptors: unique symbol
|
|
65
71
|
export const kWorkersBroadcast: unique symbol
|
|
66
72
|
}
|
|
67
73
|
|
|
@@ -191,3 +197,8 @@ export declare function transform (
|
|
|
191
197
|
): Promise<RuntimeConfiguration>
|
|
192
198
|
|
|
193
199
|
export declare function loadApplicationsCommands (): Promise<ApplicationsCommands>
|
|
200
|
+
|
|
201
|
+
export declare function setupLoopbackMessaging (
|
|
202
|
+
targetId: string,
|
|
203
|
+
options?: LoopbackMessagingOptions
|
|
204
|
+
): PlatformaticGlobal['messaging'] & { unmount: () => void }
|
package/index.js
CHANGED
|
@@ -176,6 +176,7 @@ export async function create (configOrRoot, sourceOrConfig, context) {
|
|
|
176
176
|
export { prepareApplication, transform, wrapInRuntimeConfig } from './lib/config.js'
|
|
177
177
|
export * as errors from './lib/errors.js'
|
|
178
178
|
export { RuntimeGenerator as Generator, WrappedGenerator } from './lib/generator.js'
|
|
179
|
+
export { setupLoopbackMessaging } from './lib/loopback-messaging.js'
|
|
179
180
|
export { Runtime } from './lib/runtime.js'
|
|
180
181
|
export { schema } from './lib/schema.js'
|
|
181
182
|
export * from './lib/version.js'
|
package/lib/config.js
CHANGED
|
@@ -18,8 +18,7 @@ import {
|
|
|
18
18
|
InspectorHostError,
|
|
19
19
|
InspectorPortError,
|
|
20
20
|
InvalidArgumentError,
|
|
21
|
-
InvalidEntrypointError
|
|
22
|
-
MissingEntrypointError
|
|
21
|
+
InvalidEntrypointError
|
|
23
22
|
} from './errors.js'
|
|
24
23
|
import { schema } from './schema.js'
|
|
25
24
|
import { upgrade } from './upgrade.js'
|
|
@@ -438,15 +437,8 @@ export async function transform (config, _, context) {
|
|
|
438
437
|
}
|
|
439
438
|
}
|
|
440
439
|
|
|
441
|
-
if (!hasValidEntrypoint && !context.allowMissingEntrypoint) {
|
|
442
|
-
|
|
443
|
-
throw new InvalidEntrypointError(config.entrypoint)
|
|
444
|
-
} else if (applications.length >= 1) {
|
|
445
|
-
throw new MissingEntrypointError()
|
|
446
|
-
}
|
|
447
|
-
// If there are no applications, and no entrypoint it's an empty app.
|
|
448
|
-
// It won't start, but we should be able to parse and operate on it,
|
|
449
|
-
// like adding other applications.
|
|
440
|
+
if (!hasValidEntrypoint && config.entrypoint && !context.allowMissingEntrypoint) {
|
|
441
|
+
throw new InvalidEntrypointError(config.entrypoint)
|
|
450
442
|
}
|
|
451
443
|
|
|
452
444
|
if (typeof config.metrics === 'boolean') {
|
package/lib/errors.js
CHANGED
|
@@ -49,6 +49,10 @@ export const ApplicationStartTimeoutError = createError(
|
|
|
49
49
|
`${ERROR_PREFIX}_APPLICATION_START_TIMEOUT`,
|
|
50
50
|
"Application with id '%s' failed to start in %dms."
|
|
51
51
|
)
|
|
52
|
+
export const ApplicationDependencyNotFoundError = createError(
|
|
53
|
+
`${ERROR_PREFIX}_APPLICATION_DEPENDENCY_NOT_FOUND`,
|
|
54
|
+
'Application dependency %s not found. Available applications are: %s'
|
|
55
|
+
)
|
|
52
56
|
export const ApplicationsDependenciesCycleError = createError(
|
|
53
57
|
`${ERROR_PREFIX}_APPLICATIONS_DEPENDENCIES_CYCLE`,
|
|
54
58
|
'Detected a cycle in the applications dependencies: %s'
|
package/lib/generator.js
CHANGED
|
@@ -19,17 +19,6 @@ import { transform } from './config.js'
|
|
|
19
19
|
import { schema } from './schema.js'
|
|
20
20
|
import { getArrayDifference } from './utils.js'
|
|
21
21
|
|
|
22
|
-
const wrappableProperties = {
|
|
23
|
-
logger: {
|
|
24
|
-
level: '{PLT_SERVER_LOGGER_LEVEL}'
|
|
25
|
-
},
|
|
26
|
-
server: {
|
|
27
|
-
hostname: '{PLT_SERVER_HOSTNAME}',
|
|
28
|
-
port: '{PORT}'
|
|
29
|
-
},
|
|
30
|
-
managementApi: '{PLT_MANAGEMENT_API}'
|
|
31
|
-
}
|
|
32
|
-
|
|
33
22
|
const engines = {
|
|
34
23
|
node: '>=22.19.0'
|
|
35
24
|
}
|
|
@@ -42,13 +31,36 @@ const NoApplicationNamedError = createError(
|
|
|
42
31
|
)
|
|
43
32
|
const NoEntryPointError = createError(`${ERROR_PREFIX}_NO_ENTRYPOINT`, 'No entrypoint had been defined.')
|
|
44
33
|
|
|
34
|
+
function getRuntimeWrappableProperties (skipServer) {
|
|
35
|
+
const wrappableProperties = {
|
|
36
|
+
logger: {
|
|
37
|
+
level: '{PLT_SERVER_LOGGER_LEVEL}'
|
|
38
|
+
},
|
|
39
|
+
managementApi: '{PLT_MANAGEMENT_API}'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!skipServer) {
|
|
43
|
+
wrappableProperties.server = {
|
|
44
|
+
hostname: '{PLT_SERVER_HOSTNAME}',
|
|
45
|
+
port: '{PORT}'
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return wrappableProperties
|
|
50
|
+
}
|
|
51
|
+
|
|
45
52
|
function getRuntimeBaseEnvVars (config) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
const env = {}
|
|
54
|
+
|
|
55
|
+
if (!config.skipServer) {
|
|
56
|
+
env.PLT_SERVER_HOSTNAME = '127.0.0.1'
|
|
57
|
+
env.PORT = config.port || 3042
|
|
51
58
|
}
|
|
59
|
+
|
|
60
|
+
env.PLT_SERVER_LOGGER_LEVEL = config.logLevel || 'info'
|
|
61
|
+
env.PLT_MANAGEMENT_API = true
|
|
62
|
+
|
|
63
|
+
return env
|
|
52
64
|
}
|
|
53
65
|
|
|
54
66
|
export class RuntimeGenerator extends BaseGenerator {
|
|
@@ -195,7 +207,7 @@ export class RuntimeGenerator extends BaseGenerator {
|
|
|
195
207
|
path: this.config.autoload || this.applicationsFolder,
|
|
196
208
|
exclude: ['docs']
|
|
197
209
|
},
|
|
198
|
-
...
|
|
210
|
+
...getRuntimeWrappableProperties(this.config.skipServer)
|
|
199
211
|
}
|
|
200
212
|
|
|
201
213
|
return config
|
|
@@ -251,13 +263,15 @@ export class RuntimeGenerator extends BaseGenerator {
|
|
|
251
263
|
return
|
|
252
264
|
}
|
|
253
265
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
266
|
+
if (!this.config.skipServer) {
|
|
267
|
+
// port
|
|
268
|
+
this.questions.push({
|
|
269
|
+
type: 'input',
|
|
270
|
+
name: 'port',
|
|
271
|
+
default: 3042,
|
|
272
|
+
message: 'What port do you want to use?'
|
|
273
|
+
})
|
|
274
|
+
}
|
|
261
275
|
}
|
|
262
276
|
|
|
263
277
|
setApplicationsDirectory () {
|
|
@@ -611,7 +625,7 @@ export class WrappedGenerator extends BaseGenerator {
|
|
|
611
625
|
async #createConfigFile () {
|
|
612
626
|
const config = {
|
|
613
627
|
$schema: `https://schemas.platformatic.dev/${this.module}/${this.platformaticVersion}.json`,
|
|
614
|
-
runtime:
|
|
628
|
+
runtime: getRuntimeWrappableProperties(this.config.skipServer)
|
|
615
629
|
}
|
|
616
630
|
|
|
617
631
|
this.addFile({
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { abstractLogger } from '@platformatic/foundation'
|
|
2
|
+
import { getMessaging, removeGlobals, updateGlobals } from '@platformatic/globals'
|
|
3
|
+
import { MessageChannel } from 'node:worker_threads'
|
|
4
|
+
import { MessagingITC } from './worker/messaging.js'
|
|
5
|
+
|
|
6
|
+
class LoopbackMessagingITC extends MessagingITC {
|
|
7
|
+
#application
|
|
8
|
+
#channel
|
|
9
|
+
|
|
10
|
+
constructor (id, application, runtimeConfig, logger, channel) {
|
|
11
|
+
super(id, runtimeConfig, logger)
|
|
12
|
+
|
|
13
|
+
this.#application = application
|
|
14
|
+
this.#channel = channel
|
|
15
|
+
this.addSource(channel)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Force the next worker to be the channel
|
|
19
|
+
_getNextWorker () {
|
|
20
|
+
return { application: this.#application, channel: this.#channel }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class LoopbackMessaging extends LoopbackMessagingITC {
|
|
25
|
+
#target
|
|
26
|
+
#channel
|
|
27
|
+
#previousGlobals
|
|
28
|
+
|
|
29
|
+
constructor (targetId, config, logger) {
|
|
30
|
+
const channel = new MessageChannel()
|
|
31
|
+
|
|
32
|
+
super('loopback-messaging', targetId, config, logger, channel.port1)
|
|
33
|
+
this.#channel = channel
|
|
34
|
+
this.#target = new LoopbackMessagingITC(targetId, 'loopback-messaging', config, logger, channel.port2)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
mount () {
|
|
38
|
+
this.#previousGlobals = getMessaging({ throwOnMissing: false })
|
|
39
|
+
|
|
40
|
+
updateGlobals({
|
|
41
|
+
messaging: {
|
|
42
|
+
handle: this.#target.handle.bind(this.#target),
|
|
43
|
+
send: this.#target.send.bind(this.#target),
|
|
44
|
+
notify: this.#target.notify.bind(this.#target)
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return this
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
unmount () {
|
|
52
|
+
if (this.#previousGlobals) {
|
|
53
|
+
updateGlobals({ messaging: this.#previousGlobals })
|
|
54
|
+
} else {
|
|
55
|
+
removeGlobals(['messaging'])
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.close()
|
|
59
|
+
this.#target.close()
|
|
60
|
+
this.#channel.port1.close()
|
|
61
|
+
this.#channel.port2.close()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function setupLoopbackMessaging (targetId, options) {
|
|
66
|
+
options ??= {}
|
|
67
|
+
|
|
68
|
+
const runtimeConfig = { ...(options.runtimeConfig ?? {}) }
|
|
69
|
+
runtimeConfig.messagingTimeout ??= 5000
|
|
70
|
+
|
|
71
|
+
const messaging = new LoopbackMessaging(targetId, runtimeConfig, options.logger ?? abstractLogger)
|
|
72
|
+
|
|
73
|
+
if (options.mount !== false) {
|
|
74
|
+
messaging.mount()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return messaging
|
|
78
|
+
}
|
package/lib/management-api.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from '@platformatic/foundation'
|
|
10
10
|
import fastify from 'fastify'
|
|
11
11
|
import { platform, tmpdir } from 'node:os'
|
|
12
|
-
import { join } from 'node:path'
|
|
12
|
+
import { dirname, join } from 'node:path'
|
|
13
13
|
import { setTimeout as sleep } from 'node:timers/promises'
|
|
14
14
|
import { createWebSocketStream } from 'ws'
|
|
15
15
|
import { prepareApplication } from './config.js'
|
|
@@ -342,8 +342,8 @@ export async function startManagementApi (runtime, config) {
|
|
|
342
342
|
const customSocket = typeof config === 'object' ? config?.socket : null
|
|
343
343
|
|
|
344
344
|
const runtimePIDDir = join(PLATFORMATIC_TMP_DIR, runtimePID.toString())
|
|
345
|
-
if (platform() !== 'win32'
|
|
346
|
-
await createDirectory(runtimePIDDir,
|
|
345
|
+
if (platform() !== 'win32') {
|
|
346
|
+
await createDirectory(customSocket ? dirname(customSocket) : runtimePIDDir, typeof customSocket !== 'string')
|
|
347
347
|
}
|
|
348
348
|
|
|
349
349
|
let socketPath = null
|
package/lib/prom-server.js
CHANGED
|
@@ -2,6 +2,7 @@ import fastifyAccepts from '@fastify/accepts'
|
|
|
2
2
|
import fastifyBasicAuth from '@fastify/basic-auth'
|
|
3
3
|
import { loadModule } from '@platformatic/foundation'
|
|
4
4
|
import fastify from 'fastify'
|
|
5
|
+
import { readFile } from 'node:fs/promises'
|
|
5
6
|
import { createRequire } from 'node:module'
|
|
6
7
|
import { resolve } from 'node:path'
|
|
7
8
|
|
|
@@ -52,6 +53,33 @@ async function checkReadiness (runtime) {
|
|
|
52
53
|
return { response, status }
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
async function sanitizeHTTPSArgument (arg) {
|
|
57
|
+
if (typeof arg === 'string') {
|
|
58
|
+
return arg
|
|
59
|
+
} else if (!Array.isArray(arg)) {
|
|
60
|
+
return readFile(arg.path)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const sanitized = []
|
|
64
|
+
for (const item of arg) {
|
|
65
|
+
sanitized.push(typeof item === 'string' ? item : await readFile(item.path))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return sanitized
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function sanitizeHTTPSOptions (https) {
|
|
72
|
+
if (!https) {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
...https,
|
|
78
|
+
key: await sanitizeHTTPSArgument(https.key),
|
|
79
|
+
cert: await sanitizeHTTPSArgument(https.cert)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
55
83
|
async function checkLiveness (runtime) {
|
|
56
84
|
const { status: ready, response: readinessResponse } = await checkReadiness(runtime)
|
|
57
85
|
if (!ready) {
|
|
@@ -84,8 +112,9 @@ export async function startPrometheusServer (runtime, opts) {
|
|
|
84
112
|
const port = opts.port ?? DEFAULT_PORT
|
|
85
113
|
const metricsEndpoint = opts.endpoint ?? DEFAULT_METRICS_ENDPOINT
|
|
86
114
|
const auth = opts.auth ?? null
|
|
115
|
+
const https = await sanitizeHTTPSOptions(opts.https)
|
|
87
116
|
|
|
88
|
-
const promServer = fastify({ name: 'Prometheus server', loggerInstance: runtime.logger })
|
|
117
|
+
const promServer = fastify({ name: 'Prometheus server', loggerInstance: runtime.logger, https })
|
|
89
118
|
promServer.register(fastifyAccepts)
|
|
90
119
|
|
|
91
120
|
let onRequestHook
|
package/lib/runtime.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
kTimeout,
|
|
10
10
|
parseMemorySize
|
|
11
11
|
} from '@platformatic/foundation'
|
|
12
|
+
import { getExecutable } from '@platformatic/globals'
|
|
12
13
|
import { ITC } from '@platformatic/itc'
|
|
13
14
|
import { collectProcessMetrics, client as metricsClient } from '@platformatic/metrics'
|
|
14
15
|
import fastify from 'fastify'
|
|
@@ -36,7 +37,6 @@ import {
|
|
|
36
37
|
CannotRemoveEntrypointError,
|
|
37
38
|
InvalidArgumentError,
|
|
38
39
|
MessagingError,
|
|
39
|
-
MissingEntrypointError,
|
|
40
40
|
MissingPprofCapture,
|
|
41
41
|
RuntimeAbortedError,
|
|
42
42
|
WorkerInterceptorJoinTimeoutError,
|
|
@@ -274,14 +274,18 @@ export class Runtime extends EventEmitter {
|
|
|
274
274
|
await this.init()
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
if (typeof this.#config.entrypoint === 'undefined') {
|
|
278
|
-
throw new MissingEntrypointError()
|
|
279
|
-
}
|
|
280
277
|
this.#updateStatus('starting')
|
|
281
278
|
this.#createWorkersBroadcastChannel()
|
|
282
279
|
|
|
283
280
|
try {
|
|
284
|
-
|
|
281
|
+
const applications = this.getApplicationsIds()
|
|
282
|
+
await this.startApplications(applications, silent)
|
|
283
|
+
|
|
284
|
+
if (applications.length === 0) {
|
|
285
|
+
this.#updateStatus('started')
|
|
286
|
+
await this.close(silent)
|
|
287
|
+
return
|
|
288
|
+
}
|
|
285
289
|
|
|
286
290
|
if (this.#config.inspectorOptions) {
|
|
287
291
|
const { port } = this.#config.inspectorOptions
|
|
@@ -327,7 +331,9 @@ export class Runtime extends EventEmitter {
|
|
|
327
331
|
this.#startHealthMetricsCollectionIfNeeded()
|
|
328
332
|
|
|
329
333
|
await this.#dynamicWorkersScaler?.start()
|
|
330
|
-
this.#
|
|
334
|
+
if (this.#url) {
|
|
335
|
+
this.#showUrl()
|
|
336
|
+
}
|
|
331
337
|
return this.#url
|
|
332
338
|
}
|
|
333
339
|
|
|
@@ -582,7 +588,7 @@ export class Runtime extends EventEmitter {
|
|
|
582
588
|
|
|
583
589
|
// Now, topological sort the applications based on their dependencies.
|
|
584
590
|
// If circular dependencies are detected, an error with proper error code is thrown.
|
|
585
|
-
applications = topologicalSort(dependencies)
|
|
591
|
+
applications = topologicalSort(applications, dependencies)
|
|
586
592
|
|
|
587
593
|
// Group into dependency levels so that each level's dependencies are all
|
|
588
594
|
// in previous levels. Levels are started sequentially, but applications
|
|
@@ -1122,6 +1128,10 @@ export class Runtime extends EventEmitter {
|
|
|
1122
1128
|
}
|
|
1123
1129
|
|
|
1124
1130
|
async getEntrypointDetails () {
|
|
1131
|
+
if (!this.#entrypointId) {
|
|
1132
|
+
return null
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1125
1135
|
return this.getApplicationDetails(this.#entrypointId)
|
|
1126
1136
|
}
|
|
1127
1137
|
|
|
@@ -1168,6 +1178,10 @@ export class Runtime extends EventEmitter {
|
|
|
1168
1178
|
}
|
|
1169
1179
|
|
|
1170
1180
|
async getMetrics (format = 'json') {
|
|
1181
|
+
if (this.#config.metrics === false || this.#config.metrics?.enabled === false) {
|
|
1182
|
+
throw new Error('Metrics are disabled')
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1171
1185
|
let metrics = null
|
|
1172
1186
|
|
|
1173
1187
|
const applicationRestartMetrics = format === 'json'
|
|
@@ -1716,6 +1730,10 @@ export class Runtime extends EventEmitter {
|
|
|
1716
1730
|
}
|
|
1717
1731
|
|
|
1718
1732
|
#showUrl () {
|
|
1733
|
+
if (!this.#url) {
|
|
1734
|
+
return
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1719
1737
|
this.logger.info(`Platformatic is now listening at ${this.#url}`)
|
|
1720
1738
|
}
|
|
1721
1739
|
|
|
@@ -1734,7 +1752,7 @@ export class Runtime extends EventEmitter {
|
|
|
1734
1752
|
applicationConfig.path = join(this.#root, config.resolvedApplicationsBasePath, id)
|
|
1735
1753
|
|
|
1736
1754
|
if (!existsSync(applicationConfig.path)) {
|
|
1737
|
-
const executable =
|
|
1755
|
+
const executable = getExecutable() ?? 'platformatic'
|
|
1738
1756
|
this.logger.error(
|
|
1739
1757
|
`The path for application "%s" does not exist. Please run "${executable} resolve" and try again.`,
|
|
1740
1758
|
id
|
|
@@ -2713,6 +2731,12 @@ export class Runtime extends EventEmitter {
|
|
|
2713
2731
|
|
|
2714
2732
|
async #createWorkersBroadcastChannel () {
|
|
2715
2733
|
this.#workersBroadcastChannel?.close()
|
|
2734
|
+
|
|
2735
|
+
if (this.#config.applications.length === 0) {
|
|
2736
|
+
this.#workersBroadcastChannel = undefined
|
|
2737
|
+
return
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2716
2740
|
this.#workersBroadcastChannel = new BroadcastChannel(kWorkersBroadcast)
|
|
2717
2741
|
}
|
|
2718
2742
|
|
package/lib/utils.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
2
|
import { tmpdir } from 'node:os'
|
|
3
3
|
import { join } from 'node:path'
|
|
4
|
-
import { ApplicationsDependenciesCycleError } from './errors.js'
|
|
4
|
+
import { ApplicationDependencyNotFoundError, ApplicationsDependenciesCycleError } from './errors.js'
|
|
5
5
|
|
|
6
6
|
export function getArrayDifference (a, b) {
|
|
7
7
|
return a.filter(element => {
|
|
@@ -48,13 +48,22 @@ export function topologicalLevels (sorted, graph) {
|
|
|
48
48
|
return levels
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
//
|
|
52
|
-
export function topologicalSort (graph) {
|
|
51
|
+
// valid: string[] | Map<string, string[]>, graph?: Map<string, string[]>
|
|
52
|
+
export function topologicalSort (valid, graph) {
|
|
53
|
+
if (!graph) {
|
|
54
|
+
graph = valid
|
|
55
|
+
valid = Array.from(graph.keys())
|
|
56
|
+
}
|
|
57
|
+
|
|
53
58
|
const result = []
|
|
54
59
|
const visited = new Set()
|
|
55
60
|
const path = []
|
|
56
61
|
|
|
57
62
|
function visit (node) {
|
|
63
|
+
if (!valid.includes(node)) {
|
|
64
|
+
throw new ApplicationDependencyNotFoundError(node, valid.join(', '))
|
|
65
|
+
}
|
|
66
|
+
|
|
58
67
|
if (visited.has(node)) {
|
|
59
68
|
return
|
|
60
69
|
}
|
package/lib/worker/controller.js
CHANGED
|
@@ -8,6 +8,16 @@ import {
|
|
|
8
8
|
loadConfigurationModule,
|
|
9
9
|
mirrorGlobalDispatcherForBuiltinFetch
|
|
10
10
|
} from '@platformatic/foundation'
|
|
11
|
+
import {
|
|
12
|
+
getLogger,
|
|
13
|
+
getOnActiveResourcesEventLoop,
|
|
14
|
+
getOnHttpStatsConnected,
|
|
15
|
+
getOnHttpStatsFree,
|
|
16
|
+
getOnHttpStatsPending,
|
|
17
|
+
getOnHttpStatsQueued,
|
|
18
|
+
getOnHttpStatsRunning,
|
|
19
|
+
getOnHttpStatsSize
|
|
20
|
+
} from '@platformatic/globals'
|
|
11
21
|
import debounce from 'debounce'
|
|
12
22
|
import { EventEmitter } from 'node:events'
|
|
13
23
|
import { existsSync } from 'node:fs'
|
|
@@ -17,6 +27,7 @@ import { workerData } from 'node:worker_threads'
|
|
|
17
27
|
import { getGlobalDispatcher, setGlobalDispatcher } from 'undici'
|
|
18
28
|
import { ApplicationAlreadyStartedError, RuntimeNotStartedError } from '../errors.js'
|
|
19
29
|
import { getApplicationUrl } from '../utils.js'
|
|
30
|
+
import { markAsPlatformaticDispatcher, refreshGlobalDispatcher } from './interceptors.js'
|
|
20
31
|
|
|
21
32
|
function fetchApplicationUrl (application, key) {
|
|
22
33
|
if (!key.endsWith('_URL') || !application.id) {
|
|
@@ -29,7 +40,8 @@ function fetchApplicationUrl (application, key) {
|
|
|
29
40
|
function handleUnhandled (app, type, err) {
|
|
30
41
|
const label = `worker ${workerData.worker.index} of the application "${workerData.applicationConfig.id}"`
|
|
31
42
|
|
|
32
|
-
|
|
43
|
+
const logger = getLogger()
|
|
44
|
+
logger.error({ err: ensureLoggableError(err) }, `The ${label} threw an ${type}.`)
|
|
33
45
|
|
|
34
46
|
executeWithTimeout(app?.stop(), 1000)
|
|
35
47
|
.catch()
|
|
@@ -142,7 +154,8 @@ export class Controller extends EventEmitter {
|
|
|
142
154
|
}
|
|
143
155
|
} catch (err) {
|
|
144
156
|
if (err.validationErrors) {
|
|
145
|
-
|
|
157
|
+
const logger = getLogger()
|
|
158
|
+
logger.error(
|
|
146
159
|
{ err: ensureLoggableError(err) },
|
|
147
160
|
'The application threw a validation error.'
|
|
148
161
|
)
|
|
@@ -193,6 +206,9 @@ export class Controller extends EventEmitter {
|
|
|
193
206
|
|
|
194
207
|
try {
|
|
195
208
|
await this.capability.start({ listen })
|
|
209
|
+
if (refreshGlobalDispatcher()) {
|
|
210
|
+
this.#updateDispatcher()
|
|
211
|
+
}
|
|
196
212
|
this.#listening = listen
|
|
197
213
|
/* c8 ignore next 5 */
|
|
198
214
|
} catch (err) {
|
|
@@ -244,18 +260,31 @@ export class Controller extends EventEmitter {
|
|
|
244
260
|
|
|
245
261
|
async getMetrics ({ format }) {
|
|
246
262
|
const dispatcher = getGlobalDispatcher()
|
|
247
|
-
|
|
263
|
+
const onHttpStatsFree = getOnHttpStatsFree({ throwOnMissing: false })
|
|
264
|
+
|
|
265
|
+
if (onHttpStatsFree && dispatcher?.stats) {
|
|
266
|
+
// The capability might come from an older version of @platformatic/basic
|
|
267
|
+
// which registered these globals without the fields tracking, so never throw.
|
|
268
|
+
const onHttpStatsConnected = getOnHttpStatsConnected({ throwOnMissing: false })
|
|
269
|
+
const onHttpStatsPending = getOnHttpStatsPending({ throwOnMissing: false })
|
|
270
|
+
const onHttpStatsQueued = getOnHttpStatsQueued({ throwOnMissing: false })
|
|
271
|
+
const onHttpStatsRunning = getOnHttpStatsRunning({ throwOnMissing: false })
|
|
272
|
+
const onHttpStatsSize = getOnHttpStatsSize({ throwOnMissing: false })
|
|
273
|
+
|
|
248
274
|
for (const url in dispatcher.stats) {
|
|
249
275
|
const { free, connected, pending, queued, running, size } = dispatcher.stats[url]
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
276
|
+
onHttpStatsFree(url, free || 0)
|
|
277
|
+
onHttpStatsConnected?.(url, connected || 0)
|
|
278
|
+
onHttpStatsPending?.(url, pending || 0)
|
|
279
|
+
onHttpStatsQueued?.(url, queued || 0)
|
|
280
|
+
onHttpStatsRunning?.(url, running || 0)
|
|
281
|
+
onHttpStatsSize?.(url, size || 0)
|
|
256
282
|
}
|
|
257
283
|
}
|
|
258
|
-
|
|
284
|
+
const onActiveResourcesEventLoop = getOnActiveResourcesEventLoop({ throwOnMissing: false })
|
|
285
|
+
if (onActiveResourcesEventLoop) {
|
|
286
|
+
onActiveResourcesEventLoop(getActiveResourcesInfo().length)
|
|
287
|
+
}
|
|
259
288
|
return this.capability.getMetrics({ format })
|
|
260
289
|
}
|
|
261
290
|
|
|
@@ -300,7 +329,8 @@ export class Controller extends EventEmitter {
|
|
|
300
329
|
}
|
|
301
330
|
|
|
302
331
|
#logAndThrow (err) {
|
|
303
|
-
|
|
332
|
+
const logger = getLogger()
|
|
333
|
+
logger.error(
|
|
304
334
|
{ err: ensureLoggableError(err) },
|
|
305
335
|
err[kHandledError] ? err.message : 'The application threw an error.'
|
|
306
336
|
)
|
|
@@ -326,6 +356,7 @@ export class Controller extends EventEmitter {
|
|
|
326
356
|
|
|
327
357
|
const dispatcher = getGlobalDispatcher().compose(interceptor)
|
|
328
358
|
|
|
359
|
+
markAsPlatformaticDispatcher(dispatcher)
|
|
329
360
|
setGlobalDispatcher(dispatcher)
|
|
330
361
|
mirrorGlobalDispatcherForBuiltinFetch(dispatcher)
|
|
331
362
|
}
|
|
@@ -336,7 +367,8 @@ export class Controller extends EventEmitter {
|
|
|
336
367
|
|
|
337
368
|
process.on('newListener', event => {
|
|
338
369
|
if (event === 'uncaughtException' || event === 'unhandledRejection') {
|
|
339
|
-
|
|
370
|
+
const logger = getLogger()
|
|
371
|
+
logger.warn(
|
|
340
372
|
`A listener has been added for the "process.${event}" event. This listener will be never triggered as Watt default behavior will kill the process before.\n To disable this behavior, set "exitOnUnhandledErrors" to false in the runtime config.`
|
|
341
373
|
)
|
|
342
374
|
}
|