@platformatic/runtime 3.62.1 → 3.62.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 +4 -0
- package/lib/runtime.js +57 -15
- package/lib/worker/controller.js +4 -2
- package/lib/worker/itc.js +28 -14
- package/lib/worker/main.js +14 -4
- package/lib/worker/messaging.js +20 -7
- package/package.json +17 -17
- package/schema.json +12 -1
package/config.d.ts
CHANGED
|
@@ -696,6 +696,10 @@ export type PlatformaticRuntimeConfig = {
|
|
|
696
696
|
[k: string]: string;
|
|
697
697
|
};
|
|
698
698
|
envfile?: string;
|
|
699
|
+
/**
|
|
700
|
+
* When set to true, the configuration loading fails if a {PLT_*} placeholder references an environment variable which is not set. When set to "warn", a warning listing the missing variables is logged but the placeholders are still replaced with an empty string. Defaults to false.
|
|
701
|
+
*/
|
|
702
|
+
strictEnv?: boolean | string;
|
|
699
703
|
sourceMaps?: boolean;
|
|
700
704
|
nodeModulesSourceMaps?: string[];
|
|
701
705
|
scheduler?: {
|
package/lib/runtime.js
CHANGED
|
@@ -1421,20 +1421,6 @@ export class Runtime extends EventEmitter {
|
|
|
1421
1421
|
if (applicationMetrics && applicationMetrics !== kTimeout) {
|
|
1422
1422
|
metrics ??= []
|
|
1423
1423
|
|
|
1424
|
-
// Build worker labels including custom labels from metrics config
|
|
1425
|
-
const workerLabels = {
|
|
1426
|
-
...this.#config.metrics?.labels,
|
|
1427
|
-
[this.#metricsLabelName]: worker[kApplicationId]
|
|
1428
|
-
}
|
|
1429
|
-
const workerId = worker[kWorkerId]
|
|
1430
|
-
if (workerId >= 0) {
|
|
1431
|
-
workerLabels.workerId = workerId
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
// Duplicate process metrics with worker labels and add to output
|
|
1435
|
-
if (processMetricsJson) {
|
|
1436
|
-
this.#applyLabelsToMetrics(processMetricsJson, workerLabels, metrics)
|
|
1437
|
-
}
|
|
1438
1424
|
// Add worker's thread-specific metrics
|
|
1439
1425
|
for (let i = 0; i < applicationMetrics.length; i++) {
|
|
1440
1426
|
metrics.push(applicationMetrics[i])
|
|
@@ -1454,6 +1440,24 @@ export class Runtime extends EventEmitter {
|
|
|
1454
1440
|
}
|
|
1455
1441
|
}
|
|
1456
1442
|
|
|
1443
|
+
// Report process-level metrics (e.g. process_resident_memory_bytes) only once:
|
|
1444
|
+
// they describe the whole runtime process, so replicating them for each
|
|
1445
|
+
// application running in a worker thread would just duplicate the same value.
|
|
1446
|
+
// Applications running as separate OS processes report their own process-level
|
|
1447
|
+
// metrics, with their own labels, as part of their thread metrics above.
|
|
1448
|
+
// See https://github.com/platformatic/platformatic/issues/3332.
|
|
1449
|
+
if (metrics !== null && processMetricsJson) {
|
|
1450
|
+
const processMetrics = []
|
|
1451
|
+
// Drop any configured custom label that shares the name of the application
|
|
1452
|
+
// label (a config can set both `applicationLabel: 'serviceId'` and a static
|
|
1453
|
+
// `serviceId` label): keeping it would make these runtime-wide metrics look
|
|
1454
|
+
// like they belong to an application, both here and in getFormattedMetrics().
|
|
1455
|
+
const processLabels = { ...this.#config.metrics?.labels }
|
|
1456
|
+
delete processLabels[this.#metricsLabelName]
|
|
1457
|
+
this.#applyLabelsToMetrics(processMetricsJson, processLabels, processMetrics)
|
|
1458
|
+
metrics = [...processMetrics, ...metrics]
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1457
1461
|
if (metrics !== null && applicationRestartMetrics.length > 0) {
|
|
1458
1462
|
metrics = [...applicationRestartMetrics, ...metrics]
|
|
1459
1463
|
}
|
|
@@ -1581,6 +1585,13 @@ export class Runtime extends EventEmitter {
|
|
|
1581
1585
|
|
|
1582
1586
|
const applicationsMetrics = {}
|
|
1583
1587
|
|
|
1588
|
+
// Process-level metrics are reported only once for the whole runtime, without
|
|
1589
|
+
// an application label, since they are shared by all the applications running
|
|
1590
|
+
// in worker threads (see issue #3332). Applications running as separate OS
|
|
1591
|
+
// processes report their own labeled values, which take precedence below.
|
|
1592
|
+
const runtimeProcessMetrics = {}
|
|
1593
|
+
const applicationProcessMetrics = new Set()
|
|
1594
|
+
|
|
1584
1595
|
for (const metric of metrics) {
|
|
1585
1596
|
const { name, values } = metric
|
|
1586
1597
|
|
|
@@ -1592,9 +1603,22 @@ export class Runtime extends EventEmitter {
|
|
|
1592
1603
|
const applicationId = labels?.[this.#metricsLabelName]
|
|
1593
1604
|
|
|
1594
1605
|
if (!applicationId) {
|
|
1606
|
+
if (name === 'process_cpu_percent_usage') {
|
|
1607
|
+
runtimeProcessMetrics.cpu = values[0].value
|
|
1608
|
+
continue
|
|
1609
|
+
}
|
|
1610
|
+
if (name === 'process_resident_memory_bytes') {
|
|
1611
|
+
runtimeProcessMetrics.rss = values[0].value
|
|
1612
|
+
continue
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1595
1615
|
throw new Error(`Missing ${this.#metricsLabelName} label in metrics`)
|
|
1596
1616
|
}
|
|
1597
1617
|
|
|
1618
|
+
if (name === 'process_cpu_percent_usage' || name === 'process_resident_memory_bytes') {
|
|
1619
|
+
applicationProcessMetrics.add(`${applicationId}:${name}`)
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1598
1622
|
let applicationMetrics = applicationsMetrics[applicationId]
|
|
1599
1623
|
if (!applicationMetrics) {
|
|
1600
1624
|
applicationMetrics = {
|
|
@@ -1618,6 +1642,24 @@ export class Runtime extends EventEmitter {
|
|
|
1618
1642
|
parsePromMetric(applicationMetrics, metric)
|
|
1619
1643
|
}
|
|
1620
1644
|
|
|
1645
|
+
// Apply the runtime-wide process-level values to every application that did
|
|
1646
|
+
// not report its own (i.e. every application running in a worker thread).
|
|
1647
|
+
for (const [applicationId, applicationMetrics] of Object.entries(applicationsMetrics)) {
|
|
1648
|
+
if (
|
|
1649
|
+
runtimeProcessMetrics.cpu !== undefined &&
|
|
1650
|
+
!applicationProcessMetrics.has(`${applicationId}:process_cpu_percent_usage`)
|
|
1651
|
+
) {
|
|
1652
|
+
applicationMetrics.cpu = runtimeProcessMetrics.cpu
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
if (
|
|
1656
|
+
runtimeProcessMetrics.rss !== undefined &&
|
|
1657
|
+
!applicationProcessMetrics.has(`${applicationId}:process_resident_memory_bytes`)
|
|
1658
|
+
) {
|
|
1659
|
+
applicationMetrics.rss = runtimeProcessMetrics.rss
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1621
1663
|
function parsePromMetric (applicationMetrics, promMetric) {
|
|
1622
1664
|
const { name } = promMetric
|
|
1623
1665
|
|
|
@@ -2961,7 +3003,7 @@ export class Runtime extends EventEmitter {
|
|
|
2961
3003
|
await executeWithTimeout(sendViaITC(worker, 'stop', { force: !!this.error, dependents }), exitTimeout)
|
|
2962
3004
|
} catch (error) {
|
|
2963
3005
|
this.emitAndNotify('application:worker:stop:error', eventPayload)
|
|
2964
|
-
this.logger.
|
|
3006
|
+
this.logger.error({ err: ensureLoggableError(error) }, `Failed to stop ${label}. Killing a worker thread.`)
|
|
2965
3007
|
} finally {
|
|
2966
3008
|
worker[kITC].notify('application:worker:stop:processed')
|
|
2967
3009
|
// Wait for the processed message to be received
|
package/lib/worker/controller.js
CHANGED
|
@@ -95,7 +95,8 @@ export class Controller extends EventEmitter {
|
|
|
95
95
|
worker: workerData?.worker,
|
|
96
96
|
resourceLimits: workerData?.resourceLimits,
|
|
97
97
|
hasManagementApi: !!runtimeConfig.managementApi,
|
|
98
|
-
fetchApplicationUrl: fetchApplicationUrl.bind(null, applicationConfig)
|
|
98
|
+
fetchApplicationUrl: fetchApplicationUrl.bind(null, applicationConfig),
|
|
99
|
+
strictEnv: runtimeConfig.strictEnv
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
102
|
|
|
@@ -141,7 +142,8 @@ export class Controller extends EventEmitter {
|
|
|
141
142
|
if (appConfig.config) {
|
|
142
143
|
// Parse the configuration file the first time to obtain the schema
|
|
143
144
|
const unvalidatedConfig = await loadConfiguration(appConfig.config, null, {
|
|
144
|
-
onMissingEnv: this.#context.fetchApplicationUrl
|
|
145
|
+
onMissingEnv: this.#context.fetchApplicationUrl,
|
|
146
|
+
strictEnv: this.#context.strictEnv
|
|
145
147
|
})
|
|
146
148
|
const pkg = await loadConfigurationModule(appConfig.path, unvalidatedConfig)
|
|
147
149
|
this.capability = await pkg.create(appConfig.path, appConfig.config, this.#context)
|
package/lib/worker/itc.js
CHANGED
|
@@ -183,7 +183,12 @@ export async function setupITC (controller, application, dispatcher, sharedConte
|
|
|
183
183
|
|
|
184
184
|
// Reply to the runtime that the start failed, so it can cleanup
|
|
185
185
|
once(itc, 'application:worker:start:processed').then(() => {
|
|
186
|
-
closeITC(dispatcher, itc, messaging).catch(
|
|
186
|
+
closeITC(dispatcher, itc, messaging).catch(err => {
|
|
187
|
+
logger.error(
|
|
188
|
+
{ err: ensureLoggableError(err) },
|
|
189
|
+
'Failed to close the worker ITC after a failed start.'
|
|
190
|
+
)
|
|
191
|
+
})
|
|
187
192
|
})
|
|
188
193
|
|
|
189
194
|
throw ensureLoggableError(e)
|
|
@@ -199,23 +204,32 @@ export async function setupITC (controller, application, dispatcher, sharedConte
|
|
|
199
204
|
},
|
|
200
205
|
|
|
201
206
|
async stop ({ force, dependents }) {
|
|
202
|
-
|
|
207
|
+
try {
|
|
208
|
+
const status = controller.getStatus()
|
|
203
209
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
210
|
+
if (!force && status === 'starting') {
|
|
211
|
+
await once(controller, 'start')
|
|
212
|
+
}
|
|
207
213
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
214
|
+
if (force || status.startsWith('start')) {
|
|
215
|
+
// This gives a chance to a capability to perform custom logic
|
|
216
|
+
const events = getEvents()
|
|
217
|
+
events.emit('stop')
|
|
212
218
|
|
|
213
|
-
|
|
219
|
+
await controller.stop(force, dependents)
|
|
220
|
+
}
|
|
221
|
+
} finally {
|
|
222
|
+
// Always schedule cleanup, even when stop throws. Otherwise the worker
|
|
223
|
+
// keeps open handles and runtime shutdown hangs until the grace timeout.
|
|
224
|
+
once(itc, 'application:worker:stop:processed').then(() => {
|
|
225
|
+
closeITC(dispatcher, itc, messaging).catch(err => {
|
|
226
|
+
logger.error(
|
|
227
|
+
{ err: ensureLoggableError(err) },
|
|
228
|
+
'Failed to close the worker ITC after stop.'
|
|
229
|
+
)
|
|
230
|
+
})
|
|
231
|
+
})
|
|
214
232
|
}
|
|
215
|
-
|
|
216
|
-
once(itc, 'application:worker:stop:processed').then(() => {
|
|
217
|
-
closeITC(dispatcher, itc, messaging).catch(() => {})
|
|
218
|
-
})
|
|
219
233
|
},
|
|
220
234
|
|
|
221
235
|
async getDependencies () {
|
package/lib/worker/main.js
CHANGED
|
@@ -364,6 +364,16 @@ async function main () {
|
|
|
364
364
|
|
|
365
365
|
function stripBasePath (basePath) {
|
|
366
366
|
const kBasePath = Symbol('kBasePath')
|
|
367
|
+
const absoluteUrlPattern = /^https?:\/\//i
|
|
368
|
+
|
|
369
|
+
function prependBasePath (value) {
|
|
370
|
+
// Absolute and protocol-relative URLs (RFC 7231 Location) must not be rewritten
|
|
371
|
+
if (typeof value !== 'string' || absoluteUrlPattern.test(value) || value.startsWith('//') || value.startsWith(basePath)) {
|
|
372
|
+
return value
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return basePath + value
|
|
376
|
+
}
|
|
367
377
|
|
|
368
378
|
subscribe('http.server.request.start', ({ request, response }) => {
|
|
369
379
|
if (request.url.startsWith(basePath)) {
|
|
@@ -389,8 +399,8 @@ function stripBasePath (basePath) {
|
|
|
389
399
|
|
|
390
400
|
if (headers) {
|
|
391
401
|
for (const key in headers) {
|
|
392
|
-
if (key.toLowerCase() === 'location'
|
|
393
|
-
headers[key] =
|
|
402
|
+
if (key.toLowerCase() === 'location') {
|
|
403
|
+
headers[key] = prependBasePath(headers[key])
|
|
394
404
|
}
|
|
395
405
|
}
|
|
396
406
|
}
|
|
@@ -401,8 +411,8 @@ function stripBasePath (basePath) {
|
|
|
401
411
|
|
|
402
412
|
ServerResponse.prototype.setHeader = function (name, value) {
|
|
403
413
|
if (this[kBasePath]) {
|
|
404
|
-
if (name.toLowerCase() === 'location'
|
|
405
|
-
value =
|
|
414
|
+
if (name.toLowerCase() === 'location') {
|
|
415
|
+
value = prependBasePath(value)
|
|
406
416
|
}
|
|
407
417
|
}
|
|
408
418
|
originSetHeader.call(this, name, value)
|
package/lib/worker/messaging.js
CHANGED
|
@@ -114,14 +114,21 @@ export class MessagingITC extends ITC {
|
|
|
114
114
|
context.meta = { ...options?.meta, ...telemetry?.meta }
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
117
|
+
const abortController = new AbortController()
|
|
118
|
+
const timeout = setTimeout(() => abortController.abort(), this.#timeout).unref()
|
|
119
|
+
context.signal = abortController.signal
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const send = () => super.send(name, message, context)
|
|
123
|
+
return telemetry ? await telemetry.run(send) : await send()
|
|
124
|
+
} catch (err) {
|
|
125
|
+
if (abortController.signal.aborted) {
|
|
126
|
+
throw new MessagingError(application, 'Timeout while waiting for a response.')
|
|
127
|
+
}
|
|
128
|
+
throw err
|
|
129
|
+
} finally {
|
|
130
|
+
clearTimeout(timeout)
|
|
122
131
|
}
|
|
123
|
-
|
|
124
|
-
return response
|
|
125
132
|
} catch (err) {
|
|
126
133
|
error = err
|
|
127
134
|
throw err
|
|
@@ -181,6 +188,12 @@ export class MessagingITC extends ITC {
|
|
|
181
188
|
channel.postMessage(sanitize(request, transferList), transferList)
|
|
182
189
|
}
|
|
183
190
|
|
|
191
|
+
_onRequestSettled (reqId, context) {
|
|
192
|
+
if (context.trackResponse) {
|
|
193
|
+
context.channel[kPendingResponses].delete(reqId)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
184
197
|
_createClosePromise () {
|
|
185
198
|
const { promise, resolve, reject } = Promise.withResolvers()
|
|
186
199
|
this.#closeResolvers = { resolve, reject }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/runtime",
|
|
3
|
-
"version": "3.62.
|
|
3
|
+
"version": "3.62.3",
|
|
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/composer": "3.62.
|
|
43
|
-
"@platformatic/db": "3.62.
|
|
44
|
-
"@platformatic/gateway": "3.62.
|
|
45
|
-
"@platformatic/node": "3.62.
|
|
46
|
-
"@platformatic/service": "3.62.
|
|
47
|
-
"@platformatic/sql-mapper": "3.62.
|
|
48
|
-
"@platformatic/
|
|
49
|
-
"@platformatic/
|
|
42
|
+
"@platformatic/composer": "3.62.3",
|
|
43
|
+
"@platformatic/db": "3.62.3",
|
|
44
|
+
"@platformatic/gateway": "3.62.3",
|
|
45
|
+
"@platformatic/node": "3.62.3",
|
|
46
|
+
"@platformatic/service": "3.62.3",
|
|
47
|
+
"@platformatic/sql-mapper": "3.62.3",
|
|
48
|
+
"@platformatic/wattpm-pprof-capture": "3.62.3",
|
|
49
|
+
"@platformatic/sql-graphql": "3.62.3"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@fastify/accepts": "^5.0.0",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"help-me": "^5.0.0",
|
|
70
70
|
"minimist": "^1.2.8",
|
|
71
71
|
"pino": "^10.1.0",
|
|
72
|
-
"pino-opentelemetry-transport": "^
|
|
72
|
+
"pino-opentelemetry-transport": "^4.0.2",
|
|
73
73
|
"pino-pretty": "^13.0.0",
|
|
74
74
|
"semgrator": "^0.3.0",
|
|
75
75
|
"sonic-boom": "^4.2.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/basic": "3.62.
|
|
81
|
-
"@platformatic/foundation": "3.62.
|
|
82
|
-
"@platformatic/generators": "3.62.
|
|
83
|
-
"@platformatic/globals": "3.62.
|
|
84
|
-
"@platformatic/
|
|
85
|
-
"@platformatic/telemetry": "3.62.
|
|
86
|
-
"@platformatic/
|
|
80
|
+
"@platformatic/basic": "3.62.3",
|
|
81
|
+
"@platformatic/foundation": "3.62.3",
|
|
82
|
+
"@platformatic/generators": "3.62.3",
|
|
83
|
+
"@platformatic/globals": "3.62.3",
|
|
84
|
+
"@platformatic/metrics": "3.62.3",
|
|
85
|
+
"@platformatic/telemetry": "3.62.3",
|
|
86
|
+
"@platformatic/itc": "3.62.3"
|
|
87
87
|
},
|
|
88
88
|
"engines": {
|
|
89
89
|
"node": ">=22.19.0"
|
package/schema.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.62.
|
|
2
|
+
"$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.62.3.json",
|
|
3
3
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
4
|
"title": "Platformatic Runtime Config",
|
|
5
5
|
"type": "object",
|
|
@@ -3492,6 +3492,17 @@
|
|
|
3492
3492
|
"envfile": {
|
|
3493
3493
|
"type": "string"
|
|
3494
3494
|
},
|
|
3495
|
+
"strictEnv": {
|
|
3496
|
+
"anyOf": [
|
|
3497
|
+
{
|
|
3498
|
+
"type": "boolean"
|
|
3499
|
+
},
|
|
3500
|
+
{
|
|
3501
|
+
"type": "string"
|
|
3502
|
+
}
|
|
3503
|
+
],
|
|
3504
|
+
"description": "When set to true, the configuration loading fails if a {PLT_*} placeholder references an environment variable which is not set. When set to \"warn\", a warning listing the missing variables is logged but the placeholders are still replaced with an empty string. Defaults to false."
|
|
3505
|
+
},
|
|
3495
3506
|
"sourceMaps": {
|
|
3496
3507
|
"type": "boolean",
|
|
3497
3508
|
"default": false
|