@platformatic/basic 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/capability.js +39 -2
- package/lib/worker/child-process.js +139 -1
- package/package.json +5 -5
- package/schema.json +32 -1
package/config.d.ts
CHANGED
|
@@ -123,6 +123,10 @@ export interface PlatformaticBasicConfig {
|
|
|
123
123
|
gracefulShutdown?: {
|
|
124
124
|
runtime: number | string;
|
|
125
125
|
application: number | string;
|
|
126
|
+
/**
|
|
127
|
+
* Add Connection: close header to HTTP responses during graceful shutdown
|
|
128
|
+
*/
|
|
129
|
+
closeConnections?: boolean;
|
|
126
130
|
};
|
|
127
131
|
health?: {
|
|
128
132
|
enabled?: boolean | string;
|
|
@@ -286,6 +290,23 @@ export interface PlatformaticBasicConfig {
|
|
|
286
290
|
*/
|
|
287
291
|
serviceVersion?: string;
|
|
288
292
|
};
|
|
293
|
+
/**
|
|
294
|
+
* Custom labels to add to HTTP metrics (http_request_all_duration_seconds). Each label extracts its value from an HTTP request header.
|
|
295
|
+
*/
|
|
296
|
+
httpCustomLabels?: {
|
|
297
|
+
/**
|
|
298
|
+
* The label name to use in metrics
|
|
299
|
+
*/
|
|
300
|
+
name: string;
|
|
301
|
+
/**
|
|
302
|
+
* The HTTP request header to extract the value from
|
|
303
|
+
*/
|
|
304
|
+
header: string;
|
|
305
|
+
/**
|
|
306
|
+
* Default value when header is missing (defaults to "unknown")
|
|
307
|
+
*/
|
|
308
|
+
default?: string;
|
|
309
|
+
}[];
|
|
289
310
|
};
|
|
290
311
|
telemetry?: {
|
|
291
312
|
enabled?: boolean | string;
|
package/lib/capability.js
CHANGED
|
@@ -60,6 +60,7 @@ export class BaseCapability extends EventEmitter {
|
|
|
60
60
|
#metricsCollected
|
|
61
61
|
#pendingDependenciesWaits
|
|
62
62
|
#reuseTcpPortsSubscribers
|
|
63
|
+
#closing
|
|
63
64
|
|
|
64
65
|
constructor (type, version, root, config, context, standardStreams = {}) {
|
|
65
66
|
super()
|
|
@@ -97,6 +98,16 @@ export class BaseCapability extends EventEmitter {
|
|
|
97
98
|
// True by default, can be overridden in subclasses. If false, it takes precedence over the runtime configuration
|
|
98
99
|
this.exitOnUnhandledErrors = true
|
|
99
100
|
|
|
101
|
+
// Track if graceful shutdown is in progress
|
|
102
|
+
this.#closing = false
|
|
103
|
+
|
|
104
|
+
// Listen for controller 'stopping' event to initiate graceful shutdown early
|
|
105
|
+
if (this.context.controller) {
|
|
106
|
+
this.context.controller.once('stopping', () => {
|
|
107
|
+
this.setClosing()
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
100
111
|
// Setup globals
|
|
101
112
|
this.registerGlobals({
|
|
102
113
|
capability: this,
|
|
@@ -433,6 +444,21 @@ export class BaseCapability extends EventEmitter {
|
|
|
433
444
|
}
|
|
434
445
|
}
|
|
435
446
|
|
|
447
|
+
get closing () {
|
|
448
|
+
return this.#closing
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
setClosing () {
|
|
452
|
+
if (this.#closing) return
|
|
453
|
+
this.#closing = true
|
|
454
|
+
this.emit('closing')
|
|
455
|
+
|
|
456
|
+
// Forward to child process if using childManager
|
|
457
|
+
if (this.childManager && this.clientWs) {
|
|
458
|
+
this.childManager.send(this.clientWs, 'setClosing', {}).catch(() => {})
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
436
462
|
async buildWithCommand (command, basePath, opts = {}) {
|
|
437
463
|
const { loader, scripts, context, disableChildManager } = opts
|
|
438
464
|
|
|
@@ -610,6 +636,11 @@ export class BaseCapability extends EventEmitter {
|
|
|
610
636
|
this.emit('application:worker:event:' + event.event, event.payload)
|
|
611
637
|
})
|
|
612
638
|
|
|
639
|
+
// Forward health signals from child process to runtime
|
|
640
|
+
childManager.on('healthSignals', ({ workerId, signals }) => {
|
|
641
|
+
globalThis[kITC]?.send('sendHealthSignals', { workerId, signals })
|
|
642
|
+
})
|
|
643
|
+
|
|
613
644
|
// This is not really important for the URL but sometimes it also a sign
|
|
614
645
|
// that the process has been replaced and thus we need to update the client WebSocket
|
|
615
646
|
childManager.on('url', (url, clientWs) => {
|
|
@@ -619,13 +650,19 @@ export class BaseCapability extends EventEmitter {
|
|
|
619
650
|
}
|
|
620
651
|
|
|
621
652
|
async spawn (command) {
|
|
622
|
-
|
|
653
|
+
let [executable, ...args] = parseCommandString(command)
|
|
623
654
|
const hasChainedCommands = command.includes('&&') || command.includes('||') || command.includes(';')
|
|
624
655
|
|
|
656
|
+
// Use the current Node.js executable instead of relying on PATH lookup
|
|
657
|
+
// This ensures subprocess uses the same Node.js version as the parent
|
|
658
|
+
if (executable === 'node') {
|
|
659
|
+
executable = process.execPath
|
|
660
|
+
}
|
|
661
|
+
|
|
625
662
|
/* c8 ignore next 3 */
|
|
626
663
|
const subprocess =
|
|
627
664
|
platform() === 'win32'
|
|
628
|
-
? spawn(command, { cwd: this.root, shell: true, windowsVerbatimArguments: true })
|
|
665
|
+
? spawn(command.replace(/^node\b/, process.execPath), { cwd: this.root, shell: true, windowsVerbatimArguments: true })
|
|
629
666
|
: spawn(executable, args, { cwd: this.root, shell: hasChainedCommands })
|
|
630
667
|
|
|
631
668
|
subprocess.stdout.setEncoding('utf8')
|
|
@@ -10,9 +10,10 @@ import diagnosticChannel, { tracingChannel } from 'node:diagnostics_channel'
|
|
|
10
10
|
import { EventEmitter, once } from 'node:events'
|
|
11
11
|
import { readFile } from 'node:fs/promises'
|
|
12
12
|
import { ServerResponse } from 'node:http'
|
|
13
|
-
import { register } from 'node:module'
|
|
13
|
+
import { createRequire, register } from 'node:module'
|
|
14
14
|
import { hostname, platform, tmpdir } from 'node:os'
|
|
15
15
|
import { basename, join, resolve } from 'node:path'
|
|
16
|
+
import { Duplex } from 'node:stream'
|
|
16
17
|
import { fileURLToPath } from 'node:url'
|
|
17
18
|
import pino from 'pino'
|
|
18
19
|
import { Agent, Pool, setGlobalDispatcher } from 'undici'
|
|
@@ -76,6 +77,8 @@ export class ChildProcess extends ITC {
|
|
|
76
77
|
#logger
|
|
77
78
|
#metricsRegistry
|
|
78
79
|
#pendingMessages
|
|
80
|
+
#replStream
|
|
81
|
+
#lastELU
|
|
79
82
|
|
|
80
83
|
constructor (executable) {
|
|
81
84
|
super({
|
|
@@ -91,6 +94,22 @@ export class ChildProcess extends ITC {
|
|
|
91
94
|
getMetrics: (...args) => {
|
|
92
95
|
return this.#getMetrics(...args)
|
|
93
96
|
},
|
|
97
|
+
startRepl: () => {
|
|
98
|
+
return this.#startRepl()
|
|
99
|
+
},
|
|
100
|
+
replInput: ({ data }) => {
|
|
101
|
+
return this.#replInput(data)
|
|
102
|
+
},
|
|
103
|
+
replClose: () => {
|
|
104
|
+
return this.#replClose()
|
|
105
|
+
},
|
|
106
|
+
getHealth: () => {
|
|
107
|
+
return this.#getHealth()
|
|
108
|
+
},
|
|
109
|
+
sendHealthSignals: ({ workerId, signals }) => {
|
|
110
|
+
// Forward health signals to the parent (ChildManager)
|
|
111
|
+
this.notify('healthSignals', { workerId, signals })
|
|
112
|
+
},
|
|
94
113
|
close: signal => {
|
|
95
114
|
let handled = false
|
|
96
115
|
|
|
@@ -113,6 +132,10 @@ export class ChildProcess extends ITC {
|
|
|
113
132
|
}
|
|
114
133
|
|
|
115
134
|
return handled
|
|
135
|
+
},
|
|
136
|
+
setClosing: () => {
|
|
137
|
+
globalThis.platformatic.closing = true
|
|
138
|
+
globalThis.platformatic.events.emit('closing')
|
|
116
139
|
}
|
|
117
140
|
}
|
|
118
141
|
})
|
|
@@ -149,6 +172,9 @@ export class ChildProcess extends ITC {
|
|
|
149
172
|
prometheus: { client, registry: this.#metricsRegistry },
|
|
150
173
|
notifyConfig: this.#notifyConfig.bind(this)
|
|
151
174
|
})
|
|
175
|
+
|
|
176
|
+
// Initialize health signals API for child processes
|
|
177
|
+
this.#initHealthSignalsApi()
|
|
152
178
|
}
|
|
153
179
|
|
|
154
180
|
registerGlobals (globals) {
|
|
@@ -335,6 +361,118 @@ export class ChildProcess extends ITC {
|
|
|
335
361
|
return res
|
|
336
362
|
}
|
|
337
363
|
|
|
364
|
+
#startRepl () {
|
|
365
|
+
// Dynamically load node:repl to avoid loading it when not needed
|
|
366
|
+
// (since it pulls in domain, which is quite expensive as it monkey patches EventEmitter)
|
|
367
|
+
const repl = createRequire(import.meta.url)('node:repl')
|
|
368
|
+
|
|
369
|
+
// Create a duplex stream that sends output via notify
|
|
370
|
+
const replStream = new Duplex({
|
|
371
|
+
read () {},
|
|
372
|
+
write: (chunk, encoding, callback) => {
|
|
373
|
+
this.notify('repl:output', { data: chunk.toString() })
|
|
374
|
+
callback()
|
|
375
|
+
}
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
this.#replStream = replStream
|
|
379
|
+
|
|
380
|
+
// Start the REPL with the stream
|
|
381
|
+
const replServer = repl.start({
|
|
382
|
+
prompt: `${globalThis.platformatic.applicationId}> `,
|
|
383
|
+
input: replStream,
|
|
384
|
+
output: replStream,
|
|
385
|
+
terminal: false,
|
|
386
|
+
useColors: true,
|
|
387
|
+
ignoreUndefined: true,
|
|
388
|
+
preview: false
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
// Expose useful context - note that in subprocess mode, app/capability may not be available
|
|
392
|
+
replServer.context.platformatic = globalThis.platformatic
|
|
393
|
+
replServer.context.config = globalThis.platformatic.config
|
|
394
|
+
replServer.context.logger = globalThis.platformatic.logger
|
|
395
|
+
|
|
396
|
+
replServer.on('exit', () => {
|
|
397
|
+
this.notify('repl:exit', {})
|
|
398
|
+
this.#replStream = null
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
return { started: true }
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
#replInput (data) {
|
|
405
|
+
if (this.#replStream) {
|
|
406
|
+
this.#replStream.push(data)
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
#replClose () {
|
|
411
|
+
if (this.#replStream) {
|
|
412
|
+
this.#replStream.push(null)
|
|
413
|
+
this.#replStream = null
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
#getHealth () {
|
|
418
|
+
const currentELU = performance.eventLoopUtilization()
|
|
419
|
+
const elu = performance.eventLoopUtilization(currentELU, this.#lastELU).utilization
|
|
420
|
+
this.#lastELU = currentELU
|
|
421
|
+
|
|
422
|
+
const { heapUsed, heapTotal } = process.memoryUsage()
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
elu,
|
|
426
|
+
heapUsed,
|
|
427
|
+
heapTotal
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
#initHealthSignalsApi () {
|
|
432
|
+
const queue = []
|
|
433
|
+
let isSending = false
|
|
434
|
+
let promise = null
|
|
435
|
+
const timeout = 1000
|
|
436
|
+
|
|
437
|
+
const sendHealthSignal = async (signal) => {
|
|
438
|
+
if (typeof signal !== 'object') {
|
|
439
|
+
throw new Error('Health signal must be an object')
|
|
440
|
+
}
|
|
441
|
+
if (typeof signal.type !== 'string') {
|
|
442
|
+
throw new Error('Health signal type must be a string')
|
|
443
|
+
}
|
|
444
|
+
if (!signal.timestamp || typeof signal.timestamp !== 'number') {
|
|
445
|
+
signal.timestamp = Date.now()
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
queue.push(signal)
|
|
449
|
+
|
|
450
|
+
if (!isSending) {
|
|
451
|
+
isSending = true
|
|
452
|
+
promise = new Promise((resolve, reject) => {
|
|
453
|
+
setTimeout(async () => {
|
|
454
|
+
isSending = false
|
|
455
|
+
try {
|
|
456
|
+
const signals = queue.splice(0)
|
|
457
|
+
this.notify('healthSignals', {
|
|
458
|
+
workerId: globalThis.platformatic.workerId,
|
|
459
|
+
signals
|
|
460
|
+
})
|
|
461
|
+
} catch (err) {
|
|
462
|
+
reject(err)
|
|
463
|
+
return
|
|
464
|
+
}
|
|
465
|
+
resolve()
|
|
466
|
+
}, timeout)
|
|
467
|
+
})
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return promise
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
globalThis.platformatic.sendHealthSignal = sendHealthSignal
|
|
474
|
+
}
|
|
475
|
+
|
|
338
476
|
#setupLogger () {
|
|
339
477
|
disablePinoDirectWrite()
|
|
340
478
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/basic",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.34.1-alpha.3",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
"split2": "^4.2.0",
|
|
26
26
|
"undici": "^7.0.0",
|
|
27
27
|
"ws": "^8.18.0",
|
|
28
|
-
"@platformatic/foundation": "3.
|
|
29
|
-
"@platformatic/itc": "3.
|
|
30
|
-
"@platformatic/metrics": "3.
|
|
31
|
-
"@platformatic/telemetry": "3.
|
|
28
|
+
"@platformatic/foundation": "3.34.1-alpha.3",
|
|
29
|
+
"@platformatic/itc": "3.34.1-alpha.3",
|
|
30
|
+
"@platformatic/metrics": "3.34.1-alpha.3",
|
|
31
|
+
"@platformatic/telemetry": "3.34.1-alpha.3"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"cleaner-spec-reporter": "^0.5.0",
|
package/schema.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://schemas.platformatic.dev/@platformatic/basic/3.
|
|
2
|
+
"$id": "https://schemas.platformatic.dev/@platformatic/basic/3.34.1-alpha.3.json",
|
|
3
3
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
4
|
"title": "Platformatic Basic Config",
|
|
5
5
|
"type": "object",
|
|
@@ -753,6 +753,11 @@
|
|
|
753
753
|
}
|
|
754
754
|
],
|
|
755
755
|
"default": 10000
|
|
756
|
+
},
|
|
757
|
+
"closeConnections": {
|
|
758
|
+
"type": "boolean",
|
|
759
|
+
"default": true,
|
|
760
|
+
"description": "Add Connection: close header to HTTP responses during graceful shutdown"
|
|
756
761
|
}
|
|
757
762
|
},
|
|
758
763
|
"default": {},
|
|
@@ -1293,6 +1298,32 @@
|
|
|
1293
1298
|
"endpoint"
|
|
1294
1299
|
],
|
|
1295
1300
|
"additionalProperties": false
|
|
1301
|
+
},
|
|
1302
|
+
"httpCustomLabels": {
|
|
1303
|
+
"type": "array",
|
|
1304
|
+
"description": "Custom labels to add to HTTP metrics (http_request_all_duration_seconds). Each label extracts its value from an HTTP request header.",
|
|
1305
|
+
"items": {
|
|
1306
|
+
"type": "object",
|
|
1307
|
+
"properties": {
|
|
1308
|
+
"name": {
|
|
1309
|
+
"type": "string",
|
|
1310
|
+
"description": "The label name to use in metrics"
|
|
1311
|
+
},
|
|
1312
|
+
"header": {
|
|
1313
|
+
"type": "string",
|
|
1314
|
+
"description": "The HTTP request header to extract the value from"
|
|
1315
|
+
},
|
|
1316
|
+
"default": {
|
|
1317
|
+
"type": "string",
|
|
1318
|
+
"description": "Default value when header is missing (defaults to \"unknown\")"
|
|
1319
|
+
}
|
|
1320
|
+
},
|
|
1321
|
+
"required": [
|
|
1322
|
+
"name",
|
|
1323
|
+
"header"
|
|
1324
|
+
],
|
|
1325
|
+
"additionalProperties": false
|
|
1326
|
+
}
|
|
1296
1327
|
}
|
|
1297
1328
|
},
|
|
1298
1329
|
"additionalProperties": false
|