@platformatic/runtime 3.33.0 → 3.34.1-alpha.3
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 +21 -0
- package/lib/errors.js +4 -0
- package/lib/logger.js +1 -1
- package/lib/runtime.js +15 -30
- package/lib/worker/itc.js +73 -0
- package/package.json +15 -15
- package/schema.json +32 -1
package/config.d.ts
CHANGED
|
@@ -213,6 +213,10 @@ export type PlatformaticRuntimeConfig = {
|
|
|
213
213
|
gracefulShutdown?: {
|
|
214
214
|
runtime: number | string;
|
|
215
215
|
application: number | string;
|
|
216
|
+
/**
|
|
217
|
+
* Add Connection: close header to HTTP responses during graceful shutdown
|
|
218
|
+
*/
|
|
219
|
+
closeConnections?: boolean;
|
|
216
220
|
};
|
|
217
221
|
health?: {
|
|
218
222
|
enabled?: boolean | string;
|
|
@@ -376,6 +380,23 @@ export type PlatformaticRuntimeConfig = {
|
|
|
376
380
|
*/
|
|
377
381
|
serviceVersion?: string;
|
|
378
382
|
};
|
|
383
|
+
/**
|
|
384
|
+
* Custom labels to add to HTTP metrics (http_request_all_duration_seconds). Each label extracts its value from an HTTP request header.
|
|
385
|
+
*/
|
|
386
|
+
httpCustomLabels?: {
|
|
387
|
+
/**
|
|
388
|
+
* The label name to use in metrics
|
|
389
|
+
*/
|
|
390
|
+
name: string;
|
|
391
|
+
/**
|
|
392
|
+
* The HTTP request header to extract the value from
|
|
393
|
+
*/
|
|
394
|
+
header: string;
|
|
395
|
+
/**
|
|
396
|
+
* Default value when header is missing (defaults to "unknown")
|
|
397
|
+
*/
|
|
398
|
+
default?: string;
|
|
399
|
+
}[];
|
|
379
400
|
};
|
|
380
401
|
telemetry?: {
|
|
381
402
|
enabled?: boolean | string;
|
package/lib/errors.js
CHANGED
|
@@ -148,3 +148,7 @@ export const WorkerInterceptorNotReadyError = createError(
|
|
|
148
148
|
`${ERROR_PREFIX}_WORKER_INTERCEPTOR_NOT_READY`,
|
|
149
149
|
'The "%s" application worker interceptor is not ready'
|
|
150
150
|
)
|
|
151
|
+
export const WorkerInterceptorJoinTimeoutError = createError(
|
|
152
|
+
`${ERROR_PREFIX}_WORKER_INTERCEPTOR_JOIN_TIMEOUT`,
|
|
153
|
+
'The %s failed to join the mesh network in %dms.'
|
|
154
|
+
)
|
package/lib/logger.js
CHANGED
|
@@ -139,7 +139,7 @@ function createPrettifier (context) {
|
|
|
139
139
|
const current = context.current
|
|
140
140
|
const level = current.caller ? current.caller : labelColorized.replace(label, label.padStart(6, ' '))
|
|
141
141
|
|
|
142
|
-
return `${current.prefix}
|
|
142
|
+
return `${current.prefix} |\u001B[0m${level}`
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
})
|
package/lib/runtime.js
CHANGED
|
@@ -10,13 +10,14 @@ import {
|
|
|
10
10
|
parseMemorySize
|
|
11
11
|
} from '@platformatic/foundation'
|
|
12
12
|
import { ITC } from '@platformatic/itc'
|
|
13
|
-
import { client as metricsClient
|
|
13
|
+
import { collectProcessMetrics, client as metricsClient } from '@platformatic/metrics'
|
|
14
14
|
import fastify from 'fastify'
|
|
15
15
|
import { EventEmitter, once } from 'node:events'
|
|
16
16
|
import { existsSync } from 'node:fs'
|
|
17
17
|
import { readFile } from 'node:fs/promises'
|
|
18
18
|
import { STATUS_CODES } from 'node:http'
|
|
19
19
|
import { createRequire } from 'node:module'
|
|
20
|
+
import { availableParallelism } from 'node:os'
|
|
20
21
|
import { dirname, isAbsolute, join } from 'node:path'
|
|
21
22
|
import { setImmediate as immediate, setTimeout as sleep } from 'node:timers/promises'
|
|
22
23
|
import { pathToFileURL } from 'node:url'
|
|
@@ -36,8 +37,8 @@ import {
|
|
|
36
37
|
MissingEntrypointError,
|
|
37
38
|
MissingPprofCapture,
|
|
38
39
|
RuntimeAbortedError,
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
WorkerInterceptorJoinTimeoutError,
|
|
41
|
+
WorkerNotFoundError
|
|
41
42
|
} from './errors.js'
|
|
42
43
|
import { abstractLogger, createLogger } from './logger.js'
|
|
43
44
|
import { startManagementApi } from './management-api.js'
|
|
@@ -56,6 +57,7 @@ import {
|
|
|
56
57
|
kFullId,
|
|
57
58
|
kHealthCheckTimer,
|
|
58
59
|
kId,
|
|
60
|
+
kInterceptorReadyPromise,
|
|
59
61
|
kITC,
|
|
60
62
|
kLastHealthCheckELU,
|
|
61
63
|
kStderrMarker,
|
|
@@ -63,8 +65,7 @@ import {
|
|
|
63
65
|
kWorkerId,
|
|
64
66
|
kWorkersBroadcast,
|
|
65
67
|
kWorkerStartTime,
|
|
66
|
-
kWorkerStatus
|
|
67
|
-
kInterceptorReadyPromise
|
|
68
|
+
kWorkerStatus
|
|
68
69
|
} from './worker/symbols.js'
|
|
69
70
|
|
|
70
71
|
const kWorkerFile = join(import.meta.dirname, 'worker/main.js')
|
|
@@ -89,7 +90,7 @@ function parseOrigins (origins) {
|
|
|
89
90
|
})
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
const MAX_CONCURRENCY =
|
|
93
|
+
const MAX_CONCURRENCY = availableParallelism()
|
|
93
94
|
const MAX_BOOTSTRAP_ATTEMPTS = 5
|
|
94
95
|
const IMMEDIATE_RESTART_MAX_THRESHOLD = 10
|
|
95
96
|
const MAX_WORKERS = 100
|
|
@@ -497,7 +498,7 @@ export class Runtime extends EventEmitter {
|
|
|
497
498
|
await executeInParallel(this.#setupApplication.bind(this), setupInvocations, this.#concurrency)
|
|
498
499
|
|
|
499
500
|
for (const application of applications) {
|
|
500
|
-
this.logger.
|
|
501
|
+
this.logger.debug(`Added application "${application.id}"${application.entrypoint ? ' (entrypoint)' : ''}.`)
|
|
501
502
|
this.emitAndNotify('application:added', application)
|
|
502
503
|
}
|
|
503
504
|
|
|
@@ -534,7 +535,7 @@ export class Runtime extends EventEmitter {
|
|
|
534
535
|
}
|
|
535
536
|
|
|
536
537
|
for (const application of applications) {
|
|
537
|
-
this.logger.
|
|
538
|
+
this.logger.warn(`Removed application "${application}".`)
|
|
538
539
|
this.emitAndNotify('application:removed', application)
|
|
539
540
|
}
|
|
540
541
|
|
|
@@ -2058,7 +2059,7 @@ export class Runtime extends EventEmitter {
|
|
|
2058
2059
|
|
|
2059
2060
|
if (workerUrl === kTimeout) {
|
|
2060
2061
|
this.emitAndNotify('application:worker:startTimeout', eventPayload)
|
|
2061
|
-
this.logger.
|
|
2062
|
+
this.logger.error(`The ${label} failed to start in ${config.startTimeout}ms. Forcefully killing the thread.`)
|
|
2062
2063
|
worker.terminate()
|
|
2063
2064
|
throw new ApplicationStartTimeoutError(id, config.startTimeout)
|
|
2064
2065
|
}
|
|
@@ -2072,7 +2073,11 @@ export class Runtime extends EventEmitter {
|
|
|
2072
2073
|
this.#url = workerUrl
|
|
2073
2074
|
}
|
|
2074
2075
|
|
|
2075
|
-
|
|
2076
|
+
// Wait for the interceptor to be ready
|
|
2077
|
+
const interceptorResult = await executeWithTimeout(worker[kInterceptorReadyPromise], config.startTimeout)
|
|
2078
|
+
if (interceptorResult === kTimeout) {
|
|
2079
|
+
throw new WorkerInterceptorJoinTimeoutError(label, config.startTimeout)
|
|
2080
|
+
}
|
|
2076
2081
|
|
|
2077
2082
|
worker[kWorkerStatus] = 'started'
|
|
2078
2083
|
worker[kWorkerStartTime] = Date.now()
|
|
@@ -2983,24 +2988,4 @@ export class Runtime extends EventEmitter {
|
|
|
2983
2988
|
|
|
2984
2989
|
this.#loggerContext.updatePrefixes(ids)
|
|
2985
2990
|
}
|
|
2986
|
-
|
|
2987
|
-
async #waitForWorkerInterceptor (worker, timeout = 10000) {
|
|
2988
|
-
const workerId = worker[kId]
|
|
2989
|
-
const applicationId = worker[kApplicationId]
|
|
2990
|
-
|
|
2991
|
-
const interceptorReadyTimeout = setTimeout(() => {
|
|
2992
|
-
this.logger.error(
|
|
2993
|
-
{ applicationId, workerId },
|
|
2994
|
-
'The worker interceptor is not ready after 10s'
|
|
2995
|
-
)
|
|
2996
|
-
throw new WorkerInterceptorNotReadyError(applicationId)
|
|
2997
|
-
}, timeout)
|
|
2998
|
-
|
|
2999
|
-
try {
|
|
3000
|
-
await worker[kInterceptorReadyPromise]
|
|
3001
|
-
worker[kInterceptorReadyPromise] = null
|
|
3002
|
-
} finally {
|
|
3003
|
-
clearTimeout(interceptorReadyTimeout)
|
|
3004
|
-
}
|
|
3005
|
-
}
|
|
3006
2991
|
}
|
package/lib/worker/itc.js
CHANGED
|
@@ -20,6 +20,58 @@ import { updateUndiciInterceptors } from './interceptors.js'
|
|
|
20
20
|
import { MessagingITC } from './messaging.js'
|
|
21
21
|
import { kApplicationId, kITC, kId, kWorkerId } from './symbols.js'
|
|
22
22
|
|
|
23
|
+
function startSubprocessRepl (port, childManager, clientWs, controller) {
|
|
24
|
+
// Start the REPL in the child process
|
|
25
|
+
childManager.send(clientWs, 'startRepl').catch(err => {
|
|
26
|
+
port.postMessage({ type: 'output', data: `Error starting REPL: ${err.message}\n` })
|
|
27
|
+
port.postMessage({ type: 'exit' })
|
|
28
|
+
port.close()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// Listen for repl:output notifications from the child process
|
|
32
|
+
function handleReplOutput ({ data }) {
|
|
33
|
+
port.postMessage({ type: 'output', data })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Listen for repl:exit notifications from the child process
|
|
37
|
+
function handleReplExit () {
|
|
38
|
+
cleanup()
|
|
39
|
+
port.postMessage({ type: 'exit' })
|
|
40
|
+
port.close()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function cleanup () {
|
|
44
|
+
childManager.removeListener('repl:output', handleReplOutput)
|
|
45
|
+
childManager.removeListener('repl:exit', handleReplExit)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
childManager.on('repl:output', handleReplOutput)
|
|
49
|
+
childManager.on('repl:exit', handleReplExit)
|
|
50
|
+
|
|
51
|
+
// Forward input from MessagePort to child process
|
|
52
|
+
port.on('message', (message) => {
|
|
53
|
+
if (message.type === 'input') {
|
|
54
|
+
childManager.send(clientWs, 'replInput', { data: message.data }).catch(() => {
|
|
55
|
+
// Ignore errors if the child process has exited
|
|
56
|
+
})
|
|
57
|
+
} else if (message.type === 'close') {
|
|
58
|
+
childManager.send(clientWs, 'replClose').catch(() => {
|
|
59
|
+
// Ignore errors if the child process has exited
|
|
60
|
+
})
|
|
61
|
+
cleanup()
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
port.on('close', () => {
|
|
66
|
+
childManager.send(clientWs, 'replClose').catch(() => {
|
|
67
|
+
// Ignore errors if the child process has exited
|
|
68
|
+
})
|
|
69
|
+
cleanup()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return { started: true }
|
|
73
|
+
}
|
|
74
|
+
|
|
23
75
|
async function safeHandleInITC (worker, fn) {
|
|
24
76
|
try {
|
|
25
77
|
// Make sure to catch when the worker exits, otherwise we're stuck forever
|
|
@@ -241,6 +293,19 @@ export function setupITC (controller, application, dispatcher, sharedContext) {
|
|
|
241
293
|
},
|
|
242
294
|
|
|
243
295
|
async getHealth () {
|
|
296
|
+
// Check if running in subprocess mode - forward through ChildManager
|
|
297
|
+
const childManager = controller.capability?.getChildManager?.()
|
|
298
|
+
const clientWs = controller.capability?.clientWs
|
|
299
|
+
|
|
300
|
+
if (childManager && clientWs) {
|
|
301
|
+
try {
|
|
302
|
+
return await childManager.send(clientWs, 'getHealth')
|
|
303
|
+
} catch (err) {
|
|
304
|
+
throw new FailedToRetrieveHealthError(application.id, err.message)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Existing thread implementation
|
|
244
309
|
try {
|
|
245
310
|
return await controller.getHealth()
|
|
246
311
|
} catch (err) {
|
|
@@ -273,6 +338,14 @@ export function setupITC (controller, application, dispatcher, sharedContext) {
|
|
|
273
338
|
},
|
|
274
339
|
|
|
275
340
|
startRepl (port) {
|
|
341
|
+
// Check if running in subprocess mode - forward through ChildManager
|
|
342
|
+
const childManager = controller.capability?.getChildManager?.()
|
|
343
|
+
const clientWs = controller.capability?.clientWs
|
|
344
|
+
|
|
345
|
+
if (childManager && clientWs) {
|
|
346
|
+
return startSubprocessRepl(port, childManager, clientWs, controller)
|
|
347
|
+
}
|
|
348
|
+
|
|
276
349
|
// We are loading the repl module dynamically here to avoid loading it
|
|
277
350
|
// when not needed (since it pulls in domain, which is quite expensive
|
|
278
351
|
// as it monkey patches EventEmitter).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/runtime",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.34.1-alpha.3",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -35,14 +35,14 @@
|
|
|
35
35
|
"typescript": "^5.5.4",
|
|
36
36
|
"undici-oidc-interceptor": "^0.5.0",
|
|
37
37
|
"why-is-node-running": "^2.2.2",
|
|
38
|
-
"@platformatic/
|
|
39
|
-
"@platformatic/
|
|
40
|
-
"@platformatic/
|
|
41
|
-
"@platformatic/
|
|
42
|
-
"@platformatic/
|
|
43
|
-
"@platformatic/sql-
|
|
44
|
-
"@platformatic/sql-
|
|
45
|
-
"@platformatic/wattpm-pprof-capture": "3.
|
|
38
|
+
"@platformatic/composer": "3.34.1-alpha.3",
|
|
39
|
+
"@platformatic/db": "3.34.1-alpha.3",
|
|
40
|
+
"@platformatic/node": "3.34.1-alpha.3",
|
|
41
|
+
"@platformatic/service": "3.34.1-alpha.3",
|
|
42
|
+
"@platformatic/gateway": "3.34.1-alpha.3",
|
|
43
|
+
"@platformatic/sql-graphql": "3.34.1-alpha.3",
|
|
44
|
+
"@platformatic/sql-mapper": "3.34.1-alpha.3",
|
|
45
|
+
"@platformatic/wattpm-pprof-capture": "3.34.1-alpha.3"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@fastify/accepts": "^5.0.0",
|
|
@@ -71,12 +71,12 @@
|
|
|
71
71
|
"undici": "^7.0.0",
|
|
72
72
|
"undici-thread-interceptor": "^1.0.0",
|
|
73
73
|
"ws": "^8.16.0",
|
|
74
|
-
"@platformatic/basic": "3.
|
|
75
|
-
"@platformatic/foundation": "3.
|
|
76
|
-
"@platformatic/
|
|
77
|
-
"@platformatic/
|
|
78
|
-
"@platformatic/metrics": "3.
|
|
79
|
-
"@platformatic/telemetry": "3.
|
|
74
|
+
"@platformatic/basic": "3.34.1-alpha.3",
|
|
75
|
+
"@platformatic/foundation": "3.34.1-alpha.3",
|
|
76
|
+
"@platformatic/itc": "3.34.1-alpha.3",
|
|
77
|
+
"@platformatic/generators": "3.34.1-alpha.3",
|
|
78
|
+
"@platformatic/metrics": "3.34.1-alpha.3",
|
|
79
|
+
"@platformatic/telemetry": "3.34.1-alpha.3"
|
|
80
80
|
},
|
|
81
81
|
"engines": {
|
|
82
82
|
"node": ">=22.19.0"
|
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.34.1-alpha.3.json",
|
|
3
3
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
4
|
"title": "Platformatic Runtime Config",
|
|
5
5
|
"type": "object",
|
|
@@ -1757,6 +1757,11 @@
|
|
|
1757
1757
|
}
|
|
1758
1758
|
],
|
|
1759
1759
|
"default": 10000
|
|
1760
|
+
},
|
|
1761
|
+
"closeConnections": {
|
|
1762
|
+
"type": "boolean",
|
|
1763
|
+
"default": true,
|
|
1764
|
+
"description": "Add Connection: close header to HTTP responses during graceful shutdown"
|
|
1760
1765
|
}
|
|
1761
1766
|
},
|
|
1762
1767
|
"default": {},
|
|
@@ -2297,6 +2302,32 @@
|
|
|
2297
2302
|
"endpoint"
|
|
2298
2303
|
],
|
|
2299
2304
|
"additionalProperties": false
|
|
2305
|
+
},
|
|
2306
|
+
"httpCustomLabels": {
|
|
2307
|
+
"type": "array",
|
|
2308
|
+
"description": "Custom labels to add to HTTP metrics (http_request_all_duration_seconds). Each label extracts its value from an HTTP request header.",
|
|
2309
|
+
"items": {
|
|
2310
|
+
"type": "object",
|
|
2311
|
+
"properties": {
|
|
2312
|
+
"name": {
|
|
2313
|
+
"type": "string",
|
|
2314
|
+
"description": "The label name to use in metrics"
|
|
2315
|
+
},
|
|
2316
|
+
"header": {
|
|
2317
|
+
"type": "string",
|
|
2318
|
+
"description": "The HTTP request header to extract the value from"
|
|
2319
|
+
},
|
|
2320
|
+
"default": {
|
|
2321
|
+
"type": "string",
|
|
2322
|
+
"description": "Default value when header is missing (defaults to \"unknown\")"
|
|
2323
|
+
}
|
|
2324
|
+
},
|
|
2325
|
+
"required": [
|
|
2326
|
+
"name",
|
|
2327
|
+
"header"
|
|
2328
|
+
],
|
|
2329
|
+
"additionalProperties": false
|
|
2330
|
+
}
|
|
2300
2331
|
}
|
|
2301
2332
|
},
|
|
2302
2333
|
"additionalProperties": false
|