@platformatic/runtime 3.57.0 → 3.58.1
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 +2 -1
- package/lib/generator.js +3 -2
- package/lib/logger.js +10 -1
- package/lib/prom-server.js +30 -49
- package/lib/runtime.js +71 -37
- package/lib/worker/controller.js +42 -22
- package/lib/worker/main.js +22 -16
- package/package.json +20 -18
- package/schema.json +20 -2
package/config.d.ts
CHANGED
|
@@ -221,7 +221,7 @@ export type PlatformaticRuntimeConfig = {
|
|
|
221
221
|
reuseTcpPorts?: boolean;
|
|
222
222
|
startTimeout?: number;
|
|
223
223
|
restartOnError?: boolean | number;
|
|
224
|
-
exitOnUnhandledErrors?: boolean;
|
|
224
|
+
exitOnUnhandledErrors?: boolean | number;
|
|
225
225
|
gracefulShutdown?: {
|
|
226
226
|
runtime: number | string;
|
|
227
227
|
application: number | string;
|
|
@@ -243,6 +243,7 @@ export type PlatformaticRuntimeConfig = {
|
|
|
243
243
|
bufferPoolSize?: number | string;
|
|
244
244
|
defaultHighWaterMark?: number | string;
|
|
245
245
|
};
|
|
246
|
+
healthProbes?: boolean | string;
|
|
246
247
|
undici?: {
|
|
247
248
|
agentOptions?: {
|
|
248
249
|
[k: string]: unknown;
|
package/lib/generator.js
CHANGED
|
@@ -268,8 +268,9 @@ export class RuntimeGenerator extends BaseGenerator {
|
|
|
268
268
|
this.questions.push({
|
|
269
269
|
type: 'input',
|
|
270
270
|
name: 'port',
|
|
271
|
-
default: 3042,
|
|
272
|
-
message: 'What port do you want to use?'
|
|
271
|
+
default: '3042',
|
|
272
|
+
message: 'What port do you want to use?',
|
|
273
|
+
filter: Number
|
|
273
274
|
})
|
|
274
275
|
}
|
|
275
276
|
}
|
package/lib/logger.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
buildPinoFormatters,
|
|
3
|
+
buildPinoTimestamp,
|
|
4
|
+
} from '@platformatic/foundation'
|
|
2
5
|
import { isatty } from 'node:tty'
|
|
3
6
|
import pino from 'pino'
|
|
4
7
|
import pretty from 'pino-pretty'
|
|
@@ -158,6 +161,12 @@ export async function createLogger (config) {
|
|
|
158
161
|
let cliStream
|
|
159
162
|
|
|
160
163
|
if (config.logger.transport) {
|
|
164
|
+
if (Array.isArray(config.logger.transport.targets)) {
|
|
165
|
+
for (const target of config.logger.transport.targets) {
|
|
166
|
+
target.level ??= config.logger.level
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
161
170
|
cliStream = pino.transport(config.logger.transport)
|
|
162
171
|
} else if ((process.env.FORCE_TTY || isatty(1)) && process.env.PLT_PRETTY_PRINT !== 'false') {
|
|
163
172
|
cliStream = createPrettifier(context)
|
package/lib/prom-server.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import fastifyAccepts from '@fastify/accepts'
|
|
2
2
|
import fastifyBasicAuth from '@fastify/basic-auth'
|
|
3
|
-
import { loadModule } from '@platformatic/foundation'
|
|
3
|
+
import { loadModule, sanitizeHTTPSOptions } from '@platformatic/foundation'
|
|
4
4
|
import fastify from 'fastify'
|
|
5
|
-
import { readFile } from 'node:fs/promises'
|
|
6
5
|
import { createRequire } from 'node:module'
|
|
7
6
|
import { resolve } from 'node:path'
|
|
8
7
|
|
|
@@ -53,33 +52,6 @@ async function checkReadiness (runtime) {
|
|
|
53
52
|
return { response, status }
|
|
54
53
|
}
|
|
55
54
|
|
|
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
|
-
|
|
83
55
|
async function checkLiveness (runtime) {
|
|
84
56
|
const { status: ready, response: readinessResponse } = await checkReadiness(runtime)
|
|
85
57
|
if (!ready) {
|
|
@@ -104,8 +76,11 @@ async function checkLiveness (runtime) {
|
|
|
104
76
|
return { response, status }
|
|
105
77
|
}
|
|
106
78
|
|
|
107
|
-
export async function startPrometheusServer (runtime, opts) {
|
|
108
|
-
|
|
79
|
+
export async function startPrometheusServer (runtime, opts, healthProbes) {
|
|
80
|
+
const metricsEnabled = opts.enabled !== false
|
|
81
|
+
const healthProbesEnabled = healthProbes !== false
|
|
82
|
+
|
|
83
|
+
if (!metricsEnabled && !healthProbesEnabled) {
|
|
109
84
|
return
|
|
110
85
|
}
|
|
111
86
|
const host = opts.hostname ?? DEFAULT_HOSTNAME
|
|
@@ -141,13 +116,17 @@ export async function startPrometheusServer (runtime, opts) {
|
|
|
141
116
|
logLevel: 'warn',
|
|
142
117
|
handler (req, reply) {
|
|
143
118
|
reply.type('text/plain')
|
|
144
|
-
let response =
|
|
119
|
+
let response = 'Hello from Platformatic Prometheus Server!'
|
|
145
120
|
|
|
146
|
-
if (
|
|
121
|
+
if (metricsEnabled) {
|
|
122
|
+
response += `\nThe metrics are available at ${metricsEndpoint}.`
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (healthProbesEnabled && opts.readiness !== false) {
|
|
147
126
|
response += `\nThe readiness endpoint is available at ${readinessEndpoint}.`
|
|
148
127
|
}
|
|
149
128
|
|
|
150
|
-
if (opts.liveness !== false) {
|
|
129
|
+
if (healthProbesEnabled && opts.liveness !== false) {
|
|
151
130
|
response += `\nThe liveness endpoint is available at ${livenessEndpoint}.`
|
|
152
131
|
}
|
|
153
132
|
|
|
@@ -155,22 +134,24 @@ export async function startPrometheusServer (runtime, opts) {
|
|
|
155
134
|
}
|
|
156
135
|
})
|
|
157
136
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
137
|
+
if (metricsEnabled) {
|
|
138
|
+
promServer.route({
|
|
139
|
+
url: metricsEndpoint,
|
|
140
|
+
method: 'GET',
|
|
141
|
+
logLevel: 'warn',
|
|
142
|
+
onRequest: onRequestHook,
|
|
143
|
+
handler: async (req, reply) => {
|
|
144
|
+
const accepts = req.accepts()
|
|
145
|
+
const reqType = !accepts.type('text/plain') && accepts.type('application/json') ? 'json' : 'text'
|
|
146
|
+
if (reqType === 'text') {
|
|
147
|
+
reply.type('text/plain')
|
|
148
|
+
}
|
|
149
|
+
return (await runtime.getMetrics(reqType)).metrics
|
|
168
150
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
})
|
|
151
|
+
})
|
|
152
|
+
}
|
|
172
153
|
|
|
173
|
-
if (opts.readiness !== false) {
|
|
154
|
+
if (healthProbesEnabled && opts.readiness !== false) {
|
|
174
155
|
const successStatusCode = opts.readiness?.success?.statusCode ?? DEFAULT_READINESS_SUCCESS_STATUS_CODE
|
|
175
156
|
const successBody = opts.readiness?.success?.body ?? DEFAULT_READINESS_SUCCESS_BODY
|
|
176
157
|
const failStatusCode = opts.readiness?.fail?.statusCode ?? DEFAULT_READINESS_FAIL_STATUS_CODE
|
|
@@ -209,7 +190,7 @@ export async function startPrometheusServer (runtime, opts) {
|
|
|
209
190
|
})
|
|
210
191
|
}
|
|
211
192
|
|
|
212
|
-
if (opts.liveness !== false) {
|
|
193
|
+
if (healthProbesEnabled && opts.liveness !== false) {
|
|
213
194
|
const successStatusCode = opts.liveness?.success?.statusCode ?? DEFAULT_LIVENESS_SUCCESS_STATUS_CODE
|
|
214
195
|
const successBody = opts.liveness?.success?.body ?? DEFAULT_LIVENESS_SUCCESS_BODY
|
|
215
196
|
const failStatusCode = opts.liveness?.fail?.statusCode ?? DEFAULT_LIVENESS_FAIL_STATUS_CODE
|
package/lib/runtime.js
CHANGED
|
@@ -143,6 +143,7 @@ export class Runtime extends EventEmitter {
|
|
|
143
143
|
#restartingApplications
|
|
144
144
|
#restartingWorkers
|
|
145
145
|
#dynamicWorkersScaler
|
|
146
|
+
#nextWorkerIndex
|
|
146
147
|
|
|
147
148
|
#sharedHttpCache
|
|
148
149
|
#scheduler
|
|
@@ -178,6 +179,7 @@ export class Runtime extends EventEmitter {
|
|
|
178
179
|
this.#status = undefined
|
|
179
180
|
this.#restartingApplications = new Set()
|
|
180
181
|
this.#restartingWorkers = new Map()
|
|
182
|
+
this.#nextWorkerIndex = new Map()
|
|
181
183
|
this.#sharedHttpCache = null
|
|
182
184
|
this.#applicationsConfigsPatches = new Map()
|
|
183
185
|
|
|
@@ -227,7 +229,7 @@ export class Runtime extends EventEmitter {
|
|
|
227
229
|
if (config.metrics) {
|
|
228
230
|
// Use the configured application label name for metrics (defaults to 'applicationId')
|
|
229
231
|
this.#metricsLabelName = config.metrics.applicationLabel || 'applicationId'
|
|
230
|
-
this.#prometheusServer = await startPrometheusServer(this, config.metrics)
|
|
232
|
+
this.#prometheusServer = await startPrometheusServer(this, config.metrics, config.healthProbes)
|
|
231
233
|
} else {
|
|
232
234
|
// Default to applicationId if metrics are not configured
|
|
233
235
|
this.#metricsLabelName = 'applicationId'
|
|
@@ -578,10 +580,10 @@ export class Runtime extends EventEmitter {
|
|
|
578
580
|
}
|
|
579
581
|
|
|
580
582
|
async startApplications (applications, silent = false) {
|
|
581
|
-
// For each
|
|
583
|
+
// For each application, get its dependencies from any available worker.
|
|
582
584
|
const dependencies = new Map()
|
|
583
585
|
for (const applicationId of applications) {
|
|
584
|
-
const worker = await this.#getWorkerByIdOrNext(applicationId
|
|
586
|
+
const worker = await this.#getWorkerByIdOrNext(applicationId)
|
|
585
587
|
|
|
586
588
|
dependencies.set(applicationId, await sendViaITC(worker, 'getDependencies'))
|
|
587
589
|
}
|
|
@@ -728,12 +730,13 @@ export class Runtime extends EventEmitter {
|
|
|
728
730
|
for (let i = 0; i < workersCount; i++) {
|
|
729
731
|
const workerId = workersIds[i]
|
|
730
732
|
const worker = this.#workers.get(workerId)
|
|
733
|
+
const workerIndex = parseInt(workerId.split(':')[1], 10)
|
|
731
734
|
|
|
732
735
|
if (i > 0 && config.workersRestartDelay > 0) {
|
|
733
736
|
await sleep(config.workersRestartDelay)
|
|
734
737
|
}
|
|
735
738
|
|
|
736
|
-
await this.#replaceWorker(config, applicationConfig, workersCount, id,
|
|
739
|
+
await this.#replaceWorker(config, applicationConfig, workersCount, id, workerIndex, worker, true)
|
|
737
740
|
}
|
|
738
741
|
|
|
739
742
|
this.#incrementApplicationRestartCount(id)
|
|
@@ -855,8 +858,8 @@ export class Runtime extends EventEmitter {
|
|
|
855
858
|
this.#config.metrics = metricsConfig
|
|
856
859
|
this.#metricsLabelName = metricsConfig?.applicationLabel || 'applicationId'
|
|
857
860
|
|
|
858
|
-
if (metricsConfig.enabled !== false) {
|
|
859
|
-
this.#prometheusServer = await startPrometheusServer(this, metricsConfig)
|
|
861
|
+
if (metricsConfig.enabled !== false || this.#config.healthProbes !== false) {
|
|
862
|
+
this.#prometheusServer = await startPrometheusServer(this, metricsConfig, this.#config.healthProbes)
|
|
860
863
|
}
|
|
861
864
|
|
|
862
865
|
const promises = []
|
|
@@ -1491,7 +1494,8 @@ export class Runtime extends EventEmitter {
|
|
|
1491
1494
|
|
|
1492
1495
|
async getApplicationResourcesInfo (id) {
|
|
1493
1496
|
const workersCount = this.#workers.getKeys(id).length
|
|
1494
|
-
|
|
1497
|
+
// Use round-robin to get any available worker instead of assuming index 0 exists
|
|
1498
|
+
const worker = await this.#getWorkerByIdOrNext(id, null, false, false)
|
|
1495
1499
|
const health = worker[kConfig].health
|
|
1496
1500
|
|
|
1497
1501
|
return { workers: workersCount, health }
|
|
@@ -1779,6 +1783,9 @@ export class Runtime extends EventEmitter {
|
|
|
1779
1783
|
|
|
1780
1784
|
await executeInParallel(this.#setupWorker.bind(this), setupInvocations, this.#concurrency)
|
|
1781
1785
|
|
|
1786
|
+
// Initialize the next worker index counter (next index starts after initial workers)
|
|
1787
|
+
this.#nextWorkerIndex.set(id, workers)
|
|
1788
|
+
|
|
1782
1789
|
await this.#dynamicWorkersScaler?.add(applicationConfig)
|
|
1783
1790
|
this.emitAndNotify('application:init', id)
|
|
1784
1791
|
}
|
|
@@ -2025,8 +2032,9 @@ export class Runtime extends EventEmitter {
|
|
|
2025
2032
|
}
|
|
2026
2033
|
})
|
|
2027
2034
|
|
|
2028
|
-
// Only activate watch for the first instance
|
|
2029
|
-
|
|
2035
|
+
// Only activate watch for the first instance. Replacement workers get unique
|
|
2036
|
+
// indices, so preserve the listener when replacing a single-worker app.
|
|
2037
|
+
if (index === 0 || workersCount === 1) {
|
|
2030
2038
|
// Handle applications changes
|
|
2031
2039
|
// This is not purposely activated on when this.#config.watch === true
|
|
2032
2040
|
// so that applications can eventually manually trigger a restart. This mechanism is current
|
|
@@ -2556,10 +2564,17 @@ export class Runtime extends EventEmitter {
|
|
|
2556
2564
|
return `worker ${workerId} of the application "${applicationId}"`
|
|
2557
2565
|
}
|
|
2558
2566
|
|
|
2559
|
-
|
|
2560
|
-
const
|
|
2567
|
+
#getNextWorkerIndex (applicationId) {
|
|
2568
|
+
const index = this.#nextWorkerIndex.get(applicationId) ?? 0
|
|
2569
|
+
this.#nextWorkerIndex.set(applicationId, index + 1)
|
|
2570
|
+
return index
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
async #restartCrashedWorker (config, applicationConfig, workersCount, id, oldIndex, silent, bootstrapAttempt) {
|
|
2574
|
+
// Use oldIndex for tracking to prevent duplicate restarts of the same crashed worker
|
|
2575
|
+
const restartKey = `${id}:${oldIndex}`
|
|
2561
2576
|
|
|
2562
|
-
let restartPromise = this.#restartingWorkers.get(
|
|
2577
|
+
let restartPromise = this.#restartingWorkers.get(restartKey)
|
|
2563
2578
|
if (restartPromise) {
|
|
2564
2579
|
await restartPromise
|
|
2565
2580
|
return
|
|
@@ -2567,7 +2582,7 @@ export class Runtime extends EventEmitter {
|
|
|
2567
2582
|
|
|
2568
2583
|
restartPromise = new Promise((resolve, reject) => {
|
|
2569
2584
|
async function restart () {
|
|
2570
|
-
this.#restartingWorkers.delete(
|
|
2585
|
+
this.#restartingWorkers.delete(restartKey)
|
|
2571
2586
|
|
|
2572
2587
|
// If some processes were scheduled to restart
|
|
2573
2588
|
// but the runtime is stopped, ignore it
|
|
@@ -2575,6 +2590,9 @@ export class Runtime extends EventEmitter {
|
|
|
2575
2590
|
return
|
|
2576
2591
|
}
|
|
2577
2592
|
|
|
2593
|
+
// Get a new unique index for the restarted worker
|
|
2594
|
+
const index = this.#getNextWorkerIndex(id)
|
|
2595
|
+
|
|
2578
2596
|
try {
|
|
2579
2597
|
await this.#setupWorker(config, applicationConfig, workersCount, id, index)
|
|
2580
2598
|
await this.#startWorker(config, applicationConfig, workersCount, id, index, silent, bootstrapAttempt)
|
|
@@ -2601,15 +2619,19 @@ export class Runtime extends EventEmitter {
|
|
|
2601
2619
|
}
|
|
2602
2620
|
})
|
|
2603
2621
|
|
|
2604
|
-
this.#restartingWorkers.set(
|
|
2622
|
+
this.#restartingWorkers.set(restartKey, restartPromise)
|
|
2605
2623
|
await restartPromise
|
|
2606
2624
|
}
|
|
2607
2625
|
|
|
2608
|
-
async #replaceWorker (config, applicationConfig, workersCount, applicationId,
|
|
2609
|
-
const
|
|
2610
|
-
const label = this.#workerExtendedLabel(applicationId, index, workersCount)
|
|
2626
|
+
async #replaceWorker (config, applicationConfig, workersCount, applicationId, oldIndex, worker, silent) {
|
|
2627
|
+
const oldLabel = this.#workerExtendedLabel(applicationId, oldIndex, workersCount)
|
|
2611
2628
|
let newWorker
|
|
2612
2629
|
|
|
2630
|
+
// Get a new unique index for the replacement worker
|
|
2631
|
+
const newIndex = this.#getNextWorkerIndex(applicationId)
|
|
2632
|
+
const newWorkerId = `${applicationId}:${newIndex}`
|
|
2633
|
+
const newLabel = this.#workerExtendedLabel(applicationId, newIndex, workersCount)
|
|
2634
|
+
|
|
2613
2635
|
const stopBeforeStart =
|
|
2614
2636
|
applicationConfig.entrypoint &&
|
|
2615
2637
|
(config.reuseTcpPorts === false || applicationConfig.reuseTcpPorts === false || !features.node.reusePort)
|
|
@@ -2625,16 +2647,16 @@ export class Runtime extends EventEmitter {
|
|
|
2625
2647
|
}
|
|
2626
2648
|
|
|
2627
2649
|
if (stopBeforeStart) {
|
|
2628
|
-
await this.#removeWorker(workersCount, applicationId,
|
|
2650
|
+
await this.#removeWorker(workersCount, applicationId, oldIndex, worker, silent, oldLabel)
|
|
2629
2651
|
}
|
|
2630
2652
|
|
|
2631
2653
|
try {
|
|
2632
2654
|
if (!silent) {
|
|
2633
|
-
this.logger.debug(`Preparing to start
|
|
2655
|
+
this.logger.debug(`Preparing to start ${newLabel} as replacement for ${oldLabel} ...`)
|
|
2634
2656
|
}
|
|
2635
2657
|
|
|
2636
|
-
// Create a new worker
|
|
2637
|
-
newWorker = await this.#setupWorker(configForNewWorker, applicationConfig, workersCount, applicationId,
|
|
2658
|
+
// Create a new worker with a new index, preserving any pinned port when stopBeforeStart is required.
|
|
2659
|
+
newWorker = await this.#setupWorker(configForNewWorker, applicationConfig, workersCount, applicationId, newIndex, false)
|
|
2638
2660
|
|
|
2639
2661
|
// Make sure the runtime hasn't been stopped in the meanwhile
|
|
2640
2662
|
if (this.#status !== 'started') {
|
|
@@ -2642,21 +2664,21 @@ export class Runtime extends EventEmitter {
|
|
|
2642
2664
|
}
|
|
2643
2665
|
|
|
2644
2666
|
// Add the worker to the mesh
|
|
2645
|
-
await this.#startWorker(configForNewWorker, applicationConfig, workersCount, applicationId,
|
|
2667
|
+
await this.#startWorker(configForNewWorker, applicationConfig, workersCount, applicationId, newIndex, false, 0, newWorker, true)
|
|
2646
2668
|
|
|
2647
2669
|
// Make sure the runtime hasn't been stopped in the meanwhile
|
|
2648
2670
|
if (this.#status !== 'started') {
|
|
2649
2671
|
return this.#discardWorker(newWorker)
|
|
2650
2672
|
}
|
|
2651
2673
|
|
|
2652
|
-
this.#workers.set(
|
|
2674
|
+
this.#workers.set(newWorkerId, newWorker)
|
|
2653
2675
|
} catch (e) {
|
|
2654
2676
|
newWorker?.terminate?.()
|
|
2655
2677
|
throw e
|
|
2656
2678
|
}
|
|
2657
2679
|
|
|
2658
2680
|
if (!stopBeforeStart) {
|
|
2659
|
-
await this.#removeWorker(workersCount, applicationId,
|
|
2681
|
+
await this.#removeWorker(workersCount, applicationId, oldIndex, worker, silent, oldLabel)
|
|
2660
2682
|
}
|
|
2661
2683
|
}
|
|
2662
2684
|
|
|
@@ -3145,13 +3167,16 @@ export class Runtime extends EventEmitter {
|
|
|
3145
3167
|
await this.#updateApplicationConfigHealth(applicationId, health)
|
|
3146
3168
|
}
|
|
3147
3169
|
|
|
3148
|
-
|
|
3170
|
+
// Get actual worker keys to iterate over existing workers (snapshot to avoid mutation during iteration)
|
|
3171
|
+
const workerKeys = [...this.#workers.getKeys(applicationId)]
|
|
3172
|
+
for (const workerKey of workerKeys) {
|
|
3173
|
+
const workerIndex = parseInt(workerKey.split(':')[1], 10)
|
|
3149
3174
|
this.logger.info(
|
|
3150
3175
|
{ health: { current: currentHealth, new: health } },
|
|
3151
|
-
`Restarting application "${applicationId}" worker ${
|
|
3176
|
+
`Restarting application "${applicationId}" worker ${workerIndex} to update config health heap...`
|
|
3152
3177
|
)
|
|
3153
3178
|
|
|
3154
|
-
const worker =
|
|
3179
|
+
const worker = this.#workers.get(workerKey)
|
|
3155
3180
|
if (health.maxHeapTotal) {
|
|
3156
3181
|
worker[kConfig].health.maxHeapTotal = health.maxHeapTotal
|
|
3157
3182
|
}
|
|
@@ -3159,11 +3184,11 @@ export class Runtime extends EventEmitter {
|
|
|
3159
3184
|
worker[kConfig].health.maxYoungGeneration = health.maxYoungGeneration
|
|
3160
3185
|
}
|
|
3161
3186
|
|
|
3162
|
-
await this.#replaceWorker(config, applicationConfig, currentWorkers, applicationId,
|
|
3163
|
-
report.updated.push(
|
|
3187
|
+
await this.#replaceWorker(config, applicationConfig, currentWorkers, applicationId, workerIndex, worker)
|
|
3188
|
+
report.updated.push(workerIndex)
|
|
3164
3189
|
this.logger.info(
|
|
3165
3190
|
{ health: { current: currentHealth, new: health } },
|
|
3166
|
-
`Restarted application "${applicationId}" worker ${
|
|
3191
|
+
`Restarted application "${applicationId}" worker ${workerIndex}`
|
|
3167
3192
|
)
|
|
3168
3193
|
}
|
|
3169
3194
|
report.success = true
|
|
@@ -3196,9 +3221,10 @@ export class Runtime extends EventEmitter {
|
|
|
3196
3221
|
report.started = []
|
|
3197
3222
|
try {
|
|
3198
3223
|
for (let i = currentWorkers; i < workers; i++) {
|
|
3199
|
-
|
|
3200
|
-
await this.#
|
|
3201
|
-
|
|
3224
|
+
const newIndex = this.#getNextWorkerIndex(applicationId)
|
|
3225
|
+
await this.#setupWorker(config, applicationConfig, workers, applicationId, newIndex)
|
|
3226
|
+
await this.#startWorker(config, applicationConfig, workers, applicationId, newIndex, false, 0)
|
|
3227
|
+
report.started.push(newIndex)
|
|
3202
3228
|
startedWorkersCount++
|
|
3203
3229
|
}
|
|
3204
3230
|
report.success = true
|
|
@@ -3217,11 +3243,19 @@ export class Runtime extends EventEmitter {
|
|
|
3217
3243
|
// keep the current workers count until all the application workers are all stopped
|
|
3218
3244
|
report.stopped = []
|
|
3219
3245
|
try {
|
|
3220
|
-
|
|
3221
|
-
|
|
3246
|
+
// Get actual worker keys and sort by index descending to stop most recent first (snapshot to avoid mutation during iteration)
|
|
3247
|
+
const workerKeys = [...this.#workers.getKeys(applicationId)]
|
|
3248
|
+
const workerIndices = workerKeys
|
|
3249
|
+
.map(key => parseInt(key.split(':')[1], 10))
|
|
3250
|
+
.sort((a, b) => b - a) // descending order
|
|
3251
|
+
|
|
3252
|
+
const workersToStop = currentWorkers - workers
|
|
3253
|
+
for (let i = 0; i < workersToStop && i < workerIndices.length; i++) {
|
|
3254
|
+
const workerIndex = workerIndices[i]
|
|
3255
|
+
const worker = this.#workers.get(`${applicationId}:${workerIndex}`)
|
|
3222
3256
|
await sendViaITC(worker, 'removeFromMesh')
|
|
3223
|
-
await this.#stopWorker(currentWorkers, applicationId,
|
|
3224
|
-
report.stopped.push(
|
|
3257
|
+
await this.#stopWorker(currentWorkers, applicationId, workerIndex, false, worker, [])
|
|
3258
|
+
report.stopped.push(workerIndex)
|
|
3225
3259
|
stoppedWorkersCount++
|
|
3226
3260
|
}
|
|
3227
3261
|
report.success = true
|
package/lib/worker/controller.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ensureLoggableError,
|
|
3
|
-
executeWithTimeout,
|
|
4
3
|
FileWatcher,
|
|
5
4
|
kHandledError,
|
|
6
5
|
listRecognizedConfigurationFiles,
|
|
@@ -37,17 +36,24 @@ function fetchApplicationUrl (application, key) {
|
|
|
37
36
|
return getApplicationUrl(application.id)
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
function handleUnhandled (app,
|
|
39
|
+
function handleUnhandled (app, event, listeners, timeout, err, ...args) {
|
|
41
40
|
const label = `worker ${workerData.worker.index} of the application "${workerData.applicationConfig.id}"`
|
|
42
41
|
|
|
43
42
|
const logger = getLogger()
|
|
44
|
-
logger.error({ err: ensureLoggableError(err) }, `The ${label} threw an ${
|
|
43
|
+
logger.error({ err: ensureLoggableError(err) }, `The ${label} threw an ${event} event.`)
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
// Give some time to the listeners, logger and ITC notifications to land before shutting down
|
|
46
|
+
setTimeout(() => process.exit(1), timeout)
|
|
47
|
+
|
|
48
|
+
for (const listener of listeners) {
|
|
49
|
+
try {
|
|
50
|
+
listener(err, ...args)
|
|
51
|
+
} catch (err) {
|
|
52
|
+
logger.error({ err: ensureLoggableError(err) }, `${event} error listener failed.`)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
app.stop().catch()
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
export class Controller extends EventEmitter {
|
|
@@ -74,6 +80,8 @@ export class Controller extends EventEmitter {
|
|
|
74
80
|
|
|
75
81
|
this.#context = {
|
|
76
82
|
controller: this,
|
|
83
|
+
runtimeConfig: this.runtimeConfig,
|
|
84
|
+
applicationConfig: this.applicationConfig,
|
|
77
85
|
applicationId: this.applicationId,
|
|
78
86
|
workerId: this.workerId,
|
|
79
87
|
directory: this.applicationConfig.path,
|
|
@@ -149,16 +157,19 @@ export class Controller extends EventEmitter {
|
|
|
149
157
|
cleanupHandlers()
|
|
150
158
|
}
|
|
151
159
|
|
|
152
|
-
|
|
153
|
-
|
|
160
|
+
let exitOnUnhandledErrors = this.runtimeConfig.exitOnUnhandledErrors
|
|
161
|
+
|
|
162
|
+
if (exitOnUnhandledErrors === true || typeof exitOnUnhandledErrors === 'undefined') {
|
|
163
|
+
exitOnUnhandledErrors = 100
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (typeof exitOnUnhandledErrors === 'number' && exitOnUnhandledErrors > 0) {
|
|
167
|
+
this.#setupHandlers(exitOnUnhandledErrors)
|
|
154
168
|
}
|
|
155
169
|
} catch (err) {
|
|
156
170
|
if (err.validationErrors) {
|
|
157
171
|
const logger = getLogger()
|
|
158
|
-
logger.error(
|
|
159
|
-
{ err: ensureLoggableError(err) },
|
|
160
|
-
'The application threw a validation error.'
|
|
161
|
-
)
|
|
172
|
+
logger.error({ err: ensureLoggableError(err) }, 'The application threw a validation error.')
|
|
162
173
|
|
|
163
174
|
throw err
|
|
164
175
|
} else {
|
|
@@ -361,16 +372,25 @@ export class Controller extends EventEmitter {
|
|
|
361
372
|
mirrorGlobalDispatcherForBuiltinFetch(dispatcher)
|
|
362
373
|
}
|
|
363
374
|
|
|
364
|
-
#setupHandlers () {
|
|
365
|
-
|
|
366
|
-
|
|
375
|
+
#setupHandlers (timeout) {
|
|
376
|
+
const unhandledListeners = { uncaughtException: [], unhandledRejection: [] }
|
|
377
|
+
|
|
378
|
+
process.on(
|
|
379
|
+
'uncaughtException',
|
|
380
|
+
handleUnhandled.bind(null, this, 'uncaughtException', unhandledListeners.uncaughtException, timeout)
|
|
381
|
+
)
|
|
382
|
+
process.on(
|
|
383
|
+
'unhandledRejection',
|
|
384
|
+
handleUnhandled.bind(null, this, 'unhandledRejection', unhandledListeners.unhandledRejection, timeout)
|
|
385
|
+
)
|
|
367
386
|
|
|
368
|
-
process.on('newListener', event => {
|
|
387
|
+
process.on('newListener', (event, listener) => {
|
|
369
388
|
if (event === 'uncaughtException' || event === 'unhandledRejection') {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
389
|
+
unhandledListeners[event].push(listener)
|
|
390
|
+
|
|
391
|
+
process.nextTick(() => {
|
|
392
|
+
process.removeListener(event, listener)
|
|
393
|
+
})
|
|
374
394
|
}
|
|
375
395
|
})
|
|
376
396
|
}
|
package/lib/worker/main.js
CHANGED
|
@@ -28,14 +28,12 @@ import { setupITC } from './itc.js'
|
|
|
28
28
|
import { SharedContext } from './shared-context.js'
|
|
29
29
|
import { kStderrMarker } from './symbols.js'
|
|
30
30
|
|
|
31
|
-
function handlePreInitializationUnhandled (type, err) {
|
|
31
|
+
function handlePreInitializationUnhandled (type, timeout, err) {
|
|
32
32
|
const label = `worker ${workerData.worker.index} of the application "${workerData.applicationConfig.id}"`
|
|
33
33
|
const logger = getLogger()
|
|
34
|
-
logger.error(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
)
|
|
38
|
-
process.exit(exitCodes.PROCESS_UNHANDLED_ERROR)
|
|
34
|
+
logger.error({ err: ensureLoggableError(err) }, `The ${label} threw an ${type} before initialization.`)
|
|
35
|
+
|
|
36
|
+
setTimeout(() => process.exit(exitCodes.PROCESS_UNHANDLED_ERROR), timeout)
|
|
39
37
|
}
|
|
40
38
|
|
|
41
39
|
class ForwardingEventEmitter extends EventEmitter {
|
|
@@ -68,9 +66,9 @@ function patchLogging () {
|
|
|
68
66
|
}
|
|
69
67
|
}
|
|
70
68
|
|
|
71
|
-
function setupHandlers () {
|
|
72
|
-
const unhandledExceptionHandler = handlePreInitializationUnhandled.bind(null, 'uncaught exception')
|
|
73
|
-
const unhandledRejectionHandler = handlePreInitializationUnhandled.bind(null, 'uncaught rejection')
|
|
69
|
+
function setupHandlers (timeout) {
|
|
70
|
+
const unhandledExceptionHandler = handlePreInitializationUnhandled.bind(null, 'uncaught exception', timeout)
|
|
71
|
+
const unhandledRejectionHandler = handlePreInitializationUnhandled.bind(null, 'uncaught rejection', timeout)
|
|
74
72
|
process.on('uncaughtException', unhandledExceptionHandler)
|
|
75
73
|
process.on('unhandledRejection', unhandledRejectionHandler)
|
|
76
74
|
|
|
@@ -201,7 +199,16 @@ async function setupCompileCache (runtimeConfig, applicationConfig, logger) {
|
|
|
201
199
|
}
|
|
202
200
|
|
|
203
201
|
async function main () {
|
|
204
|
-
|
|
202
|
+
let cleanup = null
|
|
203
|
+
let exitOnUnhandledErrors = workerData.config.exitOnUnhandledErrors
|
|
204
|
+
|
|
205
|
+
if (exitOnUnhandledErrors === true || typeof exitOnUnhandledErrors === 'undefined') {
|
|
206
|
+
exitOnUnhandledErrors = 100
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (typeof exitOnUnhandledErrors === 'number' && exitOnUnhandledErrors > 0) {
|
|
210
|
+
cleanup = setupHandlers(exitOnUnhandledErrors)
|
|
211
|
+
}
|
|
205
212
|
|
|
206
213
|
installUndiciGlobals(globalThis)
|
|
207
214
|
updateGlobals({
|
|
@@ -319,15 +326,14 @@ async function main () {
|
|
|
319
326
|
|
|
320
327
|
// Setup management client for privileged applications
|
|
321
328
|
if (applicationConfig.management) {
|
|
322
|
-
const mgmtEnabled =
|
|
323
|
-
|
|
324
|
-
|
|
329
|
+
const mgmtEnabled =
|
|
330
|
+
typeof applicationConfig.management === 'boolean'
|
|
331
|
+
? applicationConfig.management
|
|
332
|
+
: applicationConfig.management.enabled !== false
|
|
325
333
|
|
|
326
334
|
if (mgmtEnabled) {
|
|
327
335
|
const { ManagementClient } = await import('./management.js')
|
|
328
|
-
const ops = typeof applicationConfig.management === 'object'
|
|
329
|
-
? applicationConfig.management.operations
|
|
330
|
-
: undefined
|
|
336
|
+
const ops = typeof applicationConfig.management === 'object' ? applicationConfig.management.operations : undefined
|
|
331
337
|
updateGlobals({ management: new ManagementClient(ops) })
|
|
332
338
|
}
|
|
333
339
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/runtime",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.58.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"@fastify/compress": "^8.0.0",
|
|
19
19
|
"@fastify/express": "^4.0.0",
|
|
20
20
|
"@fastify/formbody": "^8.0.0",
|
|
21
|
+
"@sentry/node": "^10.58.0",
|
|
21
22
|
"atomic-sleep": "^1.0.0",
|
|
22
23
|
"autocannon": "^8.0.0",
|
|
23
24
|
"c8": "^11.0.0",
|
|
@@ -31,19 +32,20 @@
|
|
|
31
32
|
"json-schema-to-typescript": "^15.0.0",
|
|
32
33
|
"neostandard": "^0.12.0",
|
|
33
34
|
"pino-abstract-transport": "^2.0.0",
|
|
35
|
+
"pino-sentry-transport": "^1.6.0",
|
|
34
36
|
"split2": "^4.2.0",
|
|
35
37
|
"tstyche": "^6.2.0",
|
|
36
38
|
"typescript": "^5.5.4",
|
|
37
|
-
"undici-oidc-interceptor": "^0.
|
|
39
|
+
"undici-oidc-interceptor": "^0.9.0",
|
|
38
40
|
"why-is-node-running": "^2.2.2",
|
|
39
|
-
"@platformatic/
|
|
40
|
-
"@platformatic/
|
|
41
|
-
"@platformatic/
|
|
42
|
-
"@platformatic/
|
|
43
|
-
"@platformatic/
|
|
44
|
-
"@platformatic/
|
|
45
|
-
"@platformatic/
|
|
46
|
-
"@platformatic/
|
|
41
|
+
"@platformatic/composer": "3.58.1",
|
|
42
|
+
"@platformatic/gateway": "3.58.1",
|
|
43
|
+
"@platformatic/node": "3.58.1",
|
|
44
|
+
"@platformatic/sql-graphql": "3.58.1",
|
|
45
|
+
"@platformatic/sql-mapper": "3.58.1",
|
|
46
|
+
"@platformatic/db": "3.58.1",
|
|
47
|
+
"@platformatic/service": "3.58.1",
|
|
48
|
+
"@platformatic/wattpm-pprof-capture": "3.58.1"
|
|
47
49
|
},
|
|
48
50
|
"dependencies": {
|
|
49
51
|
"@fastify/accepts": "^5.0.0",
|
|
@@ -52,7 +54,7 @@
|
|
|
52
54
|
"@fastify/websocket": "^11.0.0",
|
|
53
55
|
"@opentelemetry/api": "^1.9.0",
|
|
54
56
|
"@platformatic/prom-client": "^1.0.0",
|
|
55
|
-
"@platformatic/undici-cache-memory": "^0.
|
|
57
|
+
"@platformatic/undici-cache-memory": "^0.9.0",
|
|
56
58
|
"@watchable/unpromise": "^1.0.2",
|
|
57
59
|
"change-case-all": "^2.1.0",
|
|
58
60
|
"close-with-grace": "^2.3.0",
|
|
@@ -73,13 +75,13 @@
|
|
|
73
75
|
"undici": "^7.27.2",
|
|
74
76
|
"undici-thread-interceptor": "^1.3.1",
|
|
75
77
|
"ws": "^8.16.0",
|
|
76
|
-
"@platformatic/basic": "3.
|
|
77
|
-
"@platformatic/
|
|
78
|
-
"@platformatic/
|
|
79
|
-
"@platformatic/
|
|
80
|
-
"@platformatic/
|
|
81
|
-
"@platformatic/
|
|
82
|
-
"@platformatic/telemetry": "3.
|
|
78
|
+
"@platformatic/basic": "3.58.1",
|
|
79
|
+
"@platformatic/foundation": "3.58.1",
|
|
80
|
+
"@platformatic/globals": "3.58.1",
|
|
81
|
+
"@platformatic/generators": "3.58.1",
|
|
82
|
+
"@platformatic/metrics": "3.58.1",
|
|
83
|
+
"@platformatic/itc": "3.58.1",
|
|
84
|
+
"@platformatic/telemetry": "3.58.1"
|
|
83
85
|
},
|
|
84
86
|
"engines": {
|
|
85
87
|
"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.58.1.json",
|
|
3
3
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
4
|
"title": "Platformatic Runtime Config",
|
|
5
5
|
"type": "object",
|
|
@@ -1928,7 +1928,14 @@
|
|
|
1928
1928
|
},
|
|
1929
1929
|
"exitOnUnhandledErrors": {
|
|
1930
1930
|
"default": true,
|
|
1931
|
-
"
|
|
1931
|
+
"anyOf": [
|
|
1932
|
+
{
|
|
1933
|
+
"type": "boolean"
|
|
1934
|
+
},
|
|
1935
|
+
{
|
|
1936
|
+
"type": "number"
|
|
1937
|
+
}
|
|
1938
|
+
]
|
|
1932
1939
|
},
|
|
1933
1940
|
"gracefulShutdown": {
|
|
1934
1941
|
"type": "object",
|
|
@@ -2110,6 +2117,17 @@
|
|
|
2110
2117
|
},
|
|
2111
2118
|
"additionalProperties": false
|
|
2112
2119
|
},
|
|
2120
|
+
"healthProbes": {
|
|
2121
|
+
"anyOf": [
|
|
2122
|
+
{
|
|
2123
|
+
"type": "boolean"
|
|
2124
|
+
},
|
|
2125
|
+
{
|
|
2126
|
+
"type": "string"
|
|
2127
|
+
}
|
|
2128
|
+
],
|
|
2129
|
+
"default": true
|
|
2130
|
+
},
|
|
2113
2131
|
"undici": {
|
|
2114
2132
|
"type": "object",
|
|
2115
2133
|
"properties": {
|