@platformatic/runtime 3.67.0 → 4.0.0-new-config.0
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 +5 -43
- package/index.d.ts +10 -12
- package/index.js +191 -6
- package/lib/config.js +125 -60
- package/lib/errors.js +5 -13
- package/lib/generator.js +19 -92
- package/lib/management-api.js +3 -5
- package/lib/management-handlers.js +3 -5
- package/lib/runtime.js +377 -183
- package/lib/schema.js +1 -1
- package/lib/upgrade.js +16 -1
- package/lib/versions/v4.0.0.js +31 -0
- package/lib/worker/controller.js +51 -55
- package/lib/worker/itc.js +5 -6
- package/lib/worker/main.js +4 -19
- package/lib/worker/symbols.js +2 -0
- package/lib/worker-scaler.js +8 -10
- package/package.json +17 -16
- package/schema.json +33 -147
package/lib/runtime.js
CHANGED
|
@@ -35,11 +35,12 @@ import { Agent, request, interceptors as undiciInterceptors } from 'undici'
|
|
|
35
35
|
import { createThreadInterceptor } from 'undici-thread-interceptor'
|
|
36
36
|
import { pprofCapturePreloadPath } from './config.js'
|
|
37
37
|
import {
|
|
38
|
+
AddressInUseError,
|
|
39
|
+
WorkerAddressInUseError,
|
|
38
40
|
ApplicationAlreadyStartedError,
|
|
39
41
|
ApplicationNotFoundError,
|
|
40
42
|
ApplicationNotStartedError,
|
|
41
43
|
ApplicationStartTimeoutError,
|
|
42
|
-
CannotRemoveEntrypointError,
|
|
43
44
|
DuplicateExtensionHealthCheckError,
|
|
44
45
|
DuplicateITCHandlerNameError,
|
|
45
46
|
ExtensionHealthRoutesUnavailableError,
|
|
@@ -85,6 +86,8 @@ import {
|
|
|
85
86
|
kStderrMarker,
|
|
86
87
|
kWorkerHealthSignals,
|
|
87
88
|
kWorkerId,
|
|
89
|
+
kWorkerPortOffset,
|
|
90
|
+
kWorkerUrl,
|
|
88
91
|
kWorkersBroadcast,
|
|
89
92
|
kWorkerStartTime,
|
|
90
93
|
kWorkerStatus
|
|
@@ -193,9 +196,6 @@ export class Runtime extends EventEmitter {
|
|
|
193
196
|
#sharedContext
|
|
194
197
|
#isProduction
|
|
195
198
|
#concurrency
|
|
196
|
-
#entrypointId
|
|
197
|
-
#url
|
|
198
|
-
#entrypointPort
|
|
199
199
|
|
|
200
200
|
#healthMetricsTimer
|
|
201
201
|
#healthMetricsCollectionActive
|
|
@@ -225,9 +225,9 @@ export class Runtime extends EventEmitter {
|
|
|
225
225
|
#lastOverloadProfiles
|
|
226
226
|
#restartingApplications
|
|
227
227
|
#restartingWorkers
|
|
228
|
+
#workerPortOffsets
|
|
228
229
|
#dynamicWorkersScaler
|
|
229
230
|
#nextWorkerIndex
|
|
230
|
-
#workerPortOffsets
|
|
231
231
|
|
|
232
232
|
#sharedHttpCache
|
|
233
233
|
#scheduler
|
|
@@ -254,8 +254,6 @@ export class Runtime extends EventEmitter {
|
|
|
254
254
|
this.#applications = new Map()
|
|
255
255
|
this.#applicationRestartCounts = new Map()
|
|
256
256
|
this.#workers = new RoundRobinMap()
|
|
257
|
-
this.#url = undefined
|
|
258
|
-
this.#entrypointPort = undefined
|
|
259
257
|
this.#channelCreationHook = createChannelCreationHook(this.#config)
|
|
260
258
|
this.#meshInterceptor = createThreadInterceptor({
|
|
261
259
|
domain: '.plt.local',
|
|
@@ -268,8 +266,8 @@ export class Runtime extends EventEmitter {
|
|
|
268
266
|
this.#status = undefined
|
|
269
267
|
this.#restartingApplications = new Set()
|
|
270
268
|
this.#restartingWorkers = new Map()
|
|
271
|
-
this.#nextWorkerIndex = new Map()
|
|
272
269
|
this.#workerPortOffsets = new Map() // fullWorkerId => portOffset
|
|
270
|
+
this.#nextWorkerIndex = new Map()
|
|
273
271
|
this.#sharedHttpCache = null
|
|
274
272
|
this.#applicationsConfigsPatches = new Map()
|
|
275
273
|
|
|
@@ -406,7 +404,7 @@ export class Runtime extends EventEmitter {
|
|
|
406
404
|
if (this.getApplicationsIds().length === 0) {
|
|
407
405
|
this.#updateStatus('started')
|
|
408
406
|
await this.close(silent)
|
|
409
|
-
return
|
|
407
|
+
return {}
|
|
410
408
|
}
|
|
411
409
|
|
|
412
410
|
if (this.#config.inspectorOptions) {
|
|
@@ -453,10 +451,8 @@ export class Runtime extends EventEmitter {
|
|
|
453
451
|
this.#startHealthMetricsCollectionIfNeeded()
|
|
454
452
|
|
|
455
453
|
await this.#dynamicWorkersScaler?.start()
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
}
|
|
459
|
-
return this.#url
|
|
454
|
+
this.#showUrls()
|
|
455
|
+
return this.getUrls()
|
|
460
456
|
}
|
|
461
457
|
|
|
462
458
|
async stop (silent = false) {
|
|
@@ -485,11 +481,6 @@ export class Runtime extends EventEmitter {
|
|
|
485
481
|
|
|
486
482
|
await this.#dynamicWorkersScaler?.stop()
|
|
487
483
|
|
|
488
|
-
// Stop the entrypoint first so that no new requests are accepted
|
|
489
|
-
if (this.#entrypointId) {
|
|
490
|
-
await this.stopApplication(this.#entrypointId, silent)
|
|
491
|
-
}
|
|
492
|
-
|
|
493
484
|
// Await extension stop hooks before stopping remaining applications so that
|
|
494
485
|
// control-plane extensions can settle work and hand off state first.
|
|
495
486
|
await this.#stopExtensions()
|
|
@@ -514,8 +505,6 @@ export class Runtime extends EventEmitter {
|
|
|
514
505
|
await this.restartApplications(toRestart)
|
|
515
506
|
|
|
516
507
|
this.emitAndNotify('restarted')
|
|
517
|
-
|
|
518
|
-
return this.#url
|
|
519
508
|
}
|
|
520
509
|
|
|
521
510
|
async close (silent = false) {
|
|
@@ -667,25 +656,8 @@ export class Runtime extends EventEmitter {
|
|
|
667
656
|
|
|
668
657
|
async addApplications (applications, start = false) {
|
|
669
658
|
const setupInvocations = []
|
|
670
|
-
// Per-worker ports do not need SO_REUSEPORT because each worker binds a different port.
|
|
671
|
-
const usesPerWorkerPorts = this.#config.server?.portAssignment === 'perWorkerIncrement'
|
|
672
|
-
|
|
673
659
|
const toStart = []
|
|
674
660
|
for (const application of applications) {
|
|
675
|
-
const workers = application.workers
|
|
676
|
-
|
|
677
|
-
if (
|
|
678
|
-
(workers.static > 1 || workers.minimum > 1) &&
|
|
679
|
-
application.entrypoint &&
|
|
680
|
-
!features.node.reusePort &&
|
|
681
|
-
!usesPerWorkerPorts
|
|
682
|
-
) {
|
|
683
|
-
this.logger.warn(
|
|
684
|
-
`"${application.id}" is set as the entrypoint, but reusePort is not available in your OS; setting workers to 1 instead of ${workers.static}`
|
|
685
|
-
)
|
|
686
|
-
application.workers = { dynamic: false, static: 1 }
|
|
687
|
-
}
|
|
688
|
-
|
|
689
661
|
this.#applications.set(application.id, application)
|
|
690
662
|
this.#applicationRestartCounts.set(application.id, this.#applicationRestartCounts.get(application.id) ?? 0)
|
|
691
663
|
setupInvocations.push([application])
|
|
@@ -695,7 +667,7 @@ export class Runtime extends EventEmitter {
|
|
|
695
667
|
await executeInParallel(this.#setupApplication.bind(this), setupInvocations, this.#concurrency)
|
|
696
668
|
|
|
697
669
|
for (const application of applications) {
|
|
698
|
-
this.logger.debug(`Added application "${application.id}"
|
|
670
|
+
this.logger.debug(`Added application "${application.id}".`)
|
|
699
671
|
this.emitAndNotify('application:added', application)
|
|
700
672
|
}
|
|
701
673
|
|
|
@@ -713,10 +685,6 @@ export class Runtime extends EventEmitter {
|
|
|
713
685
|
}
|
|
714
686
|
|
|
715
687
|
async removeApplications (applications, silent = false) {
|
|
716
|
-
if (applications.includes(this.#entrypointId)) {
|
|
717
|
-
throw new CannotRemoveEntrypointError()
|
|
718
|
-
}
|
|
719
|
-
|
|
720
688
|
const removed = []
|
|
721
689
|
for (const application of applications) {
|
|
722
690
|
if (!this.#applications.has(application)) {
|
|
@@ -780,16 +748,21 @@ export class Runtime extends EventEmitter {
|
|
|
780
748
|
|
|
781
749
|
if (!skipDependencies) {
|
|
782
750
|
try {
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
751
|
+
const details = await executeWithTimeout(
|
|
752
|
+
this.getApplications(true),
|
|
753
|
+
this.#config.gracefulShutdown.application
|
|
754
|
+
)
|
|
755
|
+
if (details !== kTimeout) {
|
|
756
|
+
for (const application of details.applications) {
|
|
757
|
+
for (const dependency of application.dependencies ?? []) {
|
|
758
|
+
let applicationDependents = dependents[dependency]
|
|
759
|
+
if (!applicationDependents) {
|
|
760
|
+
applicationDependents = new Set()
|
|
761
|
+
dependents[dependency] = applicationDependents
|
|
762
|
+
}
|
|
791
763
|
|
|
792
|
-
|
|
764
|
+
applicationDependents.add(application.id)
|
|
765
|
+
}
|
|
793
766
|
}
|
|
794
767
|
}
|
|
795
768
|
} catch (e) {
|
|
@@ -798,11 +771,6 @@ export class Runtime extends EventEmitter {
|
|
|
798
771
|
}
|
|
799
772
|
|
|
800
773
|
for (const application of applicationsToStop) {
|
|
801
|
-
// The entrypoint has been stopped above
|
|
802
|
-
if (application === this.#entrypointId) {
|
|
803
|
-
continue
|
|
804
|
-
}
|
|
805
|
-
|
|
806
774
|
stopInvocations.push([application, silent, Array.from(dependents[application] ?? [])])
|
|
807
775
|
}
|
|
808
776
|
|
|
@@ -810,13 +778,39 @@ export class Runtime extends EventEmitter {
|
|
|
810
778
|
}
|
|
811
779
|
|
|
812
780
|
async restartApplications (applicationsToRestart) {
|
|
813
|
-
const restartInvocations = []
|
|
781
|
+
const restartInvocations = applicationsToRestart.map(application => [application, true])
|
|
782
|
+
const restarts = await executeInParallel(
|
|
783
|
+
this.restartApplication.bind(this),
|
|
784
|
+
restartInvocations,
|
|
785
|
+
this.#concurrency,
|
|
786
|
+
false
|
|
787
|
+
)
|
|
788
|
+
const failed = restarts.filter(result => result instanceof Error)
|
|
789
|
+
const succeeded = restarts.filter(result => result && !(result instanceof Error))
|
|
790
|
+
|
|
791
|
+
if (failed.length > 0) {
|
|
792
|
+
await Promise.allSettled(succeeded.map(restart => restart.discard()))
|
|
793
|
+
throw failed[0]
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const results = await executeInParallel(
|
|
797
|
+
retirement => retirement.retire(),
|
|
798
|
+
succeeded.flatMap(({ retirements }) => retirements.map(retirement => [retirement])),
|
|
799
|
+
this.#concurrency,
|
|
800
|
+
false
|
|
801
|
+
)
|
|
814
802
|
|
|
815
|
-
|
|
816
|
-
|
|
803
|
+
const error = results.find(result => result instanceof Error)
|
|
804
|
+
if (error) {
|
|
805
|
+
for (const restart of succeeded) {
|
|
806
|
+
restart.release()
|
|
807
|
+
}
|
|
808
|
+
throw error
|
|
817
809
|
}
|
|
818
810
|
|
|
819
|
-
|
|
811
|
+
for (const restart of succeeded) {
|
|
812
|
+
restart.complete()
|
|
813
|
+
}
|
|
820
814
|
}
|
|
821
815
|
|
|
822
816
|
async startApplication (id, silent = false) {
|
|
@@ -873,7 +867,7 @@ export class Runtime extends EventEmitter {
|
|
|
873
867
|
this.emitAndNotify('application:stopped', id)
|
|
874
868
|
}
|
|
875
869
|
|
|
876
|
-
async restartApplication (id) {
|
|
870
|
+
async restartApplication (id, deferOldWorkerRetirement = false) {
|
|
877
871
|
const applicationConfig = this.#applications.get(id)
|
|
878
872
|
|
|
879
873
|
if (!applicationConfig) {
|
|
@@ -892,6 +886,8 @@ export class Runtime extends EventEmitter {
|
|
|
892
886
|
}
|
|
893
887
|
|
|
894
888
|
this.#restartingApplications.add(id)
|
|
889
|
+
const retirements = []
|
|
890
|
+
let deferred = false
|
|
895
891
|
|
|
896
892
|
try {
|
|
897
893
|
const config = this.#config
|
|
@@ -909,14 +905,45 @@ export class Runtime extends EventEmitter {
|
|
|
909
905
|
await sleep(config.workersRestartDelay)
|
|
910
906
|
}
|
|
911
907
|
|
|
912
|
-
await this.#replaceWorker(
|
|
908
|
+
const retirement = await this.#replaceWorker(
|
|
909
|
+
config,
|
|
910
|
+
applicationConfig,
|
|
911
|
+
workersCount,
|
|
912
|
+
id,
|
|
913
|
+
workerIndex,
|
|
914
|
+
worker,
|
|
915
|
+
true,
|
|
916
|
+
deferOldWorkerRetirement
|
|
917
|
+
)
|
|
918
|
+
if (retirement) {
|
|
919
|
+
retirements.push(retirement)
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
if (deferOldWorkerRetirement) {
|
|
924
|
+
deferred = true
|
|
925
|
+
return {
|
|
926
|
+
retirements,
|
|
927
|
+
release: () => this.#restartingApplications.delete(id),
|
|
928
|
+
discard: async () => {
|
|
929
|
+
await Promise.allSettled(retirements.map(retirement => retirement.discard()))
|
|
930
|
+
this.#restartingApplications.delete(id)
|
|
931
|
+
},
|
|
932
|
+
complete: () => {
|
|
933
|
+
this.#incrementApplicationRestartCount(id)
|
|
934
|
+
this.emitAndNotify('application:restarted', id)
|
|
935
|
+
this.#restartingApplications.delete(id)
|
|
936
|
+
}
|
|
937
|
+
}
|
|
913
938
|
}
|
|
914
939
|
|
|
915
940
|
await this.#registerApplicationSchedulerJobs(id)
|
|
916
941
|
this.#incrementApplicationRestartCount(id)
|
|
917
942
|
this.emitAndNotify('application:restarted', id)
|
|
918
943
|
} finally {
|
|
919
|
-
|
|
944
|
+
if (!deferred) {
|
|
945
|
+
this.#restartingApplications.delete(id)
|
|
946
|
+
}
|
|
920
947
|
}
|
|
921
948
|
}
|
|
922
949
|
|
|
@@ -1524,8 +1551,20 @@ export class Runtime extends EventEmitter {
|
|
|
1524
1551
|
return this.#root
|
|
1525
1552
|
}
|
|
1526
1553
|
|
|
1527
|
-
|
|
1528
|
-
|
|
1554
|
+
getUrls (applicationId) {
|
|
1555
|
+
const urls = {}
|
|
1556
|
+
const applicationIds = applicationId ? [applicationId] : this.#applications.keys()
|
|
1557
|
+
|
|
1558
|
+
for (const id of applicationIds) {
|
|
1559
|
+
for (const workerId of this.#workers.getKeys(id)) {
|
|
1560
|
+
const url = this.#workers.get(workerId)?.[kWorkerUrl]
|
|
1561
|
+
if (url) {
|
|
1562
|
+
urls[workerId] = url
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
return urls
|
|
1529
1568
|
}
|
|
1530
1569
|
|
|
1531
1570
|
getRuntimeStatus () {
|
|
@@ -1534,8 +1573,6 @@ export class Runtime extends EventEmitter {
|
|
|
1534
1573
|
|
|
1535
1574
|
async getRuntimeMetadata () {
|
|
1536
1575
|
const packageJson = await this.#getRuntimePackageJson()
|
|
1537
|
-
const entrypointDetails = await this.getEntrypointDetails()
|
|
1538
|
-
|
|
1539
1576
|
return {
|
|
1540
1577
|
pid: process.pid,
|
|
1541
1578
|
cwd: process.cwd(),
|
|
@@ -1546,8 +1583,8 @@ export class Runtime extends EventEmitter {
|
|
|
1546
1583
|
projectDir: this.#root,
|
|
1547
1584
|
packageName: packageJson.name ?? null,
|
|
1548
1585
|
packageVersion: packageJson.version ?? null,
|
|
1549
|
-
|
|
1550
|
-
|
|
1586
|
+
platformaticVersion: version,
|
|
1587
|
+
urls: this.getUrls()
|
|
1551
1588
|
}
|
|
1552
1589
|
}
|
|
1553
1590
|
|
|
@@ -1580,14 +1617,6 @@ export class Runtime extends EventEmitter {
|
|
|
1580
1617
|
return this.#managementApi?.server.address() ?? null
|
|
1581
1618
|
}
|
|
1582
1619
|
|
|
1583
|
-
async getEntrypointDetails () {
|
|
1584
|
-
if (!this.#entrypointId) {
|
|
1585
|
-
return null
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
return this.getApplicationDetails(this.#entrypointId)
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
1620
|
async getCustomHealthChecks () {
|
|
1592
1621
|
const invocations = []
|
|
1593
1622
|
|
|
@@ -2067,12 +2096,11 @@ export class Runtime extends EventEmitter {
|
|
|
2067
2096
|
}
|
|
2068
2097
|
|
|
2069
2098
|
getApplicationsIds () {
|
|
2070
|
-
return Array.from(this.#applications.keys())
|
|
2099
|
+
return Array.from(this.#applications.keys()).sort()
|
|
2071
2100
|
}
|
|
2072
2101
|
|
|
2073
2102
|
async getApplications (allowUnloaded = false) {
|
|
2074
2103
|
return {
|
|
2075
|
-
entrypoint: this.#entrypointId,
|
|
2076
2104
|
production: this.#isProduction,
|
|
2077
2105
|
applications: await Promise.all(
|
|
2078
2106
|
this.getApplicationsIds().map(id => this.getApplicationDetails(id, allowUnloaded))
|
|
@@ -2118,7 +2146,7 @@ export class Runtime extends EventEmitter {
|
|
|
2118
2146
|
throw e
|
|
2119
2147
|
}
|
|
2120
2148
|
|
|
2121
|
-
const {
|
|
2149
|
+
const { localUrl, config, path } = application[kConfig]
|
|
2122
2150
|
|
|
2123
2151
|
const sourceMaps = application[kConfig].sourceMaps ?? this.#config.sourceMaps
|
|
2124
2152
|
const status = await sendViaITC(application, 'getStatus')
|
|
@@ -2133,7 +2161,6 @@ export class Runtime extends EventEmitter {
|
|
|
2133
2161
|
dependencies,
|
|
2134
2162
|
version,
|
|
2135
2163
|
localUrl,
|
|
2136
|
-
entrypoint,
|
|
2137
2164
|
sourceMaps
|
|
2138
2165
|
}
|
|
2139
2166
|
|
|
@@ -2141,9 +2168,9 @@ export class Runtime extends EventEmitter {
|
|
|
2141
2168
|
applicationDetails.workers = this.#workers.getKeys(id).length
|
|
2142
2169
|
}
|
|
2143
2170
|
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2171
|
+
const urls = status === 'started' ? Object.values(this.getUrls(id)) : []
|
|
2172
|
+
applicationDetails.urls = urls
|
|
2173
|
+
applicationDetails.url = urls[0] ?? null
|
|
2147
2174
|
|
|
2148
2175
|
return applicationDetails
|
|
2149
2176
|
}
|
|
@@ -2260,6 +2287,7 @@ export class Runtime extends EventEmitter {
|
|
|
2260
2287
|
worker: index,
|
|
2261
2288
|
status: worker[kWorkerStatus],
|
|
2262
2289
|
thread: worker.threadId,
|
|
2290
|
+
url: worker[kWorkerUrl] ?? null,
|
|
2263
2291
|
raw: includeRaw ? worker : undefined
|
|
2264
2292
|
}
|
|
2265
2293
|
}
|
|
@@ -2381,12 +2409,26 @@ export class Runtime extends EventEmitter {
|
|
|
2381
2409
|
this.emitAndNotify(status, args)
|
|
2382
2410
|
}
|
|
2383
2411
|
|
|
2384
|
-
#
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2412
|
+
#showUrls (workerIds) {
|
|
2413
|
+
const whitelist = workerIds ? new Set(workerIds) : null
|
|
2414
|
+
|
|
2415
|
+
for (const applicationId of this.#applications.keys()) {
|
|
2416
|
+
for (const workerId of this.#workers.getKeys(applicationId)) {
|
|
2417
|
+
if (whitelist && !whitelist.has(workerId)) {
|
|
2418
|
+
continue
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
const worker = this.#workers.get(workerId)
|
|
2422
|
+
const url = worker?.[kWorkerUrl]
|
|
2423
|
+
if (!url) {
|
|
2424
|
+
continue
|
|
2425
|
+
}
|
|
2388
2426
|
|
|
2389
|
-
|
|
2427
|
+
this.logger.info(
|
|
2428
|
+
`Platformatic is now listening at ${url} for ${this.#workerExtendedLabel(applicationId, worker[kWorkerId])}`
|
|
2429
|
+
)
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2390
2432
|
}
|
|
2391
2433
|
|
|
2392
2434
|
async #setupApplication (applicationConfig) {
|
|
@@ -2422,11 +2464,37 @@ export class Runtime extends EventEmitter {
|
|
|
2422
2464
|
}
|
|
2423
2465
|
}
|
|
2424
2466
|
|
|
2425
|
-
|
|
2467
|
+
let workers = applicationConfig.workers.static
|
|
2426
2468
|
const setupInvocations = []
|
|
2427
2469
|
|
|
2428
|
-
|
|
2429
|
-
|
|
2470
|
+
// All the workers of the application are (re)created, so their port offsets match their indexes
|
|
2471
|
+
for (const workerId of this.#workerPortOffsets.keys()) {
|
|
2472
|
+
if (workerId.slice(0, workerId.lastIndexOf(':')) === id) {
|
|
2473
|
+
this.#workerPortOffsets.delete(workerId)
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
let firstIndex = 0
|
|
2478
|
+
|
|
2479
|
+
// On platforms where reusePort is not available, multiple workers cannot listen on the same fixed port.
|
|
2480
|
+
// The listener configuration is owned by the capability, so it can only be inspected after setting up the first
|
|
2481
|
+
// worker: if the application would try to share a fixed port between workers, clamp it to a single worker.
|
|
2482
|
+
if (!features.node.reusePort && (workers > 1 || applicationConfig.workers.dynamic)) {
|
|
2483
|
+
const worker = await this.#setupWorker(config, applicationConfig, workers, id, 0)
|
|
2484
|
+
firstIndex = 1
|
|
2485
|
+
|
|
2486
|
+
if (await this.#usesSharedFixedPort(worker)) {
|
|
2487
|
+
this.logger.warn(
|
|
2488
|
+
`The application "${id}" is configured to listen on a fixed port with multiple workers, but reusePort is not available in your OS. ${workers > 1 ? `Setting workers to 1 instead of ${workers}` : 'Disabling dynamic workers scaling'}. To run multiple workers, set "server.portAssignment" to "perWorkerIncrement" in the application configuration.`
|
|
2489
|
+
)
|
|
2490
|
+
|
|
2491
|
+
applicationConfig.workers = { dynamic: false, static: 1 }
|
|
2492
|
+
workers = 1
|
|
2493
|
+
await sendViaITC(worker, 'updateWorkersCount', { applicationId: id, workers })
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
for (let i = firstIndex; i < workers; i++) {
|
|
2430
2498
|
setupInvocations.push([config, applicationConfig, workers, id, i])
|
|
2431
2499
|
}
|
|
2432
2500
|
|
|
@@ -2443,6 +2511,11 @@ export class Runtime extends EventEmitter {
|
|
|
2443
2511
|
const restartOnError = this.#getApplicationRestartOnError(config, applicationConfig)
|
|
2444
2512
|
const workerId = `${applicationId}:${index}`
|
|
2445
2513
|
|
|
2514
|
+
// The port offset is the slot the worker occupies when the application uses server.portAssignment=perWorkerIncrement.
|
|
2515
|
+
// It matches the worker index unless the worker was created to replace another one, in which case it inherits its offset.
|
|
2516
|
+
const portOffset = this.#workerPortOffsets.get(workerId) ?? index
|
|
2517
|
+
this.#workerPortOffsets.set(workerId, portOffset)
|
|
2518
|
+
|
|
2446
2519
|
// Handle inspector
|
|
2447
2520
|
let inspectorOptions
|
|
2448
2521
|
|
|
@@ -2454,12 +2527,6 @@ export class Runtime extends EventEmitter {
|
|
|
2454
2527
|
inspectorOptions.port = inspectorOptions.port + this.#workers.size + 1
|
|
2455
2528
|
}
|
|
2456
2529
|
|
|
2457
|
-
let serverConfigOverride
|
|
2458
|
-
if (this.#config.server?.portAssignment === 'perWorkerIncrement') {
|
|
2459
|
-
const portOffset = this.#workerPortOffsets.get(workerId)
|
|
2460
|
-
serverConfigOverride = { port: Number(this.#config.server.port) + portOffset }
|
|
2461
|
-
}
|
|
2462
|
-
|
|
2463
2530
|
if (config.telemetry) {
|
|
2464
2531
|
applicationConfig.telemetry = {
|
|
2465
2532
|
...config.telemetry,
|
|
@@ -2500,7 +2567,10 @@ export class Runtime extends EventEmitter {
|
|
|
2500
2567
|
preload = preload.filter(p => p !== pprofCapturePath)
|
|
2501
2568
|
}
|
|
2502
2569
|
|
|
2503
|
-
|
|
2570
|
+
// v4 resolves each application's worker environment main-side, with its own env-file chain, the
|
|
2571
|
+
// two env blocks and the injected topology URLs. v3 seeded every worker from one loadEnv at the
|
|
2572
|
+
// runtime root, which is what #env still holds.
|
|
2573
|
+
const workerEnv = structuredClone(applicationConfig.workerEnv ?? this.#env)
|
|
2504
2574
|
|
|
2505
2575
|
if (applicationConfig.nodeOptions?.trim().length > 0) {
|
|
2506
2576
|
const originalNodeOptions = workerEnv.NODE_OPTIONS ?? ''
|
|
@@ -2523,17 +2593,7 @@ export class Runtime extends EventEmitter {
|
|
|
2523
2593
|
const maxYoungGenerationSizeMb = maxYoungGeneration ? Math.floor(maxYoungGeneration / (1024 * 1024)) : undefined
|
|
2524
2594
|
const codeRangeSizeMb = codeRangeSize ? Math.floor(codeRangeSize / (1024 * 1024)) : undefined
|
|
2525
2595
|
|
|
2526
|
-
const workerConfig = {
|
|
2527
|
-
...config,
|
|
2528
|
-
preload
|
|
2529
|
-
}
|
|
2530
|
-
|
|
2531
|
-
if (config.server && serverConfigOverride) {
|
|
2532
|
-
workerConfig.server = {
|
|
2533
|
-
...config.server,
|
|
2534
|
-
...serverConfigOverride
|
|
2535
|
-
}
|
|
2536
|
-
}
|
|
2596
|
+
const workerConfig = { ...config, preload }
|
|
2537
2597
|
|
|
2538
2598
|
const worker = new Worker(kWorkerFile, {
|
|
2539
2599
|
workerData: {
|
|
@@ -2546,7 +2606,8 @@ export class Runtime extends EventEmitter {
|
|
|
2546
2606
|
worker: {
|
|
2547
2607
|
id: workerId,
|
|
2548
2608
|
index,
|
|
2549
|
-
count: workersCount
|
|
2609
|
+
count: workersCount,
|
|
2610
|
+
portOffset
|
|
2550
2611
|
},
|
|
2551
2612
|
resourceLimits: {
|
|
2552
2613
|
maxOldGenerationSizeMb,
|
|
@@ -2589,7 +2650,6 @@ export class Runtime extends EventEmitter {
|
|
|
2589
2650
|
worker[kWorkerStatus] = 'exited'
|
|
2590
2651
|
this.emitAndNotify('application:worker:exited', eventPayload)
|
|
2591
2652
|
|
|
2592
|
-
const portOffset = this.#workerPortOffsets.get(worker[kFullId]) ?? index
|
|
2593
2653
|
this.#cleanupWorker(worker)
|
|
2594
2654
|
|
|
2595
2655
|
if (this.#status === 'stopping') {
|
|
@@ -2614,11 +2674,18 @@ export class Runtime extends EventEmitter {
|
|
|
2614
2674
|
this.logger.warn(`The ${errorLabel} will be restarted in ${restartOnError}ms ...`)
|
|
2615
2675
|
}
|
|
2616
2676
|
|
|
2617
|
-
this.#restartCrashedWorker(
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2677
|
+
this.#restartCrashedWorker(
|
|
2678
|
+
config,
|
|
2679
|
+
applicationConfig,
|
|
2680
|
+
workersCount,
|
|
2681
|
+
applicationId,
|
|
2682
|
+
index,
|
|
2683
|
+
false,
|
|
2684
|
+
0,
|
|
2685
|
+
worker[kWorkerPortOffset]
|
|
2686
|
+
).catch(err => {
|
|
2687
|
+
this.logger.error({ err: ensureLoggableError(err) }, `${errorLabel} could not be restarted.`)
|
|
2688
|
+
})
|
|
2622
2689
|
} else {
|
|
2623
2690
|
this.emitAndNotify('application:worker:unvailable', eventPayload)
|
|
2624
2691
|
this.logger.warn(`The ${errorLabel} is no longer available.`)
|
|
@@ -2631,6 +2698,7 @@ export class Runtime extends EventEmitter {
|
|
|
2631
2698
|
worker[kFullId] = workerId
|
|
2632
2699
|
worker[kApplicationId] = applicationId
|
|
2633
2700
|
worker[kWorkerId] = index
|
|
2701
|
+
worker[kWorkerPortOffset] = portOffset
|
|
2634
2702
|
worker[kWorkerStatus] = 'boot'
|
|
2635
2703
|
|
|
2636
2704
|
if (inspectorOptions) {
|
|
@@ -2844,9 +2912,7 @@ export class Runtime extends EventEmitter {
|
|
|
2844
2912
|
this.logger.info(`The application "${applicationId}" has been successfully reloaded.`)
|
|
2845
2913
|
this.emitAndNotify('application:worker:reloaded', eventPayload)
|
|
2846
2914
|
|
|
2847
|
-
|
|
2848
|
-
this.#showUrl()
|
|
2849
|
-
}
|
|
2915
|
+
this.#showUrls(this.#workers.getKeys(applicationId))
|
|
2850
2916
|
} catch (e) {
|
|
2851
2917
|
this.logger.error(e)
|
|
2852
2918
|
}
|
|
@@ -2881,13 +2947,11 @@ export class Runtime extends EventEmitter {
|
|
|
2881
2947
|
}
|
|
2882
2948
|
|
|
2883
2949
|
this.#workers.delete(workerId)
|
|
2950
|
+
// The exit handler of the failed worker might have removed the port offset, restore it for the next attempt
|
|
2951
|
+
this.#workerPortOffsets.set(workerId, portOffset)
|
|
2884
2952
|
return this.#setupWorker(config, applicationConfig, workersCount, applicationId, index, enabled, attempt + 1)
|
|
2885
2953
|
}
|
|
2886
2954
|
|
|
2887
|
-
if (applicationConfig.entrypoint) {
|
|
2888
|
-
this.#entrypointId = applicationId
|
|
2889
|
-
}
|
|
2890
|
-
|
|
2891
2955
|
worker[kConfig] = { ...applicationConfig, health, workers: workersCount }
|
|
2892
2956
|
worker[kWorkerStatus] = 'init'
|
|
2893
2957
|
this.emitAndNotify('application:worker:init', eventPayload)
|
|
@@ -3302,23 +3366,8 @@ export class Runtime extends EventEmitter {
|
|
|
3302
3366
|
|
|
3303
3367
|
const { url: workerUrl, scheduledTasks } = workerStartResult
|
|
3304
3368
|
worker[kWorkerScheduledTasks] = scheduledTasks
|
|
3305
|
-
if (workerUrl) {
|
|
3306
|
-
this.#url = workerUrl
|
|
3307
3369
|
|
|
3308
|
-
|
|
3309
|
-
// stopBeforeStart when reuseTcpPorts is false) bind to the same port
|
|
3310
|
-
// instead of getting a new random one.
|
|
3311
|
-
if (applicationConfig.entrypoint) {
|
|
3312
|
-
try {
|
|
3313
|
-
const boundPort = Number(new URL(workerUrl).port)
|
|
3314
|
-
if (boundPort) {
|
|
3315
|
-
this.#entrypointPort = boundPort
|
|
3316
|
-
}
|
|
3317
|
-
} catch {
|
|
3318
|
-
// URL parsing failed, leave unchanged
|
|
3319
|
-
}
|
|
3320
|
-
}
|
|
3321
|
-
}
|
|
3370
|
+
this.#recordWorkerUrl(worker, id, workerUrl)
|
|
3322
3371
|
|
|
3323
3372
|
// Wait for the interceptor to be ready
|
|
3324
3373
|
const interceptorResult = await executeWithTimeout(worker[kInterceptorReadyPromise], config.startTimeout)
|
|
@@ -3348,12 +3397,21 @@ export class Runtime extends EventEmitter {
|
|
|
3348
3397
|
).unref()
|
|
3349
3398
|
}
|
|
3350
3399
|
} catch (err) {
|
|
3351
|
-
|
|
3400
|
+
let error = ensureError(err)
|
|
3352
3401
|
worker[kITC].notify('application:worker:start:processed')
|
|
3353
|
-
const portOffset = this.#workerPortOffsets.get(worker[kFullId]) ?? index
|
|
3354
3402
|
|
|
3355
|
-
|
|
3356
|
-
|
|
3403
|
+
if (error.code === 'EADDRINUSE' && Number.isInteger(Number(error.port))) {
|
|
3404
|
+
const port = Number(error.port)
|
|
3405
|
+
const owner = this.#getPortOwner(port, id)
|
|
3406
|
+
|
|
3407
|
+
if (owner) {
|
|
3408
|
+
error = new AddressInUseError(port, owner, id)
|
|
3409
|
+
} else if (this.#getPortOwner(port, id, undefined, true)) {
|
|
3410
|
+
error = new WorkerAddressInUseError(port, id)
|
|
3411
|
+
}
|
|
3412
|
+
// Otherwise the port is used by an external process: keep the original error, which already describes it
|
|
3413
|
+
}
|
|
3414
|
+
if (error.code === 'EACCES') throw error
|
|
3357
3415
|
|
|
3358
3416
|
this.#cleanupWorker(worker)
|
|
3359
3417
|
|
|
@@ -3377,7 +3435,15 @@ export class Runtime extends EventEmitter {
|
|
|
3377
3435
|
|
|
3378
3436
|
const restartOnError = this.#getApplicationRestartOnError(config, applicationConfig)
|
|
3379
3437
|
|
|
3380
|
-
if (
|
|
3438
|
+
if (
|
|
3439
|
+
disableRestartAttempts ||
|
|
3440
|
+
!restartOnError ||
|
|
3441
|
+
error.code === 'EACCES' ||
|
|
3442
|
+
error.code === 'EADDRINUSE' ||
|
|
3443
|
+
error.code === 'EADDRNOTAVAIL' ||
|
|
3444
|
+
error.code === 'PLT_RUNTIME_EADDR_IN_USE' ||
|
|
3445
|
+
error.code === 'PLT_RUNTIME_WORKER_EADDR_IN_USE'
|
|
3446
|
+
) {
|
|
3381
3447
|
throw error
|
|
3382
3448
|
}
|
|
3383
3449
|
|
|
@@ -3397,7 +3463,16 @@ export class Runtime extends EventEmitter {
|
|
|
3397
3463
|
)
|
|
3398
3464
|
}
|
|
3399
3465
|
|
|
3400
|
-
await this.#restartCrashedWorker(
|
|
3466
|
+
await this.#restartCrashedWorker(
|
|
3467
|
+
config,
|
|
3468
|
+
applicationConfig,
|
|
3469
|
+
workersCount,
|
|
3470
|
+
id,
|
|
3471
|
+
index,
|
|
3472
|
+
silent,
|
|
3473
|
+
bootstrapAttempt,
|
|
3474
|
+
worker[kWorkerPortOffset]
|
|
3475
|
+
)
|
|
3401
3476
|
}
|
|
3402
3477
|
}
|
|
3403
3478
|
|
|
@@ -3466,18 +3541,19 @@ export class Runtime extends EventEmitter {
|
|
|
3466
3541
|
await this.#avoidOutOfOrderThreadLogs()
|
|
3467
3542
|
|
|
3468
3543
|
worker[kWorkerStatus] = 'stopped'
|
|
3544
|
+
worker[kWorkerUrl] = undefined
|
|
3469
3545
|
this.emitAndNotify('application:worker:stopped', eventPayload)
|
|
3470
3546
|
this.#broadcastWorkers()
|
|
3471
3547
|
}
|
|
3472
3548
|
|
|
3473
3549
|
#cleanupWorker (worker) {
|
|
3474
3550
|
clearTimeout(worker[kHealthCheckTimer])
|
|
3475
|
-
this.#workerPortOffsets.delete(worker[kFullId])
|
|
3476
3551
|
|
|
3477
3552
|
const currentWorker = this.#workers.get(worker[kFullId])
|
|
3478
3553
|
|
|
3479
3554
|
if (currentWorker === worker) {
|
|
3480
3555
|
this.#workers.delete(worker[kFullId])
|
|
3556
|
+
this.#workerPortOffsets.delete(worker[kFullId])
|
|
3481
3557
|
}
|
|
3482
3558
|
|
|
3483
3559
|
worker[kITC].close()
|
|
@@ -3488,6 +3564,8 @@ export class Runtime extends EventEmitter {
|
|
|
3488
3564
|
worker.removeAllListeners('exit')
|
|
3489
3565
|
await worker.terminate()
|
|
3490
3566
|
|
|
3567
|
+
// The worker might have never been registered, make sure its port offset is released
|
|
3568
|
+
this.#workerPortOffsets.delete(worker[kFullId])
|
|
3491
3569
|
return this.#cleanupWorker(worker)
|
|
3492
3570
|
}
|
|
3493
3571
|
|
|
@@ -3501,6 +3579,42 @@ export class Runtime extends EventEmitter {
|
|
|
3501
3579
|
return index
|
|
3502
3580
|
}
|
|
3503
3581
|
|
|
3582
|
+
// Returns the lowest port offset which is not used by any worker of the application
|
|
3583
|
+
#getNextWorkerPortOffset (applicationId) {
|
|
3584
|
+
const used = new Set()
|
|
3585
|
+
|
|
3586
|
+
for (const [workerId, offset] of this.#workerPortOffsets) {
|
|
3587
|
+
if (workerId.slice(0, workerId.lastIndexOf(':')) === applicationId) {
|
|
3588
|
+
used.add(offset)
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3592
|
+
let offset = 0
|
|
3593
|
+
while (used.has(offset)) {
|
|
3594
|
+
offset++
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
return offset
|
|
3598
|
+
}
|
|
3599
|
+
|
|
3600
|
+
// Returns true if the application of the worker is configured to listen on a fixed port shared by all its workers
|
|
3601
|
+
async #usesSharedFixedPort (worker) {
|
|
3602
|
+
let server
|
|
3603
|
+
|
|
3604
|
+
try {
|
|
3605
|
+
server = (await sendViaITC(worker, 'getApplicationConfig'))?.server
|
|
3606
|
+
} catch {
|
|
3607
|
+
return false
|
|
3608
|
+
}
|
|
3609
|
+
|
|
3610
|
+
if (!server || server.portAssignment === 'perWorkerIncrement') {
|
|
3611
|
+
return false
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3614
|
+
const port = Number(server.port)
|
|
3615
|
+
return Number.isInteger(port) && port > 0
|
|
3616
|
+
}
|
|
3617
|
+
|
|
3504
3618
|
// Returns the effective restartOnError value for an application: the application-level
|
|
3505
3619
|
// value, when defined, takes precedence over the runtime-level one.
|
|
3506
3620
|
// The value is normalized to a number: false becomes 0 (never restart),
|
|
@@ -3517,7 +3631,16 @@ export class Runtime extends EventEmitter {
|
|
|
3517
3631
|
return restartOnError
|
|
3518
3632
|
}
|
|
3519
3633
|
|
|
3520
|
-
async #restartCrashedWorker (
|
|
3634
|
+
async #restartCrashedWorker (
|
|
3635
|
+
config,
|
|
3636
|
+
applicationConfig,
|
|
3637
|
+
workersCount,
|
|
3638
|
+
id,
|
|
3639
|
+
oldIndex,
|
|
3640
|
+
silent,
|
|
3641
|
+
bootstrapAttempt,
|
|
3642
|
+
portOffset
|
|
3643
|
+
) {
|
|
3521
3644
|
const restartOnError = this.#getApplicationRestartOnError(config, applicationConfig)
|
|
3522
3645
|
|
|
3523
3646
|
// Use oldIndex for tracking to prevent duplicate restarts of the same crashed worker
|
|
@@ -3536,17 +3659,26 @@ export class Runtime extends EventEmitter {
|
|
|
3536
3659
|
// If some processes were scheduled to restart
|
|
3537
3660
|
// but the runtime is stopped, ignore it
|
|
3538
3661
|
if (!this.#status.startsWith('start')) {
|
|
3662
|
+
resolve()
|
|
3539
3663
|
return
|
|
3540
3664
|
}
|
|
3541
3665
|
|
|
3542
|
-
// Get a new unique index for the restarted worker
|
|
3666
|
+
// Get a new unique index for the restarted worker, which inherits the port offset of the crashed one
|
|
3543
3667
|
const newIndex = this.#getNextWorkerIndex(id)
|
|
3544
3668
|
const newWorkerId = `${id}:${newIndex}`
|
|
3545
|
-
this.#workerPortOffsets.set(newWorkerId, portOffset)
|
|
3669
|
+
this.#workerPortOffsets.set(newWorkerId, portOffset ?? oldIndex)
|
|
3546
3670
|
|
|
3547
3671
|
try {
|
|
3548
3672
|
await this.#setupWorker(config, applicationConfig, workersCount, id, newIndex)
|
|
3549
|
-
await this.#startWorker(
|
|
3673
|
+
await this.#startWorker(
|
|
3674
|
+
config,
|
|
3675
|
+
applicationConfig,
|
|
3676
|
+
workersCount,
|
|
3677
|
+
id,
|
|
3678
|
+
newIndex,
|
|
3679
|
+
silent,
|
|
3680
|
+
bootstrapAttempt
|
|
3681
|
+
)
|
|
3550
3682
|
this.#incrementApplicationRestartCount(id)
|
|
3551
3683
|
|
|
3552
3684
|
this.logger.info(
|
|
@@ -3559,6 +3691,7 @@ export class Runtime extends EventEmitter {
|
|
|
3559
3691
|
// The runtime was stopped while the restart was happening, ignore any error.
|
|
3560
3692
|
if (!this.#status.startsWith('start')) {
|
|
3561
3693
|
resolve()
|
|
3694
|
+
return
|
|
3562
3695
|
}
|
|
3563
3696
|
|
|
3564
3697
|
reject(err)
|
|
@@ -3576,31 +3709,29 @@ export class Runtime extends EventEmitter {
|
|
|
3576
3709
|
await restartPromise
|
|
3577
3710
|
}
|
|
3578
3711
|
|
|
3579
|
-
async #replaceWorker (
|
|
3712
|
+
async #replaceWorker (
|
|
3713
|
+
config,
|
|
3714
|
+
applicationConfig,
|
|
3715
|
+
workersCount,
|
|
3716
|
+
applicationId,
|
|
3717
|
+
oldIndex,
|
|
3718
|
+
worker,
|
|
3719
|
+
silent,
|
|
3720
|
+
deferOldWorkerRetirement = false
|
|
3721
|
+
) {
|
|
3580
3722
|
const oldLabel = this.#workerExtendedLabel(applicationId, oldIndex, workersCount)
|
|
3581
3723
|
let newWorker
|
|
3582
3724
|
|
|
3583
|
-
// Get a new unique index for the replacement worker
|
|
3725
|
+
// Get a new unique index for the replacement worker, which inherits the port offset of the replaced one
|
|
3584
3726
|
const newIndex = this.#getNextWorkerIndex(applicationId)
|
|
3585
3727
|
const newWorkerId = `${applicationId}:${newIndex}`
|
|
3586
3728
|
const newLabel = this.#workerExtendedLabel(applicationId, newIndex, workersCount)
|
|
3587
|
-
|
|
3588
|
-
this.#workerPortOffsets.set(newWorkerId, portOffset)
|
|
3729
|
+
this.#workerPortOffsets.set(newWorkerId, worker[kWorkerPortOffset] ?? oldIndex)
|
|
3589
3730
|
|
|
3590
3731
|
const stopBeforeStart =
|
|
3591
|
-
|
|
3732
|
+
Boolean(worker[kWorkerUrl]) &&
|
|
3592
3733
|
(config.reuseTcpPorts === false || applicationConfig.reuseTcpPorts === false || !features.node.reusePort)
|
|
3593
3734
|
|
|
3594
|
-
// When we must stop before start (no reusePort available), pin the entrypoint
|
|
3595
|
-
// port in the config so the replacement worker binds to the same port.
|
|
3596
|
-
// We only do this for stopBeforeStart because when reusePort is available
|
|
3597
|
-
// the new worker starts alongside the old one and must use port 0 to avoid
|
|
3598
|
-
// SO_REUSEPORT routing requests to the stale old worker.
|
|
3599
|
-
let configForNewWorker = config
|
|
3600
|
-
if (stopBeforeStart && this.#entrypointPort && config.server) {
|
|
3601
|
-
configForNewWorker = { ...config, server: { ...config.server, port: this.#entrypointPort } }
|
|
3602
|
-
}
|
|
3603
|
-
|
|
3604
3735
|
try {
|
|
3605
3736
|
if (!silent) {
|
|
3606
3737
|
this.logger.debug(`Preparing to start ${newLabel} as replacement for ${oldLabel} ...`)
|
|
@@ -3610,8 +3741,14 @@ export class Runtime extends EventEmitter {
|
|
|
3610
3741
|
await this.#removeWorker(workersCount, applicationId, oldIndex, worker, silent, oldLabel)
|
|
3611
3742
|
}
|
|
3612
3743
|
|
|
3613
|
-
|
|
3614
|
-
|
|
3744
|
+
newWorker = await this.#setupWorker(
|
|
3745
|
+
config,
|
|
3746
|
+
applicationConfig,
|
|
3747
|
+
workersCount,
|
|
3748
|
+
applicationId,
|
|
3749
|
+
newIndex,
|
|
3750
|
+
false
|
|
3751
|
+
)
|
|
3615
3752
|
|
|
3616
3753
|
// Make sure the runtime hasn't been stopped in the meanwhile
|
|
3617
3754
|
if (this.#status !== 'started') {
|
|
@@ -3625,7 +3762,7 @@ export class Runtime extends EventEmitter {
|
|
|
3625
3762
|
|
|
3626
3763
|
// Add the worker to the mesh
|
|
3627
3764
|
await this.#startWorker(
|
|
3628
|
-
|
|
3765
|
+
config,
|
|
3629
3766
|
applicationConfig,
|
|
3630
3767
|
workersCount,
|
|
3631
3768
|
applicationId,
|
|
@@ -3641,17 +3778,23 @@ export class Runtime extends EventEmitter {
|
|
|
3641
3778
|
return this.#discardWorker(newWorker)
|
|
3642
3779
|
}
|
|
3643
3780
|
} catch (e) {
|
|
3644
|
-
this.#workerPortOffsets.delete(newWorkerId)
|
|
3645
|
-
|
|
3646
3781
|
if (this.#workers.get(newWorkerId) === newWorker) {
|
|
3647
3782
|
this.#workers.delete(newWorkerId)
|
|
3648
3783
|
}
|
|
3649
3784
|
|
|
3785
|
+
this.#workerPortOffsets.delete(newWorkerId)
|
|
3650
3786
|
newWorker?.terminate?.()
|
|
3651
3787
|
throw e
|
|
3652
3788
|
}
|
|
3653
3789
|
|
|
3654
3790
|
if (!stopBeforeStart) {
|
|
3791
|
+
if (deferOldWorkerRetirement) {
|
|
3792
|
+
return {
|
|
3793
|
+
discard: () => this.#discardWorker(newWorker),
|
|
3794
|
+
retire: () => this.#removeWorker(workersCount, applicationId, oldIndex, worker, silent, oldLabel)
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3655
3798
|
await this.#removeWorker(workersCount, applicationId, oldIndex, worker, silent, oldLabel)
|
|
3656
3799
|
}
|
|
3657
3800
|
}
|
|
@@ -4232,13 +4375,14 @@ export class Runtime extends EventEmitter {
|
|
|
4232
4375
|
|
|
4233
4376
|
if (currentWorkers < workers) {
|
|
4234
4377
|
report.started = []
|
|
4378
|
+
|
|
4235
4379
|
let pendingWorkerId
|
|
4236
4380
|
|
|
4237
4381
|
try {
|
|
4238
4382
|
for (let i = currentWorkers; i < workers; i++) {
|
|
4239
4383
|
const newIndex = this.#getNextWorkerIndex(applicationId)
|
|
4240
4384
|
pendingWorkerId = `${applicationId}:${newIndex}`
|
|
4241
|
-
this.#workerPortOffsets.set(pendingWorkerId,
|
|
4385
|
+
this.#workerPortOffsets.set(pendingWorkerId, this.#getNextWorkerPortOffset(applicationId))
|
|
4242
4386
|
|
|
4243
4387
|
await this.#setupWorker(config, applicationConfig, workers, applicationId, newIndex)
|
|
4244
4388
|
await this.#startWorker(config, applicationConfig, workers, applicationId, newIndex, false, 0)
|
|
@@ -4249,7 +4393,9 @@ export class Runtime extends EventEmitter {
|
|
|
4249
4393
|
}
|
|
4250
4394
|
report.success = true
|
|
4251
4395
|
} catch (err) {
|
|
4252
|
-
|
|
4396
|
+
if (pendingWorkerId) {
|
|
4397
|
+
this.#workerPortOffsets.delete(pendingWorkerId)
|
|
4398
|
+
}
|
|
4253
4399
|
|
|
4254
4400
|
if (startedWorkersCount < 1) {
|
|
4255
4401
|
this.logger.error({ err }, 'Cannot start application workers, no worker started')
|
|
@@ -4266,22 +4412,18 @@ export class Runtime extends EventEmitter {
|
|
|
4266
4412
|
report.stopped = []
|
|
4267
4413
|
try {
|
|
4268
4414
|
const workersToStop = currentWorkers - workers
|
|
4269
|
-
|
|
4415
|
+
// Stop the workers with the highest port offsets (which are the most recent workers, unless some of them
|
|
4416
|
+
// were replaced) first, so that applications using server.portAssignment=perWorkerIncrement keep listening
|
|
4417
|
+
// on a contiguous range of ports starting from the configured one.
|
|
4270
4418
|
const workerIdsToStop = this.#workers
|
|
4271
4419
|
.getKeys(applicationId)
|
|
4272
4420
|
.map(key => parseInt(key.split(':')[1], 10))
|
|
4273
|
-
|
|
4274
|
-
if (allInOnePort) {
|
|
4275
|
-
// Stop most recent workers first.
|
|
4276
|
-
workerIdsToStop.sort((a, b) => b - a)
|
|
4277
|
-
} else {
|
|
4278
|
-
// When port increment is enabled, disable the workers with the highest ports.
|
|
4279
|
-
workerIdsToStop.sort((a, b) => {
|
|
4421
|
+
.sort((a, b) => {
|
|
4280
4422
|
const offsetA = this.#workerPortOffsets.get(`${applicationId}:${a}`) ?? a
|
|
4281
4423
|
const offsetB = this.#workerPortOffsets.get(`${applicationId}:${b}`) ?? b
|
|
4282
|
-
|
|
4424
|
+
|
|
4425
|
+
return offsetB - offsetA || b - a
|
|
4283
4426
|
})
|
|
4284
|
-
}
|
|
4285
4427
|
|
|
4286
4428
|
for (const workerIndex of workerIdsToStop.splice(0, workersToStop)) {
|
|
4287
4429
|
const worker = this.#workers.get(`${applicationId}:${workerIndex}`)
|
|
@@ -4821,4 +4963,56 @@ export class Runtime extends EventEmitter {
|
|
|
4821
4963
|
this.emit('application:worker:init:failed', { application: worker[kApplicationId], worker: worker[kWorkerId] })
|
|
4822
4964
|
worker.terminate()
|
|
4823
4965
|
}
|
|
4966
|
+
|
|
4967
|
+
#getPortOwner (port, applicationId, hostname, includeSameApplication = false) {
|
|
4968
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
4969
|
+
return null
|
|
4970
|
+
}
|
|
4971
|
+
|
|
4972
|
+
for (const worker of this.#workers.values()) {
|
|
4973
|
+
if (!worker[kWorkerUrl] || (!includeSameApplication && worker[kApplicationId] === applicationId)) {
|
|
4974
|
+
continue
|
|
4975
|
+
}
|
|
4976
|
+
|
|
4977
|
+
try {
|
|
4978
|
+
const url = new URL(worker[kWorkerUrl])
|
|
4979
|
+
const workerPort = Number(url.port || (url.protocol === 'https:' ? 443 : 80))
|
|
4980
|
+
const workerHost = url.hostname
|
|
4981
|
+
if (workerPort === port && this.#listenersOverlap(hostname ?? workerHost, workerHost)) {
|
|
4982
|
+
return worker[kApplicationId]
|
|
4983
|
+
}
|
|
4984
|
+
} catch {}
|
|
4985
|
+
}
|
|
4986
|
+
|
|
4987
|
+
return null
|
|
4988
|
+
}
|
|
4989
|
+
|
|
4990
|
+
#listenersOverlap (host, otherHost) {
|
|
4991
|
+
host = host.toLowerCase()
|
|
4992
|
+
const wildcards = new Set(['0.0.0.0', '::', '[::]'])
|
|
4993
|
+
otherHost = otherHost.toLowerCase()
|
|
4994
|
+
return host === otherHost || wildcards.has(host) || wildcards.has(otherHost)
|
|
4995
|
+
}
|
|
4996
|
+
|
|
4997
|
+
#recordWorkerUrl (worker, applicationId, workerUrl) {
|
|
4998
|
+
if (!workerUrl) {
|
|
4999
|
+
worker[kWorkerUrl] = undefined
|
|
5000
|
+
return
|
|
5001
|
+
}
|
|
5002
|
+
|
|
5003
|
+
let hostname
|
|
5004
|
+
let port
|
|
5005
|
+
try {
|
|
5006
|
+
const url = new URL(workerUrl)
|
|
5007
|
+
hostname = url.hostname
|
|
5008
|
+
port = Number(url.port || (url.protocol === 'https:' ? 443 : 80))
|
|
5009
|
+
} catch {}
|
|
5010
|
+
|
|
5011
|
+
const owner = this.#getPortOwner(port, applicationId, hostname)
|
|
5012
|
+
if (owner) {
|
|
5013
|
+
throw new AddressInUseError(port, owner, applicationId)
|
|
5014
|
+
}
|
|
5015
|
+
|
|
5016
|
+
worker[kWorkerUrl] = workerUrl
|
|
5017
|
+
}
|
|
4824
5018
|
}
|