@platformatic/runtime 3.0.0-alpha.6 → 3.0.0-rc.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 -0
- package/index.js +24 -3
- package/lib/config.js +81 -2
- package/lib/errors.js +5 -0
- package/lib/management-api.js +57 -26
- package/lib/prom-server.js +11 -3
- package/lib/runtime.js +142 -96
- package/lib/worker/controller.js +23 -8
- package/lib/worker/itc.js +38 -32
- package/lib/worker/main.js +17 -9
- package/package.json +18 -17
- package/schema.json +32 -1
- package/lib/dependencies.js +0 -63
package/lib/runtime.js
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
deepmerge,
|
|
3
3
|
ensureError,
|
|
4
4
|
ensureLoggableError,
|
|
5
|
+
executeInParallel,
|
|
5
6
|
executeWithTimeout,
|
|
6
7
|
features,
|
|
7
8
|
kMetadata,
|
|
@@ -22,7 +23,6 @@ import { Worker } from 'node:worker_threads'
|
|
|
22
23
|
import SonicBoom from 'sonic-boom'
|
|
23
24
|
import { Agent, request, interceptors as undiciInterceptors } from 'undici'
|
|
24
25
|
import { createThreadInterceptor } from 'undici-thread-interceptor'
|
|
25
|
-
import { checkDependencies, topologicalSort } from './dependencies.js'
|
|
26
26
|
import {
|
|
27
27
|
ApplicationAlreadyStartedError,
|
|
28
28
|
ApplicationNotFoundError,
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
InvalidArgumentError,
|
|
32
32
|
MessagingError,
|
|
33
33
|
MissingEntrypointError,
|
|
34
|
+
MissingPprofCapture,
|
|
34
35
|
RuntimeAbortedError,
|
|
35
36
|
RuntimeExitedError,
|
|
36
37
|
WorkerNotFoundError
|
|
@@ -59,18 +60,20 @@ import {
|
|
|
59
60
|
|
|
60
61
|
const kWorkerFile = join(import.meta.dirname, 'worker/main.js')
|
|
61
62
|
const kInspectorOptions = Symbol('plt.runtime.worker.inspectorOptions')
|
|
62
|
-
const kForwardEvents = Symbol('plt.runtime.worker.forwardEvents')
|
|
63
63
|
|
|
64
64
|
const MAX_LISTENERS_COUNT = 100
|
|
65
65
|
const MAX_METRICS_QUEUE_LENGTH = 5 * 60 // 5 minutes in seconds
|
|
66
66
|
const COLLECT_METRICS_TIMEOUT = 1000
|
|
67
67
|
|
|
68
|
+
const MAX_CONCURRENCY = 5
|
|
68
69
|
const MAX_BOOTSTRAP_ATTEMPTS = 5
|
|
69
70
|
const IMMEDIATE_RESTART_MAX_THRESHOLD = 10
|
|
70
71
|
const MAX_WORKERS = 100
|
|
71
72
|
|
|
72
73
|
export class Runtime extends EventEmitter {
|
|
73
74
|
logger
|
|
75
|
+
error
|
|
76
|
+
|
|
74
77
|
#loggerDestination
|
|
75
78
|
#stdio
|
|
76
79
|
|
|
@@ -81,6 +84,7 @@ export class Runtime extends EventEmitter {
|
|
|
81
84
|
#context
|
|
82
85
|
#sharedContext
|
|
83
86
|
#isProduction
|
|
87
|
+
#concurrency
|
|
84
88
|
#entrypointId
|
|
85
89
|
#url
|
|
86
90
|
|
|
@@ -112,6 +116,7 @@ export class Runtime extends EventEmitter {
|
|
|
112
116
|
this.#env = config[kMetadata].env
|
|
113
117
|
this.#context = context ?? {}
|
|
114
118
|
this.#isProduction = this.#context.isProduction ?? this.#context.production ?? false
|
|
119
|
+
this.#concurrency = this.#context.concurrency ?? MAX_CONCURRENCY
|
|
115
120
|
this.#workers = new RoundRobinMap()
|
|
116
121
|
this.#url = undefined
|
|
117
122
|
this.#meshInterceptor = createThreadInterceptor({ domain: '.plt.local', timeout: this.#config.applicationTimeout })
|
|
@@ -150,7 +155,6 @@ export class Runtime extends EventEmitter {
|
|
|
150
155
|
}
|
|
151
156
|
|
|
152
157
|
const config = this.#config
|
|
153
|
-
const autoloadEnabled = config.autoload
|
|
154
158
|
|
|
155
159
|
if (config.managementApi) {
|
|
156
160
|
this.#managementApi = await startManagementApi(this, this.#root)
|
|
@@ -190,61 +194,7 @@ export class Runtime extends EventEmitter {
|
|
|
190
194
|
this.#env['PLT_ENVIRONMENT'] = 'development'
|
|
191
195
|
}
|
|
192
196
|
|
|
193
|
-
|
|
194
|
-
for (const applicationConfig of config.applications) {
|
|
195
|
-
// If there is no application path, check if the application was resolved
|
|
196
|
-
if (!applicationConfig.path) {
|
|
197
|
-
if (applicationConfig.url) {
|
|
198
|
-
// Try to backfill the path for external applications
|
|
199
|
-
applicationConfig.path = join(this.#root, config.resolvedApplicationsBasePath, applicationConfig.id)
|
|
200
|
-
|
|
201
|
-
if (!existsSync(applicationConfig.path)) {
|
|
202
|
-
const executable = globalThis.platformatic?.executable ?? 'platformatic'
|
|
203
|
-
this.logger.error(
|
|
204
|
-
`The path for application "%s" does not exist. Please run "${executable} resolve" and try again.`,
|
|
205
|
-
applicationConfig.id
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
await this.closeAndThrow(new RuntimeAbortedError())
|
|
209
|
-
}
|
|
210
|
-
} else {
|
|
211
|
-
this.logger.error(
|
|
212
|
-
'The application "%s" has no path defined. Please check your configuration and try again.',
|
|
213
|
-
applicationConfig.id
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
await this.closeAndThrow(new RuntimeAbortedError())
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
await this.#setupApplication(applicationConfig)
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
try {
|
|
224
|
-
checkDependencies(config.applications)
|
|
225
|
-
|
|
226
|
-
// Make sure the list exists before computing the dependencies, otherwise some applications might not be stopped
|
|
227
|
-
if (autoloadEnabled) {
|
|
228
|
-
this.#workers = topologicalSort(this.#workers, config)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// When autoloading is disabled, add a warning if an application is defined before its dependencies
|
|
232
|
-
if (!autoloadEnabled) {
|
|
233
|
-
for (let i = 0; i < config.applications.length; i++) {
|
|
234
|
-
const current = config.applications[i]
|
|
235
|
-
|
|
236
|
-
for (const dep of current.dependencies ?? []) {
|
|
237
|
-
if (config.applications.findIndex(s => s.id === dep.id) > i) {
|
|
238
|
-
this.logger.warn(
|
|
239
|
-
`Application "${current.id}" depends on application "${dep.id}", but it is defined and it will be started before it. Please check your configuration file.`
|
|
240
|
-
)
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
} catch (e) {
|
|
246
|
-
await this.closeAndThrow(e)
|
|
247
|
-
}
|
|
197
|
+
await this.#setupApplications()
|
|
248
198
|
|
|
249
199
|
await this.#setDispatcher(config.undici)
|
|
250
200
|
|
|
@@ -266,12 +216,14 @@ export class Runtime extends EventEmitter {
|
|
|
266
216
|
this.#updateStatus('starting')
|
|
267
217
|
this.#createWorkersBroadcastChannel()
|
|
268
218
|
|
|
269
|
-
// Important: do not use Promise.all here since it won't properly manage dependencies
|
|
270
219
|
try {
|
|
220
|
+
const startInvocations = []
|
|
271
221
|
for (const application of this.getApplicationsIds()) {
|
|
272
|
-
|
|
222
|
+
startInvocations.push([application, silent])
|
|
273
223
|
}
|
|
274
224
|
|
|
225
|
+
await executeInParallel(this.startApplication.bind(this), startInvocations, this.#concurrency)
|
|
226
|
+
|
|
275
227
|
if (this.#config.inspectorOptions) {
|
|
276
228
|
const { port } = this.#config.inspectorOptions
|
|
277
229
|
|
|
@@ -340,16 +292,35 @@ export class Runtime extends EventEmitter {
|
|
|
340
292
|
await this.stopApplication(this.#entrypointId, silent)
|
|
341
293
|
}
|
|
342
294
|
|
|
343
|
-
|
|
344
|
-
|
|
295
|
+
const stopInvocations = []
|
|
296
|
+
|
|
297
|
+
const allApplications = await this.getApplications(true)
|
|
298
|
+
|
|
299
|
+
// Construct the reverse dependency graph
|
|
300
|
+
const dependents = {}
|
|
301
|
+
for (const application of allApplications.applications) {
|
|
302
|
+
for (const dependency of application.dependencies ?? []) {
|
|
303
|
+
let applicationDependents = dependents[dependency]
|
|
304
|
+
if (!applicationDependents) {
|
|
305
|
+
applicationDependents = new Set()
|
|
306
|
+
dependents[dependency] = applicationDependents
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
applicationDependents.add(application.id)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
for (const application of this.getApplicationsIds()) {
|
|
345
314
|
// The entrypoint has been stopped above
|
|
346
315
|
if (application === this.#entrypointId) {
|
|
347
316
|
continue
|
|
348
317
|
}
|
|
349
318
|
|
|
350
|
-
|
|
319
|
+
stopInvocations.push([application, silent, Array.from(dependents[application] ?? [])])
|
|
351
320
|
}
|
|
352
321
|
|
|
322
|
+
await executeInParallel(this.stopApplication.bind(this), stopInvocations, this.#concurrency)
|
|
323
|
+
|
|
353
324
|
await this.#meshInterceptor.close()
|
|
354
325
|
this.#workersBroadcastChannel?.close()
|
|
355
326
|
|
|
@@ -397,6 +368,7 @@ export class Runtime extends EventEmitter {
|
|
|
397
368
|
|
|
398
369
|
async closeAndThrow (error) {
|
|
399
370
|
this.#updateStatus('errored', error)
|
|
371
|
+
this.error = error
|
|
400
372
|
|
|
401
373
|
// Wait for the next tick so that any pending logging is properly flushed
|
|
402
374
|
await sleep(1)
|
|
@@ -452,9 +424,7 @@ export class Runtime extends EventEmitter {
|
|
|
452
424
|
|
|
453
425
|
emit (event, payload) {
|
|
454
426
|
for (const worker of this.#workers.values()) {
|
|
455
|
-
|
|
456
|
-
worker[kITC].notify('runtime:event', { event, payload })
|
|
457
|
-
}
|
|
427
|
+
worker[kITC].notify('runtime:event', { event, payload })
|
|
458
428
|
}
|
|
459
429
|
|
|
460
430
|
this.logger.trace({ event, payload }, 'Runtime event')
|
|
@@ -502,7 +472,7 @@ export class Runtime extends EventEmitter {
|
|
|
502
472
|
this.emit('application:started', id)
|
|
503
473
|
}
|
|
504
474
|
|
|
505
|
-
async stopApplication (id, silent = false) {
|
|
475
|
+
async stopApplication (id, silent = false, dependents = []) {
|
|
506
476
|
const config = this.#config
|
|
507
477
|
const applicationConfig = config.applications.find(s => s.id === id)
|
|
508
478
|
|
|
@@ -515,9 +485,12 @@ export class Runtime extends EventEmitter {
|
|
|
515
485
|
this.emit('application:stopping', id)
|
|
516
486
|
|
|
517
487
|
if (typeof workersCount === 'number') {
|
|
488
|
+
const stopInvocations = []
|
|
518
489
|
for (let i = 0; i < workersCount; i++) {
|
|
519
|
-
|
|
490
|
+
stopInvocations.push([workersCount, id, i, silent, undefined, dependents])
|
|
520
491
|
}
|
|
492
|
+
|
|
493
|
+
await executeInParallel(this.#stopWorker.bind(this), stopInvocations, this.#concurrency)
|
|
521
494
|
}
|
|
522
495
|
|
|
523
496
|
this.emit('application:stopped', id)
|
|
@@ -540,6 +513,20 @@ export class Runtime extends EventEmitter {
|
|
|
540
513
|
}
|
|
541
514
|
}
|
|
542
515
|
|
|
516
|
+
async startApplicationProfiling (id, options = {}, ensureStarted = true) {
|
|
517
|
+
const service = await this.#getApplicationById(id, ensureStarted)
|
|
518
|
+
this.#validatePprofCapturePreload()
|
|
519
|
+
|
|
520
|
+
return sendViaITC(service, 'startProfiling', options)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async stopApplicationProfiling (id, ensureStarted = true) {
|
|
524
|
+
const service = await this.#getApplicationById(id, ensureStarted)
|
|
525
|
+
this.#validatePprofCapturePreload()
|
|
526
|
+
|
|
527
|
+
return sendViaITC(service, 'stopProfiling')
|
|
528
|
+
}
|
|
529
|
+
|
|
543
530
|
async updateUndiciInterceptors (undiciConfig) {
|
|
544
531
|
this.#config.undici = undiciConfig
|
|
545
532
|
|
|
@@ -754,6 +741,10 @@ export class Runtime extends EventEmitter {
|
|
|
754
741
|
return report
|
|
755
742
|
}
|
|
756
743
|
|
|
744
|
+
setConcurrency (concurrency) {
|
|
745
|
+
this.#concurrency = concurrency
|
|
746
|
+
}
|
|
747
|
+
|
|
757
748
|
async getUrl () {
|
|
758
749
|
return this.#url
|
|
759
750
|
}
|
|
@@ -822,7 +813,9 @@ export class Runtime extends EventEmitter {
|
|
|
822
813
|
const label = `${application}:${i}`
|
|
823
814
|
const worker = this.#workers.get(label)
|
|
824
815
|
|
|
825
|
-
|
|
816
|
+
if (worker) {
|
|
817
|
+
status[label] = await sendViaITC(worker, 'getCustomHealthCheck')
|
|
818
|
+
}
|
|
826
819
|
}
|
|
827
820
|
}
|
|
828
821
|
|
|
@@ -837,7 +830,9 @@ export class Runtime extends EventEmitter {
|
|
|
837
830
|
const label = `${application}:${i}`
|
|
838
831
|
const worker = this.#workers.get(label)
|
|
839
832
|
|
|
840
|
-
|
|
833
|
+
if (worker) {
|
|
834
|
+
status[label] = await sendViaITC(worker, 'getCustomReadinessCheck')
|
|
835
|
+
}
|
|
841
836
|
}
|
|
842
837
|
}
|
|
843
838
|
|
|
@@ -1014,11 +1009,13 @@ export class Runtime extends EventEmitter {
|
|
|
1014
1009
|
return this.#config.applications.map(application => application.id)
|
|
1015
1010
|
}
|
|
1016
1011
|
|
|
1017
|
-
async getApplications () {
|
|
1012
|
+
async getApplications (allowUnloaded = false) {
|
|
1018
1013
|
return {
|
|
1019
1014
|
entrypoint: this.#entrypointId,
|
|
1020
1015
|
production: this.#isProduction,
|
|
1021
|
-
applications: await Promise.all(
|
|
1016
|
+
applications: await Promise.all(
|
|
1017
|
+
this.getApplicationsIds().map(id => this.getApplicationDetails(id, allowUnloaded))
|
|
1018
|
+
)
|
|
1022
1019
|
}
|
|
1023
1020
|
}
|
|
1024
1021
|
|
|
@@ -1070,19 +1067,19 @@ export class Runtime extends EventEmitter {
|
|
|
1070
1067
|
throw e
|
|
1071
1068
|
}
|
|
1072
1069
|
|
|
1073
|
-
const { entrypoint,
|
|
1070
|
+
const { entrypoint, localUrl } = application[kConfig]
|
|
1074
1071
|
|
|
1075
1072
|
const status = await sendViaITC(application, 'getStatus')
|
|
1076
|
-
const { type, version } = await sendViaITC(application, 'getApplicationInfo')
|
|
1073
|
+
const { type, version, dependencies } = await sendViaITC(application, 'getApplicationInfo')
|
|
1077
1074
|
|
|
1078
1075
|
const applicationDetails = {
|
|
1079
1076
|
id,
|
|
1080
1077
|
type,
|
|
1081
1078
|
status,
|
|
1079
|
+
dependencies,
|
|
1082
1080
|
version,
|
|
1083
1081
|
localUrl,
|
|
1084
|
-
entrypoint
|
|
1085
|
-
dependencies
|
|
1082
|
+
entrypoint
|
|
1086
1083
|
}
|
|
1087
1084
|
|
|
1088
1085
|
if (this.#isProduction) {
|
|
@@ -1175,17 +1172,59 @@ export class Runtime extends EventEmitter {
|
|
|
1175
1172
|
this.logger.info(`Platformatic is now listening at ${this.#url}`)
|
|
1176
1173
|
}
|
|
1177
1174
|
|
|
1175
|
+
async #setupApplications () {
|
|
1176
|
+
const config = this.#config
|
|
1177
|
+
const setupInvocations = []
|
|
1178
|
+
|
|
1179
|
+
// Parse all applications and verify we're not missing any path or resolved application
|
|
1180
|
+
for (const applicationConfig of config.applications) {
|
|
1181
|
+
// If there is no application path, check if the application was resolved
|
|
1182
|
+
if (!applicationConfig.path) {
|
|
1183
|
+
if (applicationConfig.url) {
|
|
1184
|
+
// Try to backfill the path for external applications
|
|
1185
|
+
applicationConfig.path = join(this.#root, config.resolvedApplicationsBasePath, applicationConfig.id)
|
|
1186
|
+
|
|
1187
|
+
if (!existsSync(applicationConfig.path)) {
|
|
1188
|
+
const executable = globalThis.platformatic?.executable ?? 'platformatic'
|
|
1189
|
+
this.logger.error(
|
|
1190
|
+
`The path for application "%s" does not exist. Please run "${executable} resolve" and try again.`,
|
|
1191
|
+
applicationConfig.id
|
|
1192
|
+
)
|
|
1193
|
+
|
|
1194
|
+
await this.closeAndThrow(new RuntimeAbortedError())
|
|
1195
|
+
}
|
|
1196
|
+
} else {
|
|
1197
|
+
this.logger.error(
|
|
1198
|
+
'The application "%s" has no path defined. Please check your configuration and try again.',
|
|
1199
|
+
applicationConfig.id
|
|
1200
|
+
)
|
|
1201
|
+
|
|
1202
|
+
await this.closeAndThrow(new RuntimeAbortedError())
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
setupInvocations.push([applicationConfig])
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
await executeInParallel(this.#setupApplication.bind(this), setupInvocations, this.#concurrency)
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1178
1212
|
async #setupApplication (applicationConfig) {
|
|
1179
|
-
if (this.#status === 'stopping' || this.#status === 'closed')
|
|
1213
|
+
if (this.#status === 'stopping' || this.#status === 'closed') {
|
|
1214
|
+
return
|
|
1215
|
+
}
|
|
1180
1216
|
|
|
1181
1217
|
const config = this.#config
|
|
1182
1218
|
const workersCount = await this.#workers.getCount(applicationConfig.id)
|
|
1183
1219
|
const id = applicationConfig.id
|
|
1220
|
+
const setupInvocations = []
|
|
1184
1221
|
|
|
1185
1222
|
for (let i = 0; i < workersCount; i++) {
|
|
1186
|
-
|
|
1223
|
+
setupInvocations.push([config, applicationConfig, workersCount, id, i])
|
|
1187
1224
|
}
|
|
1188
1225
|
|
|
1226
|
+
await executeInParallel(this.#setupWorker.bind(this), setupInvocations, this.#concurrency)
|
|
1227
|
+
|
|
1189
1228
|
this.emit('application:init', id)
|
|
1190
1229
|
}
|
|
1191
1230
|
|
|
@@ -1287,6 +1326,7 @@ export class Runtime extends EventEmitter {
|
|
|
1287
1326
|
|
|
1288
1327
|
// Track application exiting
|
|
1289
1328
|
const eventPayload = { application: applicationId, worker: index, workersCount }
|
|
1329
|
+
|
|
1290
1330
|
worker.once('exit', code => {
|
|
1291
1331
|
if (worker[kWorkerStatus] === 'exited') {
|
|
1292
1332
|
return
|
|
@@ -1338,7 +1378,6 @@ export class Runtime extends EventEmitter {
|
|
|
1338
1378
|
worker[kApplicationId] = applicationId
|
|
1339
1379
|
worker[kWorkerId] = workersCount > 1 ? index : undefined
|
|
1340
1380
|
worker[kWorkerStatus] = 'boot'
|
|
1341
|
-
worker[kForwardEvents] = false
|
|
1342
1381
|
|
|
1343
1382
|
if (inspectorOptions) {
|
|
1344
1383
|
worker[kInspectorOptions] = {
|
|
@@ -1352,12 +1391,7 @@ export class Runtime extends EventEmitter {
|
|
|
1352
1391
|
worker[kITC] = new ITC({
|
|
1353
1392
|
name: workerId + '-runtime',
|
|
1354
1393
|
port: worker,
|
|
1355
|
-
handlers:
|
|
1356
|
-
...this.#workerITCHandlers,
|
|
1357
|
-
setEventsForwarding (value) {
|
|
1358
|
-
worker[kForwardEvents] = value
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1394
|
+
handlers: this.#workerITCHandlers
|
|
1361
1395
|
})
|
|
1362
1396
|
worker[kITC].listen()
|
|
1363
1397
|
|
|
@@ -1403,15 +1437,13 @@ export class Runtime extends EventEmitter {
|
|
|
1403
1437
|
this.#meshInterceptor.route(applicationId, worker)
|
|
1404
1438
|
}
|
|
1405
1439
|
|
|
1406
|
-
//
|
|
1407
|
-
|
|
1408
|
-
applicationConfig.dependencies = dependencies
|
|
1440
|
+
// Wait for initialization
|
|
1441
|
+
await waitEventFromITC(worker, 'init')
|
|
1409
1442
|
|
|
1410
1443
|
if (applicationConfig.entrypoint) {
|
|
1411
1444
|
this.#entrypointId = applicationId
|
|
1412
1445
|
}
|
|
1413
1446
|
|
|
1414
|
-
// This must be done here as the dependencies are filled above
|
|
1415
1447
|
worker[kConfig] = { ...applicationConfig, health, workers: workersCount }
|
|
1416
1448
|
worker[kWorkerStatus] = 'init'
|
|
1417
1449
|
this.emit('application:worker:init', eventPayload)
|
|
@@ -1587,6 +1619,7 @@ export class Runtime extends EventEmitter {
|
|
|
1587
1619
|
}
|
|
1588
1620
|
} catch (err) {
|
|
1589
1621
|
const error = ensureError(err)
|
|
1622
|
+
worker[kITC].notify('application:worker:start:processed')
|
|
1590
1623
|
|
|
1591
1624
|
// TODO: handle port allocation error here
|
|
1592
1625
|
if (error.code === 'EADDRINUSE' || error.code === 'EACCES') throw error
|
|
@@ -1608,7 +1641,7 @@ export class Runtime extends EventEmitter {
|
|
|
1608
1641
|
this.emit('application:worker:start:error', { ...eventPayload, error })
|
|
1609
1642
|
|
|
1610
1643
|
if (error.code !== 'PLT_RUNTIME_APPLICATION_START_TIMEOUT') {
|
|
1611
|
-
this.logger.error({ err: ensureLoggableError(error) }, `Failed to start ${label}
|
|
1644
|
+
this.logger.error({ err: ensureLoggableError(error) }, `Failed to start ${label}: ${error.message}`)
|
|
1612
1645
|
}
|
|
1613
1646
|
|
|
1614
1647
|
const restartOnError = config.restartOnError
|
|
@@ -1619,6 +1652,7 @@ export class Runtime extends EventEmitter {
|
|
|
1619
1652
|
|
|
1620
1653
|
if (bootstrapAttempt++ >= MAX_BOOTSTRAP_ATTEMPTS || restartOnError === 0) {
|
|
1621
1654
|
this.logger.error(`Failed to start ${label} after ${MAX_BOOTSTRAP_ATTEMPTS} attempts.`)
|
|
1655
|
+
this.emit('application:worker:start:failed', { ...eventPayload, error })
|
|
1622
1656
|
throw error
|
|
1623
1657
|
}
|
|
1624
1658
|
|
|
@@ -1636,7 +1670,7 @@ export class Runtime extends EventEmitter {
|
|
|
1636
1670
|
}
|
|
1637
1671
|
}
|
|
1638
1672
|
|
|
1639
|
-
async #stopWorker (workersCount, id, index, silent, worker
|
|
1673
|
+
async #stopWorker (workersCount, id, index, silent, worker, dependents) {
|
|
1640
1674
|
if (!worker) {
|
|
1641
1675
|
worker = await this.#getWorkerById(id, index, false, false)
|
|
1642
1676
|
}
|
|
@@ -1662,16 +1696,20 @@ export class Runtime extends EventEmitter {
|
|
|
1662
1696
|
this.logger.info(`Stopping the ${label}...`)
|
|
1663
1697
|
}
|
|
1664
1698
|
|
|
1665
|
-
const exitTimeout = this.#config.gracefulShutdown.
|
|
1699
|
+
const exitTimeout = this.#config.gracefulShutdown.application
|
|
1666
1700
|
const exitPromise = once(worker, 'exit')
|
|
1667
1701
|
|
|
1668
1702
|
// Always send the stop message, it will shut down workers that only had ITC and interceptors setup
|
|
1669
1703
|
try {
|
|
1670
|
-
await executeWithTimeout(sendViaITC(worker, 'stop'), exitTimeout)
|
|
1704
|
+
await executeWithTimeout(sendViaITC(worker, 'stop', { force: !!this.error, dependents }), exitTimeout)
|
|
1671
1705
|
} catch (error) {
|
|
1672
|
-
this.emit('application:worker:stop:
|
|
1706
|
+
this.emit('application:worker:stop:error', eventPayload)
|
|
1673
1707
|
this.logger.info({ error: ensureLoggableError(error) }, `Failed to stop ${label}. Killing a worker thread.`)
|
|
1674
1708
|
} finally {
|
|
1709
|
+
worker[kITC].notify('application:worker:stop:processed')
|
|
1710
|
+
// Wait for the processed message to be received
|
|
1711
|
+
await sleep(1)
|
|
1712
|
+
|
|
1675
1713
|
worker[kITC].close()
|
|
1676
1714
|
}
|
|
1677
1715
|
|
|
@@ -1800,7 +1838,7 @@ export class Runtime extends EventEmitter {
|
|
|
1800
1838
|
throw e
|
|
1801
1839
|
}
|
|
1802
1840
|
|
|
1803
|
-
await this.#stopWorker(workersCount, applicationId, index, false, worker)
|
|
1841
|
+
await this.#stopWorker(workersCount, applicationId, index, false, worker, [])
|
|
1804
1842
|
}
|
|
1805
1843
|
|
|
1806
1844
|
async #getApplicationById (applicationId, ensureStarted = false, mustExist = true) {
|
|
@@ -2323,7 +2361,7 @@ export class Runtime extends EventEmitter {
|
|
|
2323
2361
|
for (let i = currentWorkers - 1; i >= workers; i--) {
|
|
2324
2362
|
const worker = await this.#getWorkerById(applicationId, i, false, false)
|
|
2325
2363
|
await sendViaITC(worker, 'removeFromMesh')
|
|
2326
|
-
await this.#stopWorker(currentWorkers, applicationId, i, false, worker)
|
|
2364
|
+
await this.#stopWorker(currentWorkers, applicationId, i, false, worker, [])
|
|
2327
2365
|
report.stopped.push(i)
|
|
2328
2366
|
}
|
|
2329
2367
|
await this.#updateApplicationConfigWorkers(applicationId, workers)
|
|
@@ -2343,4 +2381,12 @@ export class Runtime extends EventEmitter {
|
|
|
2343
2381
|
}
|
|
2344
2382
|
return report
|
|
2345
2383
|
}
|
|
2384
|
+
|
|
2385
|
+
#validatePprofCapturePreload () {
|
|
2386
|
+
const found = this.#config.preload?.some(p => p.includes('wattpm-pprof-capture'))
|
|
2387
|
+
|
|
2388
|
+
if (!found) {
|
|
2389
|
+
throw new MissingPprofCapture()
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2346
2392
|
}
|
package/lib/worker/controller.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ensureLoggableError,
|
|
3
3
|
FileWatcher,
|
|
4
|
+
kHandledError,
|
|
4
5
|
listRecognizedConfigurationFiles,
|
|
5
6
|
loadConfiguration,
|
|
6
7
|
loadConfigurationModule
|
|
@@ -9,6 +10,7 @@ import debounce from 'debounce'
|
|
|
9
10
|
import { EventEmitter } from 'node:events'
|
|
10
11
|
import { existsSync } from 'node:fs'
|
|
11
12
|
import { resolve } from 'node:path'
|
|
13
|
+
import { getActiveResourcesInfo } from 'node:process'
|
|
12
14
|
import { workerData } from 'node:worker_threads'
|
|
13
15
|
import { getGlobalDispatcher, setGlobalDispatcher } from 'undici'
|
|
14
16
|
import { ApplicationAlreadyStartedError, RuntimeNotStartedError } from '../errors.js'
|
|
@@ -55,9 +57,11 @@ export class Controller extends EventEmitter {
|
|
|
55
57
|
this.#lastELU = performance.eventLoopUtilization()
|
|
56
58
|
|
|
57
59
|
this.#context = {
|
|
60
|
+
controller: this,
|
|
58
61
|
applicationId: this.applicationId,
|
|
59
62
|
workerId: this.workerId,
|
|
60
63
|
directory: this.appConfig.path,
|
|
64
|
+
dependencies: this.appConfig.dependencies,
|
|
61
65
|
isEntrypoint: this.appConfig.entrypoint,
|
|
62
66
|
isProduction: this.appConfig.isProduction,
|
|
63
67
|
telemetryConfig,
|
|
@@ -83,10 +87,7 @@ export class Controller extends EventEmitter {
|
|
|
83
87
|
}
|
|
84
88
|
}
|
|
85
89
|
|
|
86
|
-
|
|
87
|
-
return this.capability.getBootstrapDependencies?.() ?? []
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
+
// Note: capability's init() is executed within start
|
|
90
91
|
async init () {
|
|
91
92
|
try {
|
|
92
93
|
const appConfig = this.appConfig
|
|
@@ -142,10 +143,17 @@ export class Controller extends EventEmitter {
|
|
|
142
143
|
|
|
143
144
|
try {
|
|
144
145
|
await this.capability.init?.()
|
|
146
|
+
this.emit('init')
|
|
145
147
|
} catch (err) {
|
|
146
148
|
this.#logAndThrow(err)
|
|
147
149
|
}
|
|
148
150
|
|
|
151
|
+
this.emit('starting')
|
|
152
|
+
|
|
153
|
+
if (this.capability.status === 'stopped') {
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
149
157
|
if (this.#watch) {
|
|
150
158
|
const watchConfig = await this.capability.getWatchConfig()
|
|
151
159
|
|
|
@@ -174,21 +182,23 @@ export class Controller extends EventEmitter {
|
|
|
174
182
|
|
|
175
183
|
this.#started = true
|
|
176
184
|
this.#starting = false
|
|
177
|
-
this.emit('
|
|
185
|
+
this.emit('started')
|
|
178
186
|
}
|
|
179
187
|
|
|
180
|
-
async stop (force = false) {
|
|
188
|
+
async stop (force = false, dependents = []) {
|
|
181
189
|
if (!force && (!this.#started || this.#starting)) {
|
|
182
190
|
throw new RuntimeNotStartedError()
|
|
183
191
|
}
|
|
184
192
|
|
|
193
|
+
this.emit('stopping')
|
|
185
194
|
await this.#stopFileWatching()
|
|
195
|
+
await this.capability.waitForDependentsStop(dependents)
|
|
186
196
|
await this.capability.stop()
|
|
187
197
|
|
|
188
198
|
this.#started = false
|
|
189
199
|
this.#starting = false
|
|
190
200
|
this.#listening = false
|
|
191
|
-
this.emit('
|
|
201
|
+
this.emit('stopped')
|
|
192
202
|
}
|
|
193
203
|
|
|
194
204
|
async listen () {
|
|
@@ -213,6 +223,7 @@ export class Controller extends EventEmitter {
|
|
|
213
223
|
globalThis.platformatic.onHttpStatsSize(url, size || 0)
|
|
214
224
|
}
|
|
215
225
|
}
|
|
226
|
+
globalThis.platformatic.onActiveResourcesEventLoop(getActiveResourcesInfo().length)
|
|
216
227
|
return this.capability.getMetrics({ format })
|
|
217
228
|
}
|
|
218
229
|
|
|
@@ -260,7 +271,11 @@ export class Controller extends EventEmitter {
|
|
|
260
271
|
}
|
|
261
272
|
|
|
262
273
|
#logAndThrow (err) {
|
|
263
|
-
globalThis.platformatic.logger.error(
|
|
274
|
+
globalThis.platformatic.logger.error(
|
|
275
|
+
{ err: ensureLoggableError(err) },
|
|
276
|
+
err[kHandledError] ? err.message : 'The application threw an error.'
|
|
277
|
+
)
|
|
278
|
+
|
|
264
279
|
throw err
|
|
265
280
|
}
|
|
266
281
|
|