@platformatic/runtime 3.68.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 -44
- package/index.d.ts +10 -14
- package/index.js +191 -6
- package/lib/config.js +122 -111
- package/lib/errors.js +5 -17
- 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 -187
- 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 -64
- package/lib/worker/itc.js +5 -6
- package/lib/worker/main.js +4 -19
- package/lib/worker/management.js +0 -4
- package/lib/worker/symbols.js +2 -0
- package/lib/worker-scaler.js +8 -10
- package/package.json +17 -16
- package/schema.json +33 -159
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
|
+
}
|
|
814
795
|
|
|
815
|
-
|
|
816
|
-
|
|
796
|
+
const results = await executeInParallel(
|
|
797
|
+
retirement => retirement.retire(),
|
|
798
|
+
succeeded.flatMap(({ retirements }) => retirements.map(retirement => [retirement])),
|
|
799
|
+
this.#concurrency,
|
|
800
|
+
false
|
|
801
|
+
)
|
|
802
|
+
|
|
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
|
}
|
|
@@ -2192,10 +2219,6 @@ export class Runtime extends EventEmitter {
|
|
|
2192
2219
|
return this.#scheduler?.getJobs() ?? []
|
|
2193
2220
|
}
|
|
2194
2221
|
|
|
2195
|
-
getScheduler () {
|
|
2196
|
-
return this.getSchedulerJobs()
|
|
2197
|
-
}
|
|
2198
|
-
|
|
2199
2222
|
pauseSchedulerJob (name) {
|
|
2200
2223
|
return this.#schedulerOrThrow().pauseJob(name)
|
|
2201
2224
|
}
|
|
@@ -2264,6 +2287,7 @@ export class Runtime extends EventEmitter {
|
|
|
2264
2287
|
worker: index,
|
|
2265
2288
|
status: worker[kWorkerStatus],
|
|
2266
2289
|
thread: worker.threadId,
|
|
2290
|
+
url: worker[kWorkerUrl] ?? null,
|
|
2267
2291
|
raw: includeRaw ? worker : undefined
|
|
2268
2292
|
}
|
|
2269
2293
|
}
|
|
@@ -2385,12 +2409,26 @@ export class Runtime extends EventEmitter {
|
|
|
2385
2409
|
this.emitAndNotify(status, args)
|
|
2386
2410
|
}
|
|
2387
2411
|
|
|
2388
|
-
#
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
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
|
+
}
|
|
2392
2420
|
|
|
2393
|
-
|
|
2421
|
+
const worker = this.#workers.get(workerId)
|
|
2422
|
+
const url = worker?.[kWorkerUrl]
|
|
2423
|
+
if (!url) {
|
|
2424
|
+
continue
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
this.logger.info(
|
|
2428
|
+
`Platformatic is now listening at ${url} for ${this.#workerExtendedLabel(applicationId, worker[kWorkerId])}`
|
|
2429
|
+
)
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2394
2432
|
}
|
|
2395
2433
|
|
|
2396
2434
|
async #setupApplication (applicationConfig) {
|
|
@@ -2426,11 +2464,37 @@ export class Runtime extends EventEmitter {
|
|
|
2426
2464
|
}
|
|
2427
2465
|
}
|
|
2428
2466
|
|
|
2429
|
-
|
|
2467
|
+
let workers = applicationConfig.workers.static
|
|
2430
2468
|
const setupInvocations = []
|
|
2431
2469
|
|
|
2432
|
-
|
|
2433
|
-
|
|
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++) {
|
|
2434
2498
|
setupInvocations.push([config, applicationConfig, workers, id, i])
|
|
2435
2499
|
}
|
|
2436
2500
|
|
|
@@ -2447,6 +2511,11 @@ export class Runtime extends EventEmitter {
|
|
|
2447
2511
|
const restartOnError = this.#getApplicationRestartOnError(config, applicationConfig)
|
|
2448
2512
|
const workerId = `${applicationId}:${index}`
|
|
2449
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
|
+
|
|
2450
2519
|
// Handle inspector
|
|
2451
2520
|
let inspectorOptions
|
|
2452
2521
|
|
|
@@ -2458,12 +2527,6 @@ export class Runtime extends EventEmitter {
|
|
|
2458
2527
|
inspectorOptions.port = inspectorOptions.port + this.#workers.size + 1
|
|
2459
2528
|
}
|
|
2460
2529
|
|
|
2461
|
-
let serverConfigOverride
|
|
2462
|
-
if (this.#config.server?.portAssignment === 'perWorkerIncrement') {
|
|
2463
|
-
const portOffset = this.#workerPortOffsets.get(workerId)
|
|
2464
|
-
serverConfigOverride = { port: Number(this.#config.server.port) + portOffset }
|
|
2465
|
-
}
|
|
2466
|
-
|
|
2467
2530
|
if (config.telemetry) {
|
|
2468
2531
|
applicationConfig.telemetry = {
|
|
2469
2532
|
...config.telemetry,
|
|
@@ -2504,7 +2567,10 @@ export class Runtime extends EventEmitter {
|
|
|
2504
2567
|
preload = preload.filter(p => p !== pprofCapturePath)
|
|
2505
2568
|
}
|
|
2506
2569
|
|
|
2507
|
-
|
|
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)
|
|
2508
2574
|
|
|
2509
2575
|
if (applicationConfig.nodeOptions?.trim().length > 0) {
|
|
2510
2576
|
const originalNodeOptions = workerEnv.NODE_OPTIONS ?? ''
|
|
@@ -2527,17 +2593,7 @@ export class Runtime extends EventEmitter {
|
|
|
2527
2593
|
const maxYoungGenerationSizeMb = maxYoungGeneration ? Math.floor(maxYoungGeneration / (1024 * 1024)) : undefined
|
|
2528
2594
|
const codeRangeSizeMb = codeRangeSize ? Math.floor(codeRangeSize / (1024 * 1024)) : undefined
|
|
2529
2595
|
|
|
2530
|
-
const workerConfig = {
|
|
2531
|
-
...config,
|
|
2532
|
-
preload
|
|
2533
|
-
}
|
|
2534
|
-
|
|
2535
|
-
if (config.server && serverConfigOverride) {
|
|
2536
|
-
workerConfig.server = {
|
|
2537
|
-
...config.server,
|
|
2538
|
-
...serverConfigOverride
|
|
2539
|
-
}
|
|
2540
|
-
}
|
|
2596
|
+
const workerConfig = { ...config, preload }
|
|
2541
2597
|
|
|
2542
2598
|
const worker = new Worker(kWorkerFile, {
|
|
2543
2599
|
workerData: {
|
|
@@ -2550,7 +2606,8 @@ export class Runtime extends EventEmitter {
|
|
|
2550
2606
|
worker: {
|
|
2551
2607
|
id: workerId,
|
|
2552
2608
|
index,
|
|
2553
|
-
count: workersCount
|
|
2609
|
+
count: workersCount,
|
|
2610
|
+
portOffset
|
|
2554
2611
|
},
|
|
2555
2612
|
resourceLimits: {
|
|
2556
2613
|
maxOldGenerationSizeMb,
|
|
@@ -2593,7 +2650,6 @@ export class Runtime extends EventEmitter {
|
|
|
2593
2650
|
worker[kWorkerStatus] = 'exited'
|
|
2594
2651
|
this.emitAndNotify('application:worker:exited', eventPayload)
|
|
2595
2652
|
|
|
2596
|
-
const portOffset = this.#workerPortOffsets.get(worker[kFullId]) ?? index
|
|
2597
2653
|
this.#cleanupWorker(worker)
|
|
2598
2654
|
|
|
2599
2655
|
if (this.#status === 'stopping') {
|
|
@@ -2618,11 +2674,18 @@ export class Runtime extends EventEmitter {
|
|
|
2618
2674
|
this.logger.warn(`The ${errorLabel} will be restarted in ${restartOnError}ms ...`)
|
|
2619
2675
|
}
|
|
2620
2676
|
|
|
2621
|
-
this.#restartCrashedWorker(
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
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
|
+
})
|
|
2626
2689
|
} else {
|
|
2627
2690
|
this.emitAndNotify('application:worker:unvailable', eventPayload)
|
|
2628
2691
|
this.logger.warn(`The ${errorLabel} is no longer available.`)
|
|
@@ -2635,6 +2698,7 @@ export class Runtime extends EventEmitter {
|
|
|
2635
2698
|
worker[kFullId] = workerId
|
|
2636
2699
|
worker[kApplicationId] = applicationId
|
|
2637
2700
|
worker[kWorkerId] = index
|
|
2701
|
+
worker[kWorkerPortOffset] = portOffset
|
|
2638
2702
|
worker[kWorkerStatus] = 'boot'
|
|
2639
2703
|
|
|
2640
2704
|
if (inspectorOptions) {
|
|
@@ -2848,9 +2912,7 @@ export class Runtime extends EventEmitter {
|
|
|
2848
2912
|
this.logger.info(`The application "${applicationId}" has been successfully reloaded.`)
|
|
2849
2913
|
this.emitAndNotify('application:worker:reloaded', eventPayload)
|
|
2850
2914
|
|
|
2851
|
-
|
|
2852
|
-
this.#showUrl()
|
|
2853
|
-
}
|
|
2915
|
+
this.#showUrls(this.#workers.getKeys(applicationId))
|
|
2854
2916
|
} catch (e) {
|
|
2855
2917
|
this.logger.error(e)
|
|
2856
2918
|
}
|
|
@@ -2885,13 +2947,11 @@ export class Runtime extends EventEmitter {
|
|
|
2885
2947
|
}
|
|
2886
2948
|
|
|
2887
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)
|
|
2888
2952
|
return this.#setupWorker(config, applicationConfig, workersCount, applicationId, index, enabled, attempt + 1)
|
|
2889
2953
|
}
|
|
2890
2954
|
|
|
2891
|
-
if (applicationConfig.entrypoint) {
|
|
2892
|
-
this.#entrypointId = applicationId
|
|
2893
|
-
}
|
|
2894
|
-
|
|
2895
2955
|
worker[kConfig] = { ...applicationConfig, health, workers: workersCount }
|
|
2896
2956
|
worker[kWorkerStatus] = 'init'
|
|
2897
2957
|
this.emitAndNotify('application:worker:init', eventPayload)
|
|
@@ -3306,23 +3366,8 @@ export class Runtime extends EventEmitter {
|
|
|
3306
3366
|
|
|
3307
3367
|
const { url: workerUrl, scheduledTasks } = workerStartResult
|
|
3308
3368
|
worker[kWorkerScheduledTasks] = scheduledTasks
|
|
3309
|
-
if (workerUrl) {
|
|
3310
|
-
this.#url = workerUrl
|
|
3311
3369
|
|
|
3312
|
-
|
|
3313
|
-
// stopBeforeStart when reuseTcpPorts is false) bind to the same port
|
|
3314
|
-
// instead of getting a new random one.
|
|
3315
|
-
if (applicationConfig.entrypoint) {
|
|
3316
|
-
try {
|
|
3317
|
-
const boundPort = Number(new URL(workerUrl).port)
|
|
3318
|
-
if (boundPort) {
|
|
3319
|
-
this.#entrypointPort = boundPort
|
|
3320
|
-
}
|
|
3321
|
-
} catch {
|
|
3322
|
-
// URL parsing failed, leave unchanged
|
|
3323
|
-
}
|
|
3324
|
-
}
|
|
3325
|
-
}
|
|
3370
|
+
this.#recordWorkerUrl(worker, id, workerUrl)
|
|
3326
3371
|
|
|
3327
3372
|
// Wait for the interceptor to be ready
|
|
3328
3373
|
const interceptorResult = await executeWithTimeout(worker[kInterceptorReadyPromise], config.startTimeout)
|
|
@@ -3352,12 +3397,21 @@ export class Runtime extends EventEmitter {
|
|
|
3352
3397
|
).unref()
|
|
3353
3398
|
}
|
|
3354
3399
|
} catch (err) {
|
|
3355
|
-
|
|
3400
|
+
let error = ensureError(err)
|
|
3356
3401
|
worker[kITC].notify('application:worker:start:processed')
|
|
3357
|
-
const portOffset = this.#workerPortOffsets.get(worker[kFullId]) ?? index
|
|
3358
3402
|
|
|
3359
|
-
|
|
3360
|
-
|
|
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
|
|
3361
3415
|
|
|
3362
3416
|
this.#cleanupWorker(worker)
|
|
3363
3417
|
|
|
@@ -3381,7 +3435,15 @@ export class Runtime extends EventEmitter {
|
|
|
3381
3435
|
|
|
3382
3436
|
const restartOnError = this.#getApplicationRestartOnError(config, applicationConfig)
|
|
3383
3437
|
|
|
3384
|
-
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
|
+
) {
|
|
3385
3447
|
throw error
|
|
3386
3448
|
}
|
|
3387
3449
|
|
|
@@ -3401,7 +3463,16 @@ export class Runtime extends EventEmitter {
|
|
|
3401
3463
|
)
|
|
3402
3464
|
}
|
|
3403
3465
|
|
|
3404
|
-
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
|
+
)
|
|
3405
3476
|
}
|
|
3406
3477
|
}
|
|
3407
3478
|
|
|
@@ -3470,18 +3541,19 @@ export class Runtime extends EventEmitter {
|
|
|
3470
3541
|
await this.#avoidOutOfOrderThreadLogs()
|
|
3471
3542
|
|
|
3472
3543
|
worker[kWorkerStatus] = 'stopped'
|
|
3544
|
+
worker[kWorkerUrl] = undefined
|
|
3473
3545
|
this.emitAndNotify('application:worker:stopped', eventPayload)
|
|
3474
3546
|
this.#broadcastWorkers()
|
|
3475
3547
|
}
|
|
3476
3548
|
|
|
3477
3549
|
#cleanupWorker (worker) {
|
|
3478
3550
|
clearTimeout(worker[kHealthCheckTimer])
|
|
3479
|
-
this.#workerPortOffsets.delete(worker[kFullId])
|
|
3480
3551
|
|
|
3481
3552
|
const currentWorker = this.#workers.get(worker[kFullId])
|
|
3482
3553
|
|
|
3483
3554
|
if (currentWorker === worker) {
|
|
3484
3555
|
this.#workers.delete(worker[kFullId])
|
|
3556
|
+
this.#workerPortOffsets.delete(worker[kFullId])
|
|
3485
3557
|
}
|
|
3486
3558
|
|
|
3487
3559
|
worker[kITC].close()
|
|
@@ -3492,6 +3564,8 @@ export class Runtime extends EventEmitter {
|
|
|
3492
3564
|
worker.removeAllListeners('exit')
|
|
3493
3565
|
await worker.terminate()
|
|
3494
3566
|
|
|
3567
|
+
// The worker might have never been registered, make sure its port offset is released
|
|
3568
|
+
this.#workerPortOffsets.delete(worker[kFullId])
|
|
3495
3569
|
return this.#cleanupWorker(worker)
|
|
3496
3570
|
}
|
|
3497
3571
|
|
|
@@ -3505,6 +3579,42 @@ export class Runtime extends EventEmitter {
|
|
|
3505
3579
|
return index
|
|
3506
3580
|
}
|
|
3507
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
|
+
|
|
3508
3618
|
// Returns the effective restartOnError value for an application: the application-level
|
|
3509
3619
|
// value, when defined, takes precedence over the runtime-level one.
|
|
3510
3620
|
// The value is normalized to a number: false becomes 0 (never restart),
|
|
@@ -3521,7 +3631,16 @@ export class Runtime extends EventEmitter {
|
|
|
3521
3631
|
return restartOnError
|
|
3522
3632
|
}
|
|
3523
3633
|
|
|
3524
|
-
async #restartCrashedWorker (
|
|
3634
|
+
async #restartCrashedWorker (
|
|
3635
|
+
config,
|
|
3636
|
+
applicationConfig,
|
|
3637
|
+
workersCount,
|
|
3638
|
+
id,
|
|
3639
|
+
oldIndex,
|
|
3640
|
+
silent,
|
|
3641
|
+
bootstrapAttempt,
|
|
3642
|
+
portOffset
|
|
3643
|
+
) {
|
|
3525
3644
|
const restartOnError = this.#getApplicationRestartOnError(config, applicationConfig)
|
|
3526
3645
|
|
|
3527
3646
|
// Use oldIndex for tracking to prevent duplicate restarts of the same crashed worker
|
|
@@ -3540,17 +3659,26 @@ export class Runtime extends EventEmitter {
|
|
|
3540
3659
|
// If some processes were scheduled to restart
|
|
3541
3660
|
// but the runtime is stopped, ignore it
|
|
3542
3661
|
if (!this.#status.startsWith('start')) {
|
|
3662
|
+
resolve()
|
|
3543
3663
|
return
|
|
3544
3664
|
}
|
|
3545
3665
|
|
|
3546
|
-
// 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
|
|
3547
3667
|
const newIndex = this.#getNextWorkerIndex(id)
|
|
3548
3668
|
const newWorkerId = `${id}:${newIndex}`
|
|
3549
|
-
this.#workerPortOffsets.set(newWorkerId, portOffset)
|
|
3669
|
+
this.#workerPortOffsets.set(newWorkerId, portOffset ?? oldIndex)
|
|
3550
3670
|
|
|
3551
3671
|
try {
|
|
3552
3672
|
await this.#setupWorker(config, applicationConfig, workersCount, id, newIndex)
|
|
3553
|
-
await this.#startWorker(
|
|
3673
|
+
await this.#startWorker(
|
|
3674
|
+
config,
|
|
3675
|
+
applicationConfig,
|
|
3676
|
+
workersCount,
|
|
3677
|
+
id,
|
|
3678
|
+
newIndex,
|
|
3679
|
+
silent,
|
|
3680
|
+
bootstrapAttempt
|
|
3681
|
+
)
|
|
3554
3682
|
this.#incrementApplicationRestartCount(id)
|
|
3555
3683
|
|
|
3556
3684
|
this.logger.info(
|
|
@@ -3563,6 +3691,7 @@ export class Runtime extends EventEmitter {
|
|
|
3563
3691
|
// The runtime was stopped while the restart was happening, ignore any error.
|
|
3564
3692
|
if (!this.#status.startsWith('start')) {
|
|
3565
3693
|
resolve()
|
|
3694
|
+
return
|
|
3566
3695
|
}
|
|
3567
3696
|
|
|
3568
3697
|
reject(err)
|
|
@@ -3580,31 +3709,29 @@ export class Runtime extends EventEmitter {
|
|
|
3580
3709
|
await restartPromise
|
|
3581
3710
|
}
|
|
3582
3711
|
|
|
3583
|
-
async #replaceWorker (
|
|
3712
|
+
async #replaceWorker (
|
|
3713
|
+
config,
|
|
3714
|
+
applicationConfig,
|
|
3715
|
+
workersCount,
|
|
3716
|
+
applicationId,
|
|
3717
|
+
oldIndex,
|
|
3718
|
+
worker,
|
|
3719
|
+
silent,
|
|
3720
|
+
deferOldWorkerRetirement = false
|
|
3721
|
+
) {
|
|
3584
3722
|
const oldLabel = this.#workerExtendedLabel(applicationId, oldIndex, workersCount)
|
|
3585
3723
|
let newWorker
|
|
3586
3724
|
|
|
3587
|
-
// 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
|
|
3588
3726
|
const newIndex = this.#getNextWorkerIndex(applicationId)
|
|
3589
3727
|
const newWorkerId = `${applicationId}:${newIndex}`
|
|
3590
3728
|
const newLabel = this.#workerExtendedLabel(applicationId, newIndex, workersCount)
|
|
3591
|
-
|
|
3592
|
-
this.#workerPortOffsets.set(newWorkerId, portOffset)
|
|
3729
|
+
this.#workerPortOffsets.set(newWorkerId, worker[kWorkerPortOffset] ?? oldIndex)
|
|
3593
3730
|
|
|
3594
3731
|
const stopBeforeStart =
|
|
3595
|
-
|
|
3732
|
+
Boolean(worker[kWorkerUrl]) &&
|
|
3596
3733
|
(config.reuseTcpPorts === false || applicationConfig.reuseTcpPorts === false || !features.node.reusePort)
|
|
3597
3734
|
|
|
3598
|
-
// When we must stop before start (no reusePort available), pin the entrypoint
|
|
3599
|
-
// port in the config so the replacement worker binds to the same port.
|
|
3600
|
-
// We only do this for stopBeforeStart because when reusePort is available
|
|
3601
|
-
// the new worker starts alongside the old one and must use port 0 to avoid
|
|
3602
|
-
// SO_REUSEPORT routing requests to the stale old worker.
|
|
3603
|
-
let configForNewWorker = config
|
|
3604
|
-
if (stopBeforeStart && this.#entrypointPort && config.server) {
|
|
3605
|
-
configForNewWorker = { ...config, server: { ...config.server, port: this.#entrypointPort } }
|
|
3606
|
-
}
|
|
3607
|
-
|
|
3608
3735
|
try {
|
|
3609
3736
|
if (!silent) {
|
|
3610
3737
|
this.logger.debug(`Preparing to start ${newLabel} as replacement for ${oldLabel} ...`)
|
|
@@ -3614,8 +3741,14 @@ export class Runtime extends EventEmitter {
|
|
|
3614
3741
|
await this.#removeWorker(workersCount, applicationId, oldIndex, worker, silent, oldLabel)
|
|
3615
3742
|
}
|
|
3616
3743
|
|
|
3617
|
-
|
|
3618
|
-
|
|
3744
|
+
newWorker = await this.#setupWorker(
|
|
3745
|
+
config,
|
|
3746
|
+
applicationConfig,
|
|
3747
|
+
workersCount,
|
|
3748
|
+
applicationId,
|
|
3749
|
+
newIndex,
|
|
3750
|
+
false
|
|
3751
|
+
)
|
|
3619
3752
|
|
|
3620
3753
|
// Make sure the runtime hasn't been stopped in the meanwhile
|
|
3621
3754
|
if (this.#status !== 'started') {
|
|
@@ -3629,7 +3762,7 @@ export class Runtime extends EventEmitter {
|
|
|
3629
3762
|
|
|
3630
3763
|
// Add the worker to the mesh
|
|
3631
3764
|
await this.#startWorker(
|
|
3632
|
-
|
|
3765
|
+
config,
|
|
3633
3766
|
applicationConfig,
|
|
3634
3767
|
workersCount,
|
|
3635
3768
|
applicationId,
|
|
@@ -3645,17 +3778,23 @@ export class Runtime extends EventEmitter {
|
|
|
3645
3778
|
return this.#discardWorker(newWorker)
|
|
3646
3779
|
}
|
|
3647
3780
|
} catch (e) {
|
|
3648
|
-
this.#workerPortOffsets.delete(newWorkerId)
|
|
3649
|
-
|
|
3650
3781
|
if (this.#workers.get(newWorkerId) === newWorker) {
|
|
3651
3782
|
this.#workers.delete(newWorkerId)
|
|
3652
3783
|
}
|
|
3653
3784
|
|
|
3785
|
+
this.#workerPortOffsets.delete(newWorkerId)
|
|
3654
3786
|
newWorker?.terminate?.()
|
|
3655
3787
|
throw e
|
|
3656
3788
|
}
|
|
3657
3789
|
|
|
3658
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
|
+
|
|
3659
3798
|
await this.#removeWorker(workersCount, applicationId, oldIndex, worker, silent, oldLabel)
|
|
3660
3799
|
}
|
|
3661
3800
|
}
|
|
@@ -4236,13 +4375,14 @@ export class Runtime extends EventEmitter {
|
|
|
4236
4375
|
|
|
4237
4376
|
if (currentWorkers < workers) {
|
|
4238
4377
|
report.started = []
|
|
4378
|
+
|
|
4239
4379
|
let pendingWorkerId
|
|
4240
4380
|
|
|
4241
4381
|
try {
|
|
4242
4382
|
for (let i = currentWorkers; i < workers; i++) {
|
|
4243
4383
|
const newIndex = this.#getNextWorkerIndex(applicationId)
|
|
4244
4384
|
pendingWorkerId = `${applicationId}:${newIndex}`
|
|
4245
|
-
this.#workerPortOffsets.set(pendingWorkerId,
|
|
4385
|
+
this.#workerPortOffsets.set(pendingWorkerId, this.#getNextWorkerPortOffset(applicationId))
|
|
4246
4386
|
|
|
4247
4387
|
await this.#setupWorker(config, applicationConfig, workers, applicationId, newIndex)
|
|
4248
4388
|
await this.#startWorker(config, applicationConfig, workers, applicationId, newIndex, false, 0)
|
|
@@ -4253,7 +4393,9 @@ export class Runtime extends EventEmitter {
|
|
|
4253
4393
|
}
|
|
4254
4394
|
report.success = true
|
|
4255
4395
|
} catch (err) {
|
|
4256
|
-
|
|
4396
|
+
if (pendingWorkerId) {
|
|
4397
|
+
this.#workerPortOffsets.delete(pendingWorkerId)
|
|
4398
|
+
}
|
|
4257
4399
|
|
|
4258
4400
|
if (startedWorkersCount < 1) {
|
|
4259
4401
|
this.logger.error({ err }, 'Cannot start application workers, no worker started')
|
|
@@ -4270,22 +4412,18 @@ export class Runtime extends EventEmitter {
|
|
|
4270
4412
|
report.stopped = []
|
|
4271
4413
|
try {
|
|
4272
4414
|
const workersToStop = currentWorkers - workers
|
|
4273
|
-
|
|
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.
|
|
4274
4418
|
const workerIdsToStop = this.#workers
|
|
4275
4419
|
.getKeys(applicationId)
|
|
4276
4420
|
.map(key => parseInt(key.split(':')[1], 10))
|
|
4277
|
-
|
|
4278
|
-
if (allInOnePort) {
|
|
4279
|
-
// Stop most recent workers first.
|
|
4280
|
-
workerIdsToStop.sort((a, b) => b - a)
|
|
4281
|
-
} else {
|
|
4282
|
-
// When port increment is enabled, disable the workers with the highest ports.
|
|
4283
|
-
workerIdsToStop.sort((a, b) => {
|
|
4421
|
+
.sort((a, b) => {
|
|
4284
4422
|
const offsetA = this.#workerPortOffsets.get(`${applicationId}:${a}`) ?? a
|
|
4285
4423
|
const offsetB = this.#workerPortOffsets.get(`${applicationId}:${b}`) ?? b
|
|
4286
|
-
|
|
4424
|
+
|
|
4425
|
+
return offsetB - offsetA || b - a
|
|
4287
4426
|
})
|
|
4288
|
-
}
|
|
4289
4427
|
|
|
4290
4428
|
for (const workerIndex of workerIdsToStop.splice(0, workersToStop)) {
|
|
4291
4429
|
const worker = this.#workers.get(`${applicationId}:${workerIndex}`)
|
|
@@ -4825,4 +4963,56 @@ export class Runtime extends EventEmitter {
|
|
|
4825
4963
|
this.emit('application:worker:init:failed', { application: worker[kApplicationId], worker: worker[kWorkerId] })
|
|
4826
4964
|
worker.terminate()
|
|
4827
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
|
+
}
|
|
4828
5018
|
}
|