@platformatic/runtime 4.0.0-new-config.1 → 4.0.0-new-config.5
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 +594 -596
- package/index.d.ts +11 -5
- package/index.js +161 -85
- package/lib/config.js +47 -251
- package/lib/errors.js +8 -0
- package/lib/generator.js +199 -35
- package/lib/logger.js +3 -3
- package/lib/management-api.js +4 -60
- package/lib/policies.js +27 -0
- package/lib/runtime.js +315 -239
- package/lib/schema.js +76 -0
- package/lib/worker/controller.js +54 -49
- package/lib/worker/interceptors.js +34 -34
- package/lib/worker/itc.js +49 -2
- package/lib/worker/main.js +28 -53
- package/lib/worker/symbols.js +1 -1
- package/package.json +18 -18
- package/schema.json +87 -44
package/lib/runtime.js
CHANGED
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
executeInParallel,
|
|
6
6
|
executeWithTimeout,
|
|
7
7
|
features,
|
|
8
|
-
kEnvFileFallbackKeys,
|
|
9
8
|
kMetadata,
|
|
10
9
|
kTimeout,
|
|
11
10
|
parseMemorySize
|
|
@@ -25,6 +24,7 @@ import { STATUS_CODES } from 'node:http'
|
|
|
25
24
|
import { createRequire } from 'node:module'
|
|
26
25
|
import { availableParallelism } from 'node:os'
|
|
27
26
|
import { basename, dirname, isAbsolute, join } from 'node:path'
|
|
27
|
+
import { randomUUID } from 'node:crypto'
|
|
28
28
|
import { Readable } from 'node:stream'
|
|
29
29
|
import { finished } from 'node:stream/promises'
|
|
30
30
|
import { setImmediate as immediate, setTimeout as sleep } from 'node:timers/promises'
|
|
@@ -32,7 +32,7 @@ import { pathToFileURL } from 'node:url'
|
|
|
32
32
|
import { Worker } from 'node:worker_threads'
|
|
33
33
|
import SonicBoom from 'sonic-boom'
|
|
34
34
|
import { Agent, request, interceptors as undiciInterceptors } from 'undici'
|
|
35
|
-
import {
|
|
35
|
+
import { createCoordinator, createInterceptor } from 'undici-thread-interceptor'
|
|
36
36
|
import { pprofCapturePreloadPath } from './config.js'
|
|
37
37
|
import {
|
|
38
38
|
AddressInUseError,
|
|
@@ -53,17 +53,17 @@ import {
|
|
|
53
53
|
MessagingError,
|
|
54
54
|
MetricFamilyCollisionError,
|
|
55
55
|
MissingPprofCapture,
|
|
56
|
+
MixedServingStateError,
|
|
56
57
|
ReservedITCHandlerNameError,
|
|
57
58
|
RuntimeAbortedError,
|
|
58
59
|
RuntimeExtensionBuildAlreadyCalledError,
|
|
59
|
-
WorkerInterceptorJoinTimeoutError,
|
|
60
60
|
WorkerNotFoundError
|
|
61
61
|
} from './errors.js'
|
|
62
62
|
import { abstractLogger, createLogger } from './logger.js'
|
|
63
63
|
import { startManagementApi } from './management-api.js'
|
|
64
64
|
import { createManagementHandlers } from './management-handlers.js'
|
|
65
65
|
import { OpenTelemetryMetricsForwarder } from './opentelemetry-metrics.js'
|
|
66
|
-
import { createChannelCreationHook } from './policies.js'
|
|
66
|
+
import { createChannelCreationHook, createTargetPermissionHook } from './policies.js'
|
|
67
67
|
import { startHealthProbesServer, startPrometheusServer } from './prom-server.js'
|
|
68
68
|
import { startScheduler } from './scheduler.js'
|
|
69
69
|
import { createSharedStore } from './shared-http-cache.js'
|
|
@@ -79,7 +79,6 @@ import {
|
|
|
79
79
|
kFullId,
|
|
80
80
|
kHealthCheckTimer,
|
|
81
81
|
kId,
|
|
82
|
-
kInterceptorReadyPromise,
|
|
83
82
|
kIsSubprocessHost,
|
|
84
83
|
kITC,
|
|
85
84
|
kLastHealthCheckELU,
|
|
@@ -87,6 +86,7 @@ import {
|
|
|
87
86
|
kWorkerHealthSignals,
|
|
88
87
|
kWorkerId,
|
|
89
88
|
kWorkerPortOffset,
|
|
89
|
+
kWorkerServerOptions,
|
|
90
90
|
kWorkerUrl,
|
|
91
91
|
kWorkersBroadcast,
|
|
92
92
|
kWorkerStartTime,
|
|
@@ -176,6 +176,55 @@ const IMMEDIATE_RESTART_MAX_THRESHOLD = 10
|
|
|
176
176
|
const MAX_WORKERS = 100
|
|
177
177
|
const DEFAULT_RESTART_ON_ERROR_DELAY = 5000
|
|
178
178
|
|
|
179
|
+
/*
|
|
180
|
+
Both public payloads are built from a snapshot and frozen through. What a consumer could observe
|
|
181
|
+
in v3 was scalars and a file path, so handing out interior state was harmless in practice; v4
|
|
182
|
+
nests resolvedConfig -- an entire capability payload -- inside every entry, and the getters read
|
|
183
|
+
straight off live state. A consumer mutating what it received would be editing the configuration
|
|
184
|
+
that later restarts and scale-up workers read, silently, and would make worker generations
|
|
185
|
+
disagree about what they are running. setApplicationConfigPatch exists precisely so that changing
|
|
186
|
+
a running application's configuration is explicit, patch-shaped and visible.
|
|
187
|
+
|
|
188
|
+
Only plain objects and arrays are copied. Anything carrying its own prototype -- a class
|
|
189
|
+
instance, a stream, a buffer -- is handed back as it is, because a copy of it would not be the
|
|
190
|
+
thing; the hazard this closes is a consumer editing configuration, not one editing a socket.
|
|
191
|
+
*/
|
|
192
|
+
function frozenSnapshot (value, seen = new Map()) {
|
|
193
|
+
if (value === null || typeof value !== 'object') {
|
|
194
|
+
return value
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (seen.has(value)) {
|
|
198
|
+
return seen.get(value)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (Array.isArray(value)) {
|
|
202
|
+
const copy = []
|
|
203
|
+
seen.set(value, copy)
|
|
204
|
+
|
|
205
|
+
for (const entry of value) {
|
|
206
|
+
copy.push(frozenSnapshot(entry, seen))
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return Object.freeze(copy)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const prototype = Object.getPrototypeOf(value)
|
|
213
|
+
|
|
214
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
215
|
+
return value
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const copy = {}
|
|
219
|
+
seen.set(value, copy)
|
|
220
|
+
|
|
221
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
222
|
+
copy[key] = frozenSnapshot(entry, seen)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return Object.freeze(copy)
|
|
226
|
+
}
|
|
227
|
+
|
|
179
228
|
export class Runtime extends EventEmitter {
|
|
180
229
|
logger
|
|
181
230
|
error
|
|
@@ -201,11 +250,15 @@ export class Runtime extends EventEmitter {
|
|
|
201
250
|
#healthMetricsCollectionActive
|
|
202
251
|
|
|
203
252
|
#meshInterceptor
|
|
253
|
+
#meshCoordinator
|
|
254
|
+
#meshId
|
|
204
255
|
#dispatcher
|
|
205
256
|
|
|
206
257
|
#managementApi
|
|
207
258
|
#prometheusServer
|
|
208
259
|
#healthProbesServer
|
|
260
|
+
#metricsServersInitialized
|
|
261
|
+
#metricsServersStartPromise
|
|
209
262
|
#opentelemetryMetricsForwarder
|
|
210
263
|
#inspectorServer
|
|
211
264
|
#metricsLabelName
|
|
@@ -223,6 +276,7 @@ export class Runtime extends EventEmitter {
|
|
|
223
276
|
#extensionLivenessChecks
|
|
224
277
|
#extensionHealthRoutes
|
|
225
278
|
#lastOverloadProfiles
|
|
279
|
+
#servingStates
|
|
226
280
|
#restartingApplications
|
|
227
281
|
#restartingWorkers
|
|
228
282
|
#workerPortOffsets
|
|
@@ -255,13 +309,7 @@ export class Runtime extends EventEmitter {
|
|
|
255
309
|
this.#applicationRestartCounts = new Map()
|
|
256
310
|
this.#workers = new RoundRobinMap()
|
|
257
311
|
this.#channelCreationHook = createChannelCreationHook(this.#config)
|
|
258
|
-
this.#
|
|
259
|
-
domain: '.plt.local',
|
|
260
|
-
timeout: this.#config.applicationTimeout,
|
|
261
|
-
meshTimeout: this.#context.meshTimeout ?? true,
|
|
262
|
-
onChannelCreation: this.#channelCreationHook,
|
|
263
|
-
onError: this.#onMeshInterceptorError.bind(this)
|
|
264
|
-
})
|
|
312
|
+
this.#meshId = `runtime-${randomUUID()}`
|
|
265
313
|
this.logger = abstractLogger // This is replaced by the real logger in init() and eventually removed in close()
|
|
266
314
|
this.#status = undefined
|
|
267
315
|
this.#restartingApplications = new Set()
|
|
@@ -296,20 +344,26 @@ export class Runtime extends EventEmitter {
|
|
|
296
344
|
// Registered per-worker in #setupWorker, reserved so extensions cannot clobber it
|
|
297
345
|
this.#reservedITCHandlerNames.add('profiling:started')
|
|
298
346
|
this.#extensions = []
|
|
347
|
+
this.#metricsServersInitialized = false
|
|
348
|
+
this.#metricsServersStartPromise = null
|
|
299
349
|
this.#extensionsWantHealthMetrics = false
|
|
300
350
|
this.#extensionReadinessChecks = new Map()
|
|
301
351
|
this.#extensionLivenessChecks = new Map()
|
|
302
352
|
this.#extensionHealthRoutes = []
|
|
303
353
|
this.#lastOverloadProfiles = new Map()
|
|
354
|
+
this.#servingStates = new Map()
|
|
304
355
|
this.#sharedContext = {}
|
|
305
356
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
357
|
+
/*
|
|
358
|
+
v3 injected `PLT_DEV` and `PLT_ENVIRONMENT` here. v4 removes them: an application branches on
|
|
359
|
+
its own variables, or the decision moves into the configuration, where the context carries
|
|
360
|
+
`production` and `mode` with types. `NODE_ENV` is the one the runtime still defaults, at the
|
|
361
|
+
bottom of the env ladder, so anything the project sets outranks it.
|
|
362
|
+
|
|
363
|
+
They were worse than redundant by the end -- the v4 worker environment is the one the loader
|
|
364
|
+
resolved per application, and these were written onto the runtime's own copy, so under
|
|
365
|
+
`wattpm start` an application still read `PLT_ENVIRONMENT=development`.
|
|
366
|
+
*/
|
|
313
367
|
}
|
|
314
368
|
|
|
315
369
|
async init () {
|
|
@@ -327,10 +381,9 @@ export class Runtime extends EventEmitter {
|
|
|
327
381
|
this.#metricsLabelName = 'applicationId'
|
|
328
382
|
}
|
|
329
383
|
|
|
330
|
-
// Initialize process-level metrics registry
|
|
384
|
+
// Initialize process-level metrics registry only when metrics are enabled.
|
|
331
385
|
// These metrics are the same across all workers and only need to be collected once
|
|
332
|
-
|
|
333
|
-
if (config.metrics || config.managementApi) {
|
|
386
|
+
if (config.metrics && config.metrics.enabled !== false) {
|
|
334
387
|
this.#processMetricsRegistry = new metricsClient.Registry()
|
|
335
388
|
collectProcessMetrics(this.#processMetricsRegistry)
|
|
336
389
|
}
|
|
@@ -366,12 +419,22 @@ export class Runtime extends EventEmitter {
|
|
|
366
419
|
// readiness/liveness checks and probe routes before Fastify starts listening.
|
|
367
420
|
await this.#loadExtensions()
|
|
368
421
|
|
|
369
|
-
|
|
370
|
-
|
|
422
|
+
// A runtime without applications exits during start and must not briefly
|
|
423
|
+
// claim the default health-probe port. Applications added later, including
|
|
424
|
+
// by extension start hooks, initialize these servers in addApplications().
|
|
425
|
+
if (config.applications.length > 0) {
|
|
426
|
+
await this.#ensureMetricsServersStarted()
|
|
371
427
|
}
|
|
372
428
|
|
|
373
|
-
this.#
|
|
374
|
-
this.#
|
|
429
|
+
this.#meshCoordinator = createCoordinator({ meshId: this.#meshId })
|
|
430
|
+
this.#meshInterceptor = createInterceptor({
|
|
431
|
+
meshId: this.#meshId,
|
|
432
|
+
domain: '.plt.local',
|
|
433
|
+
connectTimeout: this.#config.applicationTimeout,
|
|
434
|
+
bootstrapTimeout: this.#config.applicationTimeout,
|
|
435
|
+
allowTarget: createTargetPermissionHook(this.#config)
|
|
436
|
+
})
|
|
437
|
+
await this.#meshInterceptor.ready
|
|
375
438
|
|
|
376
439
|
await this.addApplications(this.#config.applications)
|
|
377
440
|
await this.#setDispatcher(config.undici)
|
|
@@ -487,7 +550,8 @@ export class Runtime extends EventEmitter {
|
|
|
487
550
|
|
|
488
551
|
await this.stopApplications(this.getApplicationsIds(), silent)
|
|
489
552
|
|
|
490
|
-
await this.#meshInterceptor
|
|
553
|
+
await this.#meshInterceptor?.close()
|
|
554
|
+
this.#meshCoordinator?.destroy()
|
|
491
555
|
this.#workersBroadcastChannel?.close()
|
|
492
556
|
|
|
493
557
|
this.#updateStatus('stopped')
|
|
@@ -520,6 +584,7 @@ export class Runtime extends EventEmitter {
|
|
|
520
584
|
clearTimeout(this.#healthMetricsTimer)
|
|
521
585
|
this.#healthMetricsCollectionActive = false
|
|
522
586
|
this.#lastOverloadProfiles.clear()
|
|
587
|
+
this.#servingStates.clear()
|
|
523
588
|
|
|
524
589
|
await this.stop(silent)
|
|
525
590
|
this.#updateStatus('closing')
|
|
@@ -666,6 +731,10 @@ export class Runtime extends EventEmitter {
|
|
|
666
731
|
|
|
667
732
|
await executeInParallel(this.#setupApplication.bind(this), setupInvocations, this.#concurrency)
|
|
668
733
|
|
|
734
|
+
if (applications.length > 0) {
|
|
735
|
+
await this.#ensureMetricsServersStarted()
|
|
736
|
+
}
|
|
737
|
+
|
|
669
738
|
for (const application of applications) {
|
|
670
739
|
this.logger.debug(`Added application "${application.id}".`)
|
|
671
740
|
this.emitAndNotify('application:added', application)
|
|
@@ -677,7 +746,7 @@ export class Runtime extends EventEmitter {
|
|
|
677
746
|
|
|
678
747
|
const created = []
|
|
679
748
|
for (const { id } of applications) {
|
|
680
|
-
created.push(await this
|
|
749
|
+
created.push(await this.#buildApplicationDetails(id))
|
|
681
750
|
}
|
|
682
751
|
|
|
683
752
|
this.#updateLoggingPrefixes()
|
|
@@ -693,8 +762,13 @@ export class Runtime extends EventEmitter {
|
|
|
693
762
|
|
|
694
763
|
// Use allowUnloaded so that applications without a live worker
|
|
695
764
|
// (stopped or crashed with restartOnError: 0) can still be removed.
|
|
696
|
-
const details = await this
|
|
765
|
+
const details = await this.#buildApplicationDetails(application, true)
|
|
697
766
|
details.status = 'removed'
|
|
767
|
+
|
|
768
|
+
// The snapshot is taken while the application is still up, but what it reports is an
|
|
769
|
+
// application about to be gone: it is not making a claim about how it would serve.
|
|
770
|
+
delete details.servingState
|
|
771
|
+
|
|
698
772
|
removed.push(details)
|
|
699
773
|
}
|
|
700
774
|
|
|
@@ -735,8 +809,24 @@ export class Runtime extends EventEmitter {
|
|
|
735
809
|
const levels = topologicalLevels(applications, dependencies)
|
|
736
810
|
|
|
737
811
|
for (const level of levels) {
|
|
738
|
-
const
|
|
739
|
-
|
|
812
|
+
const applicationsWithPort = await Promise.all(
|
|
813
|
+
level.map(async applicationId => {
|
|
814
|
+
const worker = await this.#getWorkerByIdOrNext(applicationId)
|
|
815
|
+
const applicationConfig = await sendViaITC(worker, 'getApplicationConfig')
|
|
816
|
+
const port = Number(applicationConfig?.server?.port)
|
|
817
|
+
return { applicationId, hasPort: Number.isInteger(port) && port > 0 }
|
|
818
|
+
})
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
for (const hasPort of [true, false]) {
|
|
822
|
+
const startInvocations = applicationsWithPort
|
|
823
|
+
.filter(application => application.hasPort === hasPort)
|
|
824
|
+
.map(({ applicationId }) => [applicationId, silent])
|
|
825
|
+
|
|
826
|
+
if (startInvocations.length > 0) {
|
|
827
|
+
await executeInParallel(this.startApplication.bind(this), startInvocations, this.#concurrency)
|
|
828
|
+
}
|
|
829
|
+
}
|
|
740
830
|
}
|
|
741
831
|
}
|
|
742
832
|
|
|
@@ -838,6 +928,7 @@ export class Runtime extends EventEmitter {
|
|
|
838
928
|
}
|
|
839
929
|
|
|
840
930
|
await this.#registerApplicationSchedulerJobs(id)
|
|
931
|
+
await this.#collectServingState(id)
|
|
841
932
|
|
|
842
933
|
this.emitAndNotify('application:started', id)
|
|
843
934
|
await this.#dynamicWorkersScaler?.applyPendingUpdate(id)
|
|
@@ -864,6 +955,11 @@ export class Runtime extends EventEmitter {
|
|
|
864
955
|
await executeInParallel(this.#stopWorker.bind(this), stopInvocations, this.#concurrency)
|
|
865
956
|
}
|
|
866
957
|
|
|
958
|
+
// Absent rather than 'inactive': a stopped application is not making a claim about how it
|
|
959
|
+
// would serve, and conflating "not started" with "started and serving nothing" is the exact
|
|
960
|
+
// distinction this field exists to draw.
|
|
961
|
+
this.#servingStates.delete(id)
|
|
962
|
+
|
|
867
963
|
this.emitAndNotify('application:stopped', id)
|
|
868
964
|
}
|
|
869
965
|
|
|
@@ -1308,6 +1404,8 @@ export class Runtime extends EventEmitter {
|
|
|
1308
1404
|
this.#healthProbesServer = null
|
|
1309
1405
|
}
|
|
1310
1406
|
|
|
1407
|
+
this.#metricsServersInitialized = false
|
|
1408
|
+
|
|
1311
1409
|
if (this.#opentelemetryMetricsForwarder) {
|
|
1312
1410
|
await this.#opentelemetryMetricsForwarder.close()
|
|
1313
1411
|
this.#opentelemetryMetricsForwarder = null
|
|
@@ -1321,10 +1419,7 @@ export class Runtime extends EventEmitter {
|
|
|
1321
1419
|
entry.applied = false
|
|
1322
1420
|
}
|
|
1323
1421
|
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
this.#healthProbesServer = await startHealthProbesServer(this, metricsConfig, this.#config.healthProbes)
|
|
1327
|
-
this.#assertExtensionHealthRoutesApplied()
|
|
1422
|
+
await this.#ensureMetricsServersStarted()
|
|
1328
1423
|
|
|
1329
1424
|
await this.#startOpenTelemetryMetricsForwarder(metricsConfig?.opentelemetry)
|
|
1330
1425
|
|
|
@@ -1346,6 +1441,41 @@ export class Runtime extends EventEmitter {
|
|
|
1346
1441
|
return { success: true, config: metricsConfig }
|
|
1347
1442
|
}
|
|
1348
1443
|
|
|
1444
|
+
async #ensureMetricsServersStarted () {
|
|
1445
|
+
if (this.#metricsServersInitialized) {
|
|
1446
|
+
return
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
if (this.#metricsServersStartPromise) {
|
|
1450
|
+
return this.#metricsServersStartPromise
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
this.#metricsServersStartPromise = (async () => {
|
|
1454
|
+
const config = this.#config
|
|
1455
|
+
|
|
1456
|
+
try {
|
|
1457
|
+
if (config.metrics || (typeof config.healthProbes === 'object' && config.healthProbes !== null)) {
|
|
1458
|
+
this.#prometheusServer = await startPrometheusServer(this, config.metrics ?? false, config.healthProbes)
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
this.#healthProbesServer = await startHealthProbesServer(this, config.metrics, config.healthProbes)
|
|
1462
|
+
this.#assertExtensionHealthRoutesApplied()
|
|
1463
|
+
this.#metricsServersInitialized = true
|
|
1464
|
+
} catch (err) {
|
|
1465
|
+
await Promise.allSettled([this.#prometheusServer?.close(), this.#healthProbesServer?.close()])
|
|
1466
|
+
this.#prometheusServer = null
|
|
1467
|
+
this.#healthProbesServer = null
|
|
1468
|
+
throw err
|
|
1469
|
+
}
|
|
1470
|
+
})()
|
|
1471
|
+
|
|
1472
|
+
try {
|
|
1473
|
+
await this.#metricsServersStartPromise
|
|
1474
|
+
} finally {
|
|
1475
|
+
this.#metricsServersStartPromise = null
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1349
1479
|
async #startOpenTelemetryMetricsForwarder (config) {
|
|
1350
1480
|
if (!config?.endpoint || config.enabled === false || config.enabled === 'false') {
|
|
1351
1481
|
return
|
|
@@ -1581,6 +1711,14 @@ export class Runtime extends EventEmitter {
|
|
|
1581
1711
|
execPath: process.execPath,
|
|
1582
1712
|
nodeVersion: process.version,
|
|
1583
1713
|
projectDir: this.#root,
|
|
1714
|
+
/*
|
|
1715
|
+
What `applications:add`/`remove --save` actually consume, so they can stop reading the whole
|
|
1716
|
+
runtime configuration over HTTP to get at three values. `autoload` is the declaration as
|
|
1717
|
+
authored, not the expansion: v4 expands it in the eval worker, and --save has to edit what
|
|
1718
|
+
the file says rather than what it produced.
|
|
1719
|
+
*/
|
|
1720
|
+
configPath: this.#config[kMetadata]?.path ?? null,
|
|
1721
|
+
autoload: this.#config.autoload ?? null,
|
|
1584
1722
|
packageName: packageJson.name ?? null,
|
|
1585
1723
|
packageVersion: packageJson.version ?? null,
|
|
1586
1724
|
platformaticVersion: version,
|
|
@@ -1592,13 +1730,31 @@ export class Runtime extends EventEmitter {
|
|
|
1592
1730
|
return this.#env
|
|
1593
1731
|
}
|
|
1594
1732
|
|
|
1733
|
+
/*
|
|
1734
|
+
What a watcher has to follow to know this configuration changed. v4 reports the whole set the
|
|
1735
|
+
evaluation read; v3 resolves per worker and has no such set, so it is the deciding file alone --
|
|
1736
|
+
which is what dev watched before either way.
|
|
1737
|
+
*/
|
|
1738
|
+
getConfigurationWatchTargets () {
|
|
1739
|
+
const metadata = this.#config[kMetadata]
|
|
1740
|
+
const targets = metadata?.v4?.watchTargets
|
|
1741
|
+
|
|
1742
|
+
if (targets) {
|
|
1743
|
+
return { files: [...targets.files], directories: [...targets.directories] }
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
return { files: metadata?.path ? [metadata.path] : [], directories: [] }
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1595
1749
|
getRuntimeConfig (includeMeta = false) {
|
|
1750
|
+
// includeMeta is an internal contract and hands back live state, symbol key and all. It leaves
|
|
1751
|
+
// the public surface with the DTO change; until then, its in-tree callers depend on identity.
|
|
1596
1752
|
if (includeMeta) {
|
|
1597
1753
|
return this.#config
|
|
1598
1754
|
}
|
|
1599
1755
|
|
|
1600
1756
|
const { [kMetadata]: _, ...config } = this.#config
|
|
1601
|
-
return config
|
|
1757
|
+
return frozenSnapshot(config)
|
|
1602
1758
|
}
|
|
1603
1759
|
|
|
1604
1760
|
getInterceptor () {
|
|
@@ -1617,6 +1773,41 @@ export class Runtime extends EventEmitter {
|
|
|
1617
1773
|
return this.#managementApi?.server.address() ?? null
|
|
1618
1774
|
}
|
|
1619
1775
|
|
|
1776
|
+
/*
|
|
1777
|
+
servingState is computed per worker: for a worker-classified capability it depends on what the
|
|
1778
|
+
application's factory returned in *that* worker, and nothing stops arbitrary code from returning
|
|
1779
|
+
a server from worker 0 and a background result from worker 1. Sampling one worker -- which is
|
|
1780
|
+
what getApplicationDetails does for every other field -- would make the reported value depend on
|
|
1781
|
+
which worker the selector picked, and, worse, would leave mesh dispatch routing a share of
|
|
1782
|
+
requests to a worker that destroys them. So every worker answers and a mixed answer is refused,
|
|
1783
|
+
naming each worker and the state it reported.
|
|
1784
|
+
*/
|
|
1785
|
+
async #collectServingState (id) {
|
|
1786
|
+
const invocations = this.#workers.getKeys(id).map(workerId => [workerId, this.#workers.get(workerId)])
|
|
1787
|
+
|
|
1788
|
+
if (invocations.length === 0) {
|
|
1789
|
+
this.#servingStates.delete(id)
|
|
1790
|
+
return
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
const states = await sendMultipleViaITC(invocations, 'getServingState', undefined, [], this.#concurrency)
|
|
1794
|
+
const reported = Object.entries(states).filter(([, state]) => typeof state === 'string')
|
|
1795
|
+
|
|
1796
|
+
if (reported.length === 0) {
|
|
1797
|
+
this.#servingStates.delete(id)
|
|
1798
|
+
return
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
const distinct = new Set(reported.map(([, state]) => state))
|
|
1802
|
+
|
|
1803
|
+
if (distinct.size > 1) {
|
|
1804
|
+
this.#servingStates.delete(id)
|
|
1805
|
+
throw new MixedServingStateError(id, reported.map(([worker, state]) => `${worker} reported ${state}`).join(', '))
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
this.#servingStates.set(id, reported[0][1])
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1620
1811
|
async getCustomHealthChecks () {
|
|
1621
1812
|
const invocations = []
|
|
1622
1813
|
|
|
@@ -1768,7 +1959,7 @@ export class Runtime extends EventEmitter {
|
|
|
1768
1959
|
// Drop any configured custom label that shares the name of the application
|
|
1769
1960
|
// label (a config can set both `applicationLabel: 'serviceId'` and a static
|
|
1770
1961
|
// `serviceId` label): keeping it would make these runtime-wide metrics look
|
|
1771
|
-
// like they belong to an application
|
|
1962
|
+
// like they belong to an application during metrics aggregation.
|
|
1772
1963
|
const processLabels = { ...this.#config.metrics?.labels }
|
|
1773
1964
|
delete processLabels[this.#metricsLabelName]
|
|
1774
1965
|
this.#applyLabelsToMetrics(processMetricsJson, processLabels, processMetrics)
|
|
@@ -1933,155 +2124,6 @@ export class Runtime extends EventEmitter {
|
|
|
1933
2124
|
return output
|
|
1934
2125
|
}
|
|
1935
2126
|
|
|
1936
|
-
async getFormattedMetrics () {
|
|
1937
|
-
try {
|
|
1938
|
-
const { metrics } = await this.getMetrics()
|
|
1939
|
-
|
|
1940
|
-
if (metrics === null || metrics.length === 0) {
|
|
1941
|
-
return null
|
|
1942
|
-
}
|
|
1943
|
-
|
|
1944
|
-
const metricsNames = [
|
|
1945
|
-
'process_cpu_percent_usage',
|
|
1946
|
-
'process_resident_memory_bytes',
|
|
1947
|
-
'nodejs_heap_size_total_bytes',
|
|
1948
|
-
'nodejs_heap_size_used_bytes',
|
|
1949
|
-
'nodejs_heap_space_size_total_bytes',
|
|
1950
|
-
'nodejs_eventloop_utilization',
|
|
1951
|
-
'http_request_all_summary_seconds'
|
|
1952
|
-
]
|
|
1953
|
-
|
|
1954
|
-
const applicationsMetrics = {}
|
|
1955
|
-
|
|
1956
|
-
// Process-level metrics are reported only once for the whole runtime, without
|
|
1957
|
-
// an application label, since they are shared by all the applications running
|
|
1958
|
-
// in worker threads (see issue #3332). Applications running as separate OS
|
|
1959
|
-
// processes report their own labeled values, which take precedence below.
|
|
1960
|
-
const runtimeProcessMetrics = {}
|
|
1961
|
-
const applicationProcessMetrics = new Set()
|
|
1962
|
-
|
|
1963
|
-
for (const metric of metrics) {
|
|
1964
|
-
const { name, values } = metric
|
|
1965
|
-
|
|
1966
|
-
if (!metricsNames.includes(name)) continue
|
|
1967
|
-
if (!values || values.length === 0) continue
|
|
1968
|
-
|
|
1969
|
-
const labels = values[0].labels
|
|
1970
|
-
// Use the configured label name (serviceId for v2 compatibility, applicationId for v3+)
|
|
1971
|
-
const applicationId = labels?.[this.#metricsLabelName]
|
|
1972
|
-
|
|
1973
|
-
if (!applicationId) {
|
|
1974
|
-
if (name === 'process_cpu_percent_usage') {
|
|
1975
|
-
runtimeProcessMetrics.cpu = values[0].value
|
|
1976
|
-
continue
|
|
1977
|
-
}
|
|
1978
|
-
if (name === 'process_resident_memory_bytes') {
|
|
1979
|
-
runtimeProcessMetrics.rss = values[0].value
|
|
1980
|
-
continue
|
|
1981
|
-
}
|
|
1982
|
-
|
|
1983
|
-
throw new Error(`Missing ${this.#metricsLabelName} label in metrics`)
|
|
1984
|
-
}
|
|
1985
|
-
|
|
1986
|
-
if (name === 'process_cpu_percent_usage' || name === 'process_resident_memory_bytes') {
|
|
1987
|
-
applicationProcessMetrics.add(`${applicationId}:${name}`)
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
|
-
let applicationMetrics = applicationsMetrics[applicationId]
|
|
1991
|
-
if (!applicationMetrics) {
|
|
1992
|
-
applicationMetrics = {
|
|
1993
|
-
cpu: 0,
|
|
1994
|
-
rss: 0,
|
|
1995
|
-
totalHeapSize: 0,
|
|
1996
|
-
usedHeapSize: 0,
|
|
1997
|
-
newSpaceSize: 0,
|
|
1998
|
-
oldSpaceSize: 0,
|
|
1999
|
-
elu: 0,
|
|
2000
|
-
latency: {
|
|
2001
|
-
p50: 0,
|
|
2002
|
-
p90: 0,
|
|
2003
|
-
p95: 0,
|
|
2004
|
-
p99: 0
|
|
2005
|
-
}
|
|
2006
|
-
}
|
|
2007
|
-
applicationsMetrics[applicationId] = applicationMetrics
|
|
2008
|
-
}
|
|
2009
|
-
|
|
2010
|
-
parsePromMetric(applicationMetrics, metric)
|
|
2011
|
-
}
|
|
2012
|
-
|
|
2013
|
-
// Apply the runtime-wide process-level values to every application that did
|
|
2014
|
-
// not report its own (i.e. every application running in a worker thread).
|
|
2015
|
-
for (const [applicationId, applicationMetrics] of Object.entries(applicationsMetrics)) {
|
|
2016
|
-
if (
|
|
2017
|
-
runtimeProcessMetrics.cpu !== undefined &&
|
|
2018
|
-
!applicationProcessMetrics.has(`${applicationId}:process_cpu_percent_usage`)
|
|
2019
|
-
) {
|
|
2020
|
-
applicationMetrics.cpu = runtimeProcessMetrics.cpu
|
|
2021
|
-
}
|
|
2022
|
-
|
|
2023
|
-
if (
|
|
2024
|
-
runtimeProcessMetrics.rss !== undefined &&
|
|
2025
|
-
!applicationProcessMetrics.has(`${applicationId}:process_resident_memory_bytes`)
|
|
2026
|
-
) {
|
|
2027
|
-
applicationMetrics.rss = runtimeProcessMetrics.rss
|
|
2028
|
-
}
|
|
2029
|
-
}
|
|
2030
|
-
|
|
2031
|
-
function parsePromMetric (applicationMetrics, promMetric) {
|
|
2032
|
-
const { name } = promMetric
|
|
2033
|
-
|
|
2034
|
-
if (name === 'process_cpu_percent_usage') {
|
|
2035
|
-
applicationMetrics.cpu = promMetric.values[0].value
|
|
2036
|
-
return
|
|
2037
|
-
}
|
|
2038
|
-
if (name === 'process_resident_memory_bytes') {
|
|
2039
|
-
applicationMetrics.rss = promMetric.values[0].value
|
|
2040
|
-
return
|
|
2041
|
-
}
|
|
2042
|
-
if (name === 'nodejs_heap_size_total_bytes') {
|
|
2043
|
-
applicationMetrics.totalHeapSize = promMetric.values[0].value
|
|
2044
|
-
return
|
|
2045
|
-
}
|
|
2046
|
-
if (name === 'nodejs_heap_size_used_bytes') {
|
|
2047
|
-
applicationMetrics.usedHeapSize = promMetric.values[0].value
|
|
2048
|
-
return
|
|
2049
|
-
}
|
|
2050
|
-
if (name === 'nodejs_heap_space_size_total_bytes') {
|
|
2051
|
-
const newSpaceSize = promMetric.values.find(value => value.labels.space === 'new')
|
|
2052
|
-
const oldSpaceSize = promMetric.values.find(value => value.labels.space === 'old')
|
|
2053
|
-
|
|
2054
|
-
applicationMetrics.newSpaceSize = newSpaceSize.value
|
|
2055
|
-
applicationMetrics.oldSpaceSize = oldSpaceSize.value
|
|
2056
|
-
return
|
|
2057
|
-
}
|
|
2058
|
-
if (name === 'nodejs_eventloop_utilization') {
|
|
2059
|
-
applicationMetrics.elu = promMetric.values[0].value
|
|
2060
|
-
return
|
|
2061
|
-
}
|
|
2062
|
-
if (name === 'http_request_all_summary_seconds') {
|
|
2063
|
-
applicationMetrics.latency = {
|
|
2064
|
-
p50: promMetric.values.find(value => value.labels.quantile === 0.5)?.value || 0,
|
|
2065
|
-
p90: promMetric.values.find(value => value.labels.quantile === 0.9)?.value || 0,
|
|
2066
|
-
p95: promMetric.values.find(value => value.labels.quantile === 0.95)?.value || 0,
|
|
2067
|
-
p99: promMetric.values.find(value => value.labels.quantile === 0.99)?.value || 0
|
|
2068
|
-
}
|
|
2069
|
-
}
|
|
2070
|
-
}
|
|
2071
|
-
|
|
2072
|
-
return {
|
|
2073
|
-
version: 1,
|
|
2074
|
-
date: new Date().toISOString(),
|
|
2075
|
-
applications: applicationsMetrics
|
|
2076
|
-
}
|
|
2077
|
-
} catch (err) {
|
|
2078
|
-
// If any metric is missing, return nothing
|
|
2079
|
-
this.logger.warn({ err }, 'Cannot fetch metrics')
|
|
2080
|
-
|
|
2081
|
-
return null
|
|
2082
|
-
}
|
|
2083
|
-
}
|
|
2084
|
-
|
|
2085
2127
|
getSharedContext () {
|
|
2086
2128
|
return this.#sharedContext
|
|
2087
2129
|
}
|
|
@@ -2100,12 +2142,16 @@ export class Runtime extends EventEmitter {
|
|
|
2100
2142
|
}
|
|
2101
2143
|
|
|
2102
2144
|
async getApplications (allowUnloaded = false) {
|
|
2103
|
-
return {
|
|
2145
|
+
return frozenSnapshot({
|
|
2104
2146
|
production: this.#isProduction,
|
|
2105
2147
|
applications: await Promise.all(
|
|
2106
|
-
this.getApplicationsIds().map(id => this
|
|
2148
|
+
this.getApplicationsIds().map(id => this.#buildApplicationDetails(id, allowUnloaded))
|
|
2107
2149
|
)
|
|
2108
|
-
}
|
|
2150
|
+
})
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
async getApplicationDetails (id, allowUnloaded = false) {
|
|
2154
|
+
return frozenSnapshot(await this.#buildApplicationDetails(id, allowUnloaded))
|
|
2109
2155
|
}
|
|
2110
2156
|
|
|
2111
2157
|
async getApplicationMeta (id) {
|
|
@@ -2133,7 +2179,7 @@ export class Runtime extends EventEmitter {
|
|
|
2133
2179
|
}
|
|
2134
2180
|
}
|
|
2135
2181
|
|
|
2136
|
-
async
|
|
2182
|
+
async #buildApplicationDetails (id, allowUnloaded = false) {
|
|
2137
2183
|
let application
|
|
2138
2184
|
|
|
2139
2185
|
try {
|
|
@@ -2146,7 +2192,7 @@ export class Runtime extends EventEmitter {
|
|
|
2146
2192
|
throw e
|
|
2147
2193
|
}
|
|
2148
2194
|
|
|
2149
|
-
const { localUrl, config, path } = application[kConfig]
|
|
2195
|
+
const { localUrl, config, configPath, path } = application[kConfig]
|
|
2150
2196
|
|
|
2151
2197
|
const sourceMaps = application[kConfig].sourceMaps ?? this.#config.sourceMaps
|
|
2152
2198
|
const status = await sendViaITC(application, 'getStatus')
|
|
@@ -2155,7 +2201,6 @@ export class Runtime extends EventEmitter {
|
|
|
2155
2201
|
const applicationDetails = {
|
|
2156
2202
|
id,
|
|
2157
2203
|
type,
|
|
2158
|
-
config,
|
|
2159
2204
|
path,
|
|
2160
2205
|
status,
|
|
2161
2206
|
dependencies,
|
|
@@ -2164,6 +2209,27 @@ export class Runtime extends EventEmitter {
|
|
|
2164
2209
|
sourceMaps
|
|
2165
2210
|
}
|
|
2166
2211
|
|
|
2212
|
+
/*
|
|
2213
|
+
v3's `config` was the application's configuration file path. v4 evaluates that file main-side
|
|
2214
|
+
and hands the worker the payload, so the path becomes `configPath` and the entry's own
|
|
2215
|
+
`config` is no longer a path to report. Emitting the key that matches the dialect keeps a
|
|
2216
|
+
consumer from reading one and silently getting the other.
|
|
2217
|
+
*/
|
|
2218
|
+
if (typeof config === 'string') {
|
|
2219
|
+
applicationDetails.config = config
|
|
2220
|
+
} else if (configPath) {
|
|
2221
|
+
applicationDetails.configPath = configPath
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
// status is worker lifecycle; servingState is how the thing serves. They are different
|
|
2225
|
+
// questions, and the runtime gates URL emission on status === 'started', so they cannot share
|
|
2226
|
+
// a field.
|
|
2227
|
+
const servingState = this.#servingStates.get(id)
|
|
2228
|
+
|
|
2229
|
+
if (servingState) {
|
|
2230
|
+
applicationDetails.servingState = servingState
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2167
2233
|
if (this.#isProduction) {
|
|
2168
2234
|
applicationDetails.workers = this.#workers.getKeys(id).length
|
|
2169
2235
|
}
|
|
@@ -2431,6 +2497,17 @@ export class Runtime extends EventEmitter {
|
|
|
2431
2497
|
}
|
|
2432
2498
|
}
|
|
2433
2499
|
|
|
2500
|
+
async #refuseUnresolvedApplication (id) {
|
|
2501
|
+
const executable = getExecutable() ?? 'platformatic'
|
|
2502
|
+
|
|
2503
|
+
this.logger.error(
|
|
2504
|
+
`The path for application "%s" does not exist. Please run "${executable} resolve" and try again.`,
|
|
2505
|
+
id
|
|
2506
|
+
)
|
|
2507
|
+
|
|
2508
|
+
await this.closeAndThrow(new RuntimeAbortedError())
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2434
2511
|
async #setupApplication (applicationConfig) {
|
|
2435
2512
|
if (this.#status === 'stopping' || this.#status === 'closed') {
|
|
2436
2513
|
return
|
|
@@ -2446,13 +2523,7 @@ export class Runtime extends EventEmitter {
|
|
|
2446
2523
|
applicationConfig.path = join(this.#root, config.resolvedApplicationsBasePath, id)
|
|
2447
2524
|
|
|
2448
2525
|
if (!existsSync(applicationConfig.path)) {
|
|
2449
|
-
|
|
2450
|
-
this.logger.error(
|
|
2451
|
-
`The path for application "%s" does not exist. Please run "${executable} resolve" and try again.`,
|
|
2452
|
-
id
|
|
2453
|
-
)
|
|
2454
|
-
|
|
2455
|
-
await this.closeAndThrow(new RuntimeAbortedError())
|
|
2526
|
+
await this.#refuseUnresolvedApplication(id)
|
|
2456
2527
|
}
|
|
2457
2528
|
} else {
|
|
2458
2529
|
this.logger.error(
|
|
@@ -2462,6 +2533,15 @@ export class Runtime extends EventEmitter {
|
|
|
2462
2533
|
|
|
2463
2534
|
await this.closeAndThrow(new RuntimeAbortedError())
|
|
2464
2535
|
}
|
|
2536
|
+
} else if (applicationConfig.unresolved) {
|
|
2537
|
+
/*
|
|
2538
|
+
A remote application that declares where its clone belongs, and whose clone is not there --
|
|
2539
|
+
either the directory is missing or it holds no application at all. The pathless case above
|
|
2540
|
+
is the same state said differently, and both are what `resolve` exists to fix, so both get
|
|
2541
|
+
the message that names it rather than the capability detector's report that the directory
|
|
2542
|
+
holds nothing.
|
|
2543
|
+
*/
|
|
2544
|
+
await this.#refuseUnresolvedApplication(id)
|
|
2465
2545
|
}
|
|
2466
2546
|
|
|
2467
2547
|
let workers = applicationConfig.workers.static
|
|
@@ -2527,11 +2607,11 @@ export class Runtime extends EventEmitter {
|
|
|
2527
2607
|
inspectorOptions.port = inspectorOptions.port + this.#workers.size + 1
|
|
2528
2608
|
}
|
|
2529
2609
|
|
|
2530
|
-
if (config.
|
|
2531
|
-
applicationConfig.
|
|
2532
|
-
...config.
|
|
2533
|
-
...applicationConfig.
|
|
2534
|
-
applicationName: `${config.
|
|
2610
|
+
if (config.tracing) {
|
|
2611
|
+
applicationConfig.tracing = {
|
|
2612
|
+
...config.tracing,
|
|
2613
|
+
...applicationConfig.tracing,
|
|
2614
|
+
applicationName: `${config.tracing.applicationName}-${applicationConfig.id}`
|
|
2535
2615
|
}
|
|
2536
2616
|
}
|
|
2537
2617
|
|
|
@@ -2540,9 +2620,9 @@ export class Runtime extends EventEmitter {
|
|
|
2540
2620
|
|
|
2541
2621
|
const execArgv = applicationConfig.execArgv ?? []
|
|
2542
2622
|
|
|
2543
|
-
if (!applicationConfig.
|
|
2623
|
+
if (!applicationConfig.skipTracingHooks && config.tracing && config.tracing.enabled !== false) {
|
|
2544
2624
|
const require = createRequire(import.meta.url)
|
|
2545
|
-
const telemetryPath = require.resolve('@platformatic/
|
|
2625
|
+
const telemetryPath = require.resolve('@platformatic/tracing')
|
|
2546
2626
|
const openTelemetrySetupPath = join(telemetryPath, '..', 'lib', 'node-telemetry.js')
|
|
2547
2627
|
const hookUrl = pathToFileURL(require.resolve('@opentelemetry/instrumentation/hook.mjs'))
|
|
2548
2628
|
|
|
@@ -2598,6 +2678,7 @@ export class Runtime extends EventEmitter {
|
|
|
2598
2678
|
const worker = new Worker(kWorkerFile, {
|
|
2599
2679
|
workerData: {
|
|
2600
2680
|
config: workerConfig,
|
|
2681
|
+
meshId: this.#meshId,
|
|
2601
2682
|
applicationConfig: {
|
|
2602
2683
|
...applicationConfig,
|
|
2603
2684
|
isProduction: this.#isProduction,
|
|
@@ -2615,10 +2696,7 @@ export class Runtime extends EventEmitter {
|
|
|
2615
2696
|
codeRangeSizeMb
|
|
2616
2697
|
},
|
|
2617
2698
|
inspectorOptions,
|
|
2618
|
-
dirname: this.#root
|
|
2619
|
-
// Keys of the worker environment which only come from an env file of the runtime:
|
|
2620
|
-
// the env file of the application is allowed to override those.
|
|
2621
|
-
envFileFallbackKeys: this.#env[kEnvFileFallbackKeys] ?? []
|
|
2699
|
+
dirname: this.#root
|
|
2622
2700
|
},
|
|
2623
2701
|
argv: applicationConfig.arguments,
|
|
2624
2702
|
execArgv,
|
|
@@ -2739,6 +2817,10 @@ export class Runtime extends EventEmitter {
|
|
|
2739
2817
|
// Forward events from the worker
|
|
2740
2818
|
// Do not use emitAndNotify here since we don't want to forward unknown events
|
|
2741
2819
|
worker[kITC].on('event', ({ event, payload }) => {
|
|
2820
|
+
if (event === 'serverOptions') {
|
|
2821
|
+
worker[kWorkerServerOptions] = payload[0]
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2742
2824
|
event = `application:worker:event:${event}`
|
|
2743
2825
|
|
|
2744
2826
|
this.emit(event, ...payload, workerId, applicationId, index)
|
|
@@ -2924,11 +3006,6 @@ export class Runtime extends EventEmitter {
|
|
|
2924
3006
|
this.#workers.set(workerId, worker)
|
|
2925
3007
|
}
|
|
2926
3008
|
|
|
2927
|
-
// Setup the interceptor
|
|
2928
|
-
// kInterceptorReadyPromise resolves when the worker
|
|
2929
|
-
// is ready to receive requests: after calling the replaceServer method
|
|
2930
|
-
worker[kInterceptorReadyPromise] = this.#meshInterceptor.route(applicationId, worker)
|
|
2931
|
-
|
|
2932
3009
|
// Wait for initialization
|
|
2933
3010
|
try {
|
|
2934
3011
|
await waitEventFromITC(worker, 'init')
|
|
@@ -3369,12 +3446,6 @@ export class Runtime extends EventEmitter {
|
|
|
3369
3446
|
|
|
3370
3447
|
this.#recordWorkerUrl(worker, id, workerUrl)
|
|
3371
3448
|
|
|
3372
|
-
// Wait for the interceptor to be ready
|
|
3373
|
-
const interceptorResult = await executeWithTimeout(worker[kInterceptorReadyPromise], config.startTimeout)
|
|
3374
|
-
if (interceptorResult === kTimeout) {
|
|
3375
|
-
throw new WorkerInterceptorJoinTimeoutError(label, config.startTimeout)
|
|
3376
|
-
}
|
|
3377
|
-
|
|
3378
3449
|
worker[kWorkerStatus] = 'started'
|
|
3379
3450
|
worker[kWorkerStartTime] = Date.now()
|
|
3380
3451
|
|
|
@@ -3560,7 +3631,6 @@ export class Runtime extends EventEmitter {
|
|
|
3560
3631
|
}
|
|
3561
3632
|
|
|
3562
3633
|
async #discardWorker (worker) {
|
|
3563
|
-
await this.#meshInterceptor.unroute(worker[kApplicationId], worker, true)
|
|
3564
3634
|
worker.removeAllListeners('exit')
|
|
3565
3635
|
await worker.terminate()
|
|
3566
3636
|
|
|
@@ -3730,6 +3800,7 @@ export class Runtime extends EventEmitter {
|
|
|
3730
3800
|
|
|
3731
3801
|
const stopBeforeStart =
|
|
3732
3802
|
Boolean(worker[kWorkerUrl]) &&
|
|
3803
|
+
worker[kWorkerServerOptions]?.port !== 0 &&
|
|
3733
3804
|
(config.reuseTcpPorts === false || applicationConfig.reuseTcpPorts === false || !features.node.reusePort)
|
|
3734
3805
|
|
|
3735
3806
|
try {
|
|
@@ -3970,11 +4041,20 @@ export class Runtime extends EventEmitter {
|
|
|
3970
4041
|
}
|
|
3971
4042
|
|
|
3972
4043
|
async #getRuntimePackageJson () {
|
|
3973
|
-
const
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
4044
|
+
const packageJsonPath = join(this.#root, 'package.json')
|
|
4045
|
+
|
|
4046
|
+
/*
|
|
4047
|
+
A project need not have one, and a runtime that cannot report its metadata without one is a
|
|
4048
|
+
runtime whose `applications:add --save` stops working -- that command reads `projectDir`,
|
|
4049
|
+
`configPath` and `autoload` from this, and has done since `GET /config` was removed. The
|
|
4050
|
+
metadata already declares `packageName` and `packageVersion` as nullable, so absence is a
|
|
4051
|
+
state its shape allows.
|
|
4052
|
+
*/
|
|
4053
|
+
try {
|
|
4054
|
+
return JSON.parse(await readFile(packageJsonPath, 'utf8'))
|
|
4055
|
+
} catch {
|
|
4056
|
+
return {}
|
|
4057
|
+
}
|
|
3978
4058
|
}
|
|
3979
4059
|
|
|
3980
4060
|
#handleWorkerStandardStreams (worker, applicationId, workerId) {
|
|
@@ -4391,6 +4471,10 @@ export class Runtime extends EventEmitter {
|
|
|
4391
4471
|
report.started.push(newIndex)
|
|
4392
4472
|
startedWorkersCount++
|
|
4393
4473
|
}
|
|
4474
|
+
|
|
4475
|
+
// A worker added after boot re-runs the application's code, so it can answer differently
|
|
4476
|
+
// from the workers already running. Scale-up is the second place the answers must agree.
|
|
4477
|
+
await this.#collectServingState(applicationId)
|
|
4394
4478
|
report.success = true
|
|
4395
4479
|
} catch (err) {
|
|
4396
4480
|
if (pendingWorkerId) {
|
|
@@ -4478,6 +4562,10 @@ export class Runtime extends EventEmitter {
|
|
|
4478
4562
|
}
|
|
4479
4563
|
}
|
|
4480
4564
|
|
|
4565
|
+
if (applicationConfig.sourcePath) {
|
|
4566
|
+
allows.add(`--allow-fs-read=${join(applicationConfig.sourcePath, '*')}`)
|
|
4567
|
+
}
|
|
4568
|
+
|
|
4481
4569
|
if (write?.length) {
|
|
4482
4570
|
for (const p of write) {
|
|
4483
4571
|
allows.add(`--allow-fs-write=${isAbsolute(p) ? p : join(applicationConfig.path, p)}`)
|
|
@@ -4952,18 +5040,6 @@ export class Runtime extends EventEmitter {
|
|
|
4952
5040
|
this.#loggerContext.updatePrefixes(ids)
|
|
4953
5041
|
}
|
|
4954
5042
|
|
|
4955
|
-
#onMeshInterceptorError (error) {
|
|
4956
|
-
const worker = error.port
|
|
4957
|
-
|
|
4958
|
-
this.logger.error(
|
|
4959
|
-
{ err: ensureLoggableError(error.cause) },
|
|
4960
|
-
`The ${this.#workerExtendedLabel(worker[kApplicationId], worker[kWorkerId])} threw an error during mesh network setup. Replacing it ...`
|
|
4961
|
-
)
|
|
4962
|
-
|
|
4963
|
-
this.emit('application:worker:init:failed', { application: worker[kApplicationId], worker: worker[kWorkerId] })
|
|
4964
|
-
worker.terminate()
|
|
4965
|
-
}
|
|
4966
|
-
|
|
4967
5043
|
#getPortOwner (port, applicationId, hostname, includeSameApplication = false) {
|
|
4968
5044
|
if (!Number.isInteger(port) || port <= 0) {
|
|
4969
5045
|
return null
|