@platformatic/runtime 3.60.0 → 3.61.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config.d.ts +4 -0
- package/lib/errors.js +1 -1
- package/lib/runtime.js +78 -23
- package/lib/worker/round-robin-map.js +1 -1
- package/package.json +16 -16
- package/schema.json +9 -1
package/config.d.ts
CHANGED
|
@@ -196,6 +196,10 @@ export type PlatformaticRuntimeConfig = {
|
|
|
196
196
|
server?: {
|
|
197
197
|
hostname?: string;
|
|
198
198
|
port?: number | string;
|
|
199
|
+
/**
|
|
200
|
+
* Configures how entrypoint server worker ports are assigned. When set to shared, all workers listen on the same port. When set to perWorkerIncrement, each worker will use its own port, starting from port (worker 0).
|
|
201
|
+
*/
|
|
202
|
+
portAssignment?: "shared" | "perWorkerIncrement";
|
|
199
203
|
/**
|
|
200
204
|
* The maximum length of the queue of pending connections
|
|
201
205
|
*/
|
package/lib/errors.js
CHANGED
|
@@ -39,7 +39,7 @@ export const ApplicationNotFoundError = createError(
|
|
|
39
39
|
)
|
|
40
40
|
export const WorkerNotFoundError = createError(
|
|
41
41
|
`${ERROR_PREFIX}_WORKER_NOT_FOUND`,
|
|
42
|
-
'Worker %s of application %s not found. Available
|
|
42
|
+
'Worker %s of application %s not found. Available workers are: %s'
|
|
43
43
|
)
|
|
44
44
|
export const ApplicationNotStartedError = createError(
|
|
45
45
|
`${ERROR_PREFIX}_APPLICATION_NOT_STARTED`,
|
package/lib/runtime.js
CHANGED
|
@@ -155,6 +155,7 @@ export class Runtime extends EventEmitter {
|
|
|
155
155
|
#restartingWorkers
|
|
156
156
|
#dynamicWorkersScaler
|
|
157
157
|
#nextWorkerIndex
|
|
158
|
+
#workerPortOffsets
|
|
158
159
|
|
|
159
160
|
#sharedHttpCache
|
|
160
161
|
#scheduler
|
|
@@ -196,6 +197,7 @@ export class Runtime extends EventEmitter {
|
|
|
196
197
|
this.#restartingApplications = new Set()
|
|
197
198
|
this.#restartingWorkers = new Map()
|
|
198
199
|
this.#nextWorkerIndex = new Map()
|
|
200
|
+
this.#workerPortOffsets = new Map() // fullWorkerId => portOffset
|
|
199
201
|
this.#sharedHttpCache = null
|
|
200
202
|
this.#applicationsConfigsPatches = new Map()
|
|
201
203
|
|
|
@@ -544,12 +546,19 @@ export class Runtime extends EventEmitter {
|
|
|
544
546
|
|
|
545
547
|
async addApplications (applications, start = false) {
|
|
546
548
|
const setupInvocations = []
|
|
549
|
+
// Per-worker ports do not need SO_REUSEPORT because each worker binds a different port.
|
|
550
|
+
const usesPerWorkerPorts = this.#config.server?.portAssignment === 'perWorkerIncrement'
|
|
547
551
|
|
|
548
552
|
const toStart = []
|
|
549
553
|
for (const application of applications) {
|
|
550
554
|
const workers = application.workers
|
|
551
555
|
|
|
552
|
-
if (
|
|
556
|
+
if (
|
|
557
|
+
(workers.static > 1 || workers.minimum > 1) &&
|
|
558
|
+
application.entrypoint &&
|
|
559
|
+
!features.node.reusePort &&
|
|
560
|
+
!usesPerWorkerPorts
|
|
561
|
+
) {
|
|
553
562
|
this.logger.warn(
|
|
554
563
|
`"${application.id}" is set as the entrypoint, but reusePort is not available in your OS; setting workers to 1 instead of ${workers.static}`
|
|
555
564
|
)
|
|
@@ -1833,6 +1842,7 @@ export class Runtime extends EventEmitter {
|
|
|
1833
1842
|
const setupInvocations = []
|
|
1834
1843
|
|
|
1835
1844
|
for (let i = 0; i < workers; i++) {
|
|
1845
|
+
this.#workerPortOffsets.set(`${id}:${i}`, i)
|
|
1836
1846
|
setupInvocations.push([config, applicationConfig, workers, id, i])
|
|
1837
1847
|
}
|
|
1838
1848
|
|
|
@@ -1860,6 +1870,12 @@ export class Runtime extends EventEmitter {
|
|
|
1860
1870
|
inspectorOptions.port = inspectorOptions.port + this.#workers.size + 1
|
|
1861
1871
|
}
|
|
1862
1872
|
|
|
1873
|
+
let serverConfigOverride
|
|
1874
|
+
if (this.#config.server?.portAssignment === 'perWorkerIncrement') {
|
|
1875
|
+
const portOffset = this.#workerPortOffsets.get(workerId)
|
|
1876
|
+
serverConfigOverride = { port: Number(this.#config.server.port) + portOffset }
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1863
1879
|
if (config.telemetry) {
|
|
1864
1880
|
applicationConfig.telemetry = {
|
|
1865
1881
|
...config.telemetry,
|
|
@@ -1923,12 +1939,21 @@ export class Runtime extends EventEmitter {
|
|
|
1923
1939
|
const maxYoungGenerationSizeMb = maxYoungGeneration ? Math.floor(maxYoungGeneration / (1024 * 1024)) : undefined
|
|
1924
1940
|
const codeRangeSizeMb = codeRangeSize ? Math.floor(codeRangeSize / (1024 * 1024)) : undefined
|
|
1925
1941
|
|
|
1942
|
+
const workerConfig = {
|
|
1943
|
+
...config,
|
|
1944
|
+
preload
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
if (config.server && serverConfigOverride) {
|
|
1948
|
+
workerConfig.server = {
|
|
1949
|
+
...config.server,
|
|
1950
|
+
...serverConfigOverride
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1926
1954
|
const worker = new Worker(kWorkerFile, {
|
|
1927
1955
|
workerData: {
|
|
1928
|
-
config:
|
|
1929
|
-
...config,
|
|
1930
|
-
preload
|
|
1931
|
-
},
|
|
1956
|
+
config: workerConfig,
|
|
1932
1957
|
applicationConfig: {
|
|
1933
1958
|
...applicationConfig,
|
|
1934
1959
|
isProduction: this.#isProduction,
|
|
@@ -1977,6 +2002,7 @@ export class Runtime extends EventEmitter {
|
|
|
1977
2002
|
worker[kWorkerStatus] = 'exited'
|
|
1978
2003
|
this.emitAndNotify('application:worker:exited', eventPayload)
|
|
1979
2004
|
|
|
2005
|
+
const portOffset = this.#workerPortOffsets.get(worker[kFullId]) ?? index
|
|
1980
2006
|
this.#cleanupWorker(worker)
|
|
1981
2007
|
|
|
1982
2008
|
if (this.#status === 'stopping') {
|
|
@@ -2001,7 +2027,7 @@ export class Runtime extends EventEmitter {
|
|
|
2001
2027
|
this.logger.warn(`The ${errorLabel} will be restarted in ${restartOnError}ms ...`)
|
|
2002
2028
|
}
|
|
2003
2029
|
|
|
2004
|
-
this.#restartCrashedWorker(config, applicationConfig, workersCount, applicationId, index, false, 0).catch(
|
|
2030
|
+
this.#restartCrashedWorker(config, applicationConfig, workersCount, applicationId, index, false, 0, portOffset).catch(
|
|
2005
2031
|
err => {
|
|
2006
2032
|
this.logger.error({ err: ensureLoggableError(err) }, `${errorLabel} could not be restarted.`)
|
|
2007
2033
|
}
|
|
@@ -2486,6 +2512,7 @@ export class Runtime extends EventEmitter {
|
|
|
2486
2512
|
} catch (err) {
|
|
2487
2513
|
const error = ensureError(err)
|
|
2488
2514
|
worker[kITC].notify('application:worker:start:processed')
|
|
2515
|
+
const portOffset = this.#workerPortOffsets.get(worker[kFullId]) ?? index
|
|
2489
2516
|
|
|
2490
2517
|
// TODO: handle port allocation error here
|
|
2491
2518
|
if (error.code === 'EADDRINUSE' || error.code === 'EACCES') throw error
|
|
@@ -2532,7 +2559,7 @@ export class Runtime extends EventEmitter {
|
|
|
2532
2559
|
)
|
|
2533
2560
|
}
|
|
2534
2561
|
|
|
2535
|
-
await this.#restartCrashedWorker(config, applicationConfig, workersCount, id, index, silent, bootstrapAttempt)
|
|
2562
|
+
await this.#restartCrashedWorker(config, applicationConfig, workersCount, id, index, silent, bootstrapAttempt, portOffset)
|
|
2536
2563
|
}
|
|
2537
2564
|
}
|
|
2538
2565
|
|
|
@@ -2601,6 +2628,7 @@ export class Runtime extends EventEmitter {
|
|
|
2601
2628
|
|
|
2602
2629
|
#cleanupWorker (worker) {
|
|
2603
2630
|
clearTimeout(worker[kHealthCheckTimer])
|
|
2631
|
+
this.#workerPortOffsets.delete(worker[kFullId])
|
|
2604
2632
|
|
|
2605
2633
|
const currentWorker = this.#workers.get(worker[kFullId])
|
|
2606
2634
|
|
|
@@ -2629,7 +2657,7 @@ export class Runtime extends EventEmitter {
|
|
|
2629
2657
|
return index
|
|
2630
2658
|
}
|
|
2631
2659
|
|
|
2632
|
-
async #restartCrashedWorker (config, applicationConfig, workersCount, id, oldIndex, silent, bootstrapAttempt) {
|
|
2660
|
+
async #restartCrashedWorker (config, applicationConfig, workersCount, id, oldIndex, silent, bootstrapAttempt, portOffset) {
|
|
2633
2661
|
// Use oldIndex for tracking to prevent duplicate restarts of the same crashed worker
|
|
2634
2662
|
const restartKey = `${id}:${oldIndex}`
|
|
2635
2663
|
|
|
@@ -2650,18 +2678,22 @@ export class Runtime extends EventEmitter {
|
|
|
2650
2678
|
}
|
|
2651
2679
|
|
|
2652
2680
|
// Get a new unique index for the restarted worker
|
|
2653
|
-
const
|
|
2681
|
+
const newIndex = this.#getNextWorkerIndex(id)
|
|
2682
|
+
const newWorkerId = `${id}:${newIndex}`
|
|
2683
|
+
this.#workerPortOffsets.set(newWorkerId, portOffset)
|
|
2654
2684
|
|
|
2655
2685
|
try {
|
|
2656
|
-
await this.#setupWorker(config, applicationConfig, workersCount, id,
|
|
2657
|
-
await this.#startWorker(config, applicationConfig, workersCount, id,
|
|
2686
|
+
await this.#setupWorker(config, applicationConfig, workersCount, id, newIndex)
|
|
2687
|
+
await this.#startWorker(config, applicationConfig, workersCount, id, newIndex, silent, bootstrapAttempt)
|
|
2658
2688
|
this.#incrementApplicationRestartCount(id)
|
|
2659
2689
|
|
|
2660
2690
|
this.logger.info(
|
|
2661
|
-
`The ${this.#workerExtendedLabel(id,
|
|
2691
|
+
`The ${this.#workerExtendedLabel(id, newIndex, workersCount)} has been successfully restarted ...`
|
|
2662
2692
|
)
|
|
2663
2693
|
resolve()
|
|
2664
2694
|
} catch (err) {
|
|
2695
|
+
this.#workerPortOffsets.delete(newWorkerId)
|
|
2696
|
+
|
|
2665
2697
|
// The runtime was stopped while the restart was happening, ignore any error.
|
|
2666
2698
|
if (!this.#status.startsWith('start')) {
|
|
2667
2699
|
resolve()
|
|
@@ -2690,6 +2722,8 @@ export class Runtime extends EventEmitter {
|
|
|
2690
2722
|
const newIndex = this.#getNextWorkerIndex(applicationId)
|
|
2691
2723
|
const newWorkerId = `${applicationId}:${newIndex}`
|
|
2692
2724
|
const newLabel = this.#workerExtendedLabel(applicationId, newIndex, workersCount)
|
|
2725
|
+
const portOffset = this.#workerPortOffsets.get(`${applicationId}:${oldIndex}`)
|
|
2726
|
+
this.#workerPortOffsets.set(newWorkerId, portOffset)
|
|
2693
2727
|
|
|
2694
2728
|
const stopBeforeStart =
|
|
2695
2729
|
applicationConfig.entrypoint &&
|
|
@@ -2705,15 +2739,15 @@ export class Runtime extends EventEmitter {
|
|
|
2705
2739
|
configForNewWorker = { ...config, server: { ...config.server, port: this.#entrypointPort } }
|
|
2706
2740
|
}
|
|
2707
2741
|
|
|
2708
|
-
if (stopBeforeStart) {
|
|
2709
|
-
await this.#removeWorker(workersCount, applicationId, oldIndex, worker, silent, oldLabel)
|
|
2710
|
-
}
|
|
2711
|
-
|
|
2712
2742
|
try {
|
|
2713
2743
|
if (!silent) {
|
|
2714
2744
|
this.logger.debug(`Preparing to start ${newLabel} as replacement for ${oldLabel} ...`)
|
|
2715
2745
|
}
|
|
2716
2746
|
|
|
2747
|
+
if (stopBeforeStart) {
|
|
2748
|
+
await this.#removeWorker(workersCount, applicationId, oldIndex, worker, silent, oldLabel)
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2717
2751
|
// Create a new worker with a new index, preserving any pinned port when stopBeforeStart is required.
|
|
2718
2752
|
newWorker = await this.#setupWorker(configForNewWorker, applicationConfig, workersCount, applicationId, newIndex, false)
|
|
2719
2753
|
|
|
@@ -2732,6 +2766,7 @@ export class Runtime extends EventEmitter {
|
|
|
2732
2766
|
|
|
2733
2767
|
this.#workers.set(newWorkerId, newWorker)
|
|
2734
2768
|
} catch (e) {
|
|
2769
|
+
this.#workerPortOffsets.delete(newWorkerId)
|
|
2735
2770
|
newWorker?.terminate?.()
|
|
2736
2771
|
throw e
|
|
2737
2772
|
}
|
|
@@ -3286,16 +3321,25 @@ export class Runtime extends EventEmitter {
|
|
|
3286
3321
|
|
|
3287
3322
|
if (currentWorkers < workers) {
|
|
3288
3323
|
report.started = []
|
|
3324
|
+
let pendingWorkerId
|
|
3325
|
+
|
|
3289
3326
|
try {
|
|
3290
3327
|
for (let i = currentWorkers; i < workers; i++) {
|
|
3291
3328
|
const newIndex = this.#getNextWorkerIndex(applicationId)
|
|
3329
|
+
pendingWorkerId = `${applicationId}:${newIndex}`
|
|
3330
|
+
this.#workerPortOffsets.set(pendingWorkerId, i)
|
|
3331
|
+
|
|
3292
3332
|
await this.#setupWorker(config, applicationConfig, workers, applicationId, newIndex)
|
|
3293
3333
|
await this.#startWorker(config, applicationConfig, workers, applicationId, newIndex, false, 0)
|
|
3334
|
+
|
|
3335
|
+
pendingWorkerId = undefined
|
|
3294
3336
|
report.started.push(newIndex)
|
|
3295
3337
|
startedWorkersCount++
|
|
3296
3338
|
}
|
|
3297
3339
|
report.success = true
|
|
3298
3340
|
} catch (err) {
|
|
3341
|
+
pendingWorkerId && this.#workerPortOffsets.delete(pendingWorkerId)
|
|
3342
|
+
|
|
3299
3343
|
if (startedWorkersCount < 1) {
|
|
3300
3344
|
this.logger.error({ err }, 'Cannot start application workers, no worker started')
|
|
3301
3345
|
} else {
|
|
@@ -3310,21 +3354,32 @@ export class Runtime extends EventEmitter {
|
|
|
3310
3354
|
// keep the current workers count until all the application workers are all stopped
|
|
3311
3355
|
report.stopped = []
|
|
3312
3356
|
try {
|
|
3313
|
-
|
|
3314
|
-
const
|
|
3315
|
-
const
|
|
3357
|
+
const workersToStop = currentWorkers - workers
|
|
3358
|
+
const allInOnePort = this.#config.server?.portAssignment !== 'perWorkerIncrement'
|
|
3359
|
+
const workerIdsToStop = this.#workers
|
|
3360
|
+
.getKeys(applicationId)
|
|
3316
3361
|
.map(key => parseInt(key.split(':')[1], 10))
|
|
3317
|
-
.sort((a, b) => b - a) // descending order
|
|
3318
3362
|
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3363
|
+
if (allInOnePort) {
|
|
3364
|
+
// Stop most recent workers first.
|
|
3365
|
+
workerIdsToStop.sort((a, b) => b - a)
|
|
3366
|
+
} else {
|
|
3367
|
+
// When port increment is enabled, disable the workers with the highest ports.
|
|
3368
|
+
workerIdsToStop.sort((a, b) => {
|
|
3369
|
+
const offsetA = this.#workerPortOffsets.get(`${applicationId}:${a}`) ?? a
|
|
3370
|
+
const offsetB = this.#workerPortOffsets.get(`${applicationId}:${b}`) ?? b
|
|
3371
|
+
return offsetB - offsetA
|
|
3372
|
+
})
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
for (const workerIndex of workerIdsToStop.splice(0, workersToStop)) {
|
|
3322
3376
|
const worker = this.#workers.get(`${applicationId}:${workerIndex}`)
|
|
3323
3377
|
await sendViaITC(worker, 'removeFromMesh')
|
|
3324
3378
|
await this.#stopWorker(currentWorkers, applicationId, workerIndex, false, worker, [])
|
|
3325
3379
|
report.stopped.push(workerIndex)
|
|
3326
3380
|
stoppedWorkersCount++
|
|
3327
3381
|
}
|
|
3382
|
+
|
|
3328
3383
|
report.success = true
|
|
3329
3384
|
} catch (err) {
|
|
3330
3385
|
if (stoppedWorkersCount < 1) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/runtime",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.61.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -39,14 +39,14 @@
|
|
|
39
39
|
"typescript": "^5.5.4",
|
|
40
40
|
"undici-oidc-interceptor": "^0.9.0",
|
|
41
41
|
"why-is-node-running": "^2.2.2",
|
|
42
|
-
"@platformatic/
|
|
43
|
-
"@platformatic/
|
|
44
|
-
"@platformatic/
|
|
45
|
-
"@platformatic/node": "3.
|
|
46
|
-
"@platformatic/
|
|
47
|
-
"@platformatic/sql-graphql": "3.
|
|
48
|
-
"@platformatic/
|
|
49
|
-
"@platformatic/
|
|
42
|
+
"@platformatic/db": "3.61.1",
|
|
43
|
+
"@platformatic/gateway": "3.61.1",
|
|
44
|
+
"@platformatic/service": "3.61.1",
|
|
45
|
+
"@platformatic/node": "3.61.1",
|
|
46
|
+
"@platformatic/composer": "3.61.1",
|
|
47
|
+
"@platformatic/sql-graphql": "3.61.1",
|
|
48
|
+
"@platformatic/wattpm-pprof-capture": "3.61.1",
|
|
49
|
+
"@platformatic/sql-mapper": "3.61.1"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@fastify/accepts": "^5.0.0",
|
|
@@ -77,13 +77,13 @@
|
|
|
77
77
|
"undici": "^7.27.2",
|
|
78
78
|
"undici-thread-interceptor": "^1.3.1",
|
|
79
79
|
"ws": "^8.16.0",
|
|
80
|
-
"@platformatic/basic": "3.
|
|
81
|
-
"@platformatic/foundation": "3.
|
|
82
|
-
"@platformatic/
|
|
83
|
-
"@platformatic/
|
|
84
|
-
"@platformatic/
|
|
85
|
-
"@platformatic/telemetry": "3.
|
|
86
|
-
"@platformatic/
|
|
80
|
+
"@platformatic/basic": "3.61.1",
|
|
81
|
+
"@platformatic/foundation": "3.61.1",
|
|
82
|
+
"@platformatic/globals": "3.61.1",
|
|
83
|
+
"@platformatic/itc": "3.61.1",
|
|
84
|
+
"@platformatic/metrics": "3.61.1",
|
|
85
|
+
"@platformatic/telemetry": "3.61.1",
|
|
86
|
+
"@platformatic/generators": "3.61.1"
|
|
87
87
|
},
|
|
88
88
|
"engines": {
|
|
89
89
|
"node": ">=22.19.0"
|
package/schema.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.
|
|
2
|
+
"$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.61.1.json",
|
|
3
3
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
4
|
"title": "Platformatic Runtime Config",
|
|
5
5
|
"type": "object",
|
|
@@ -1889,6 +1889,14 @@
|
|
|
1889
1889
|
}
|
|
1890
1890
|
]
|
|
1891
1891
|
},
|
|
1892
|
+
"portAssignment": {
|
|
1893
|
+
"type": "string",
|
|
1894
|
+
"enum": [
|
|
1895
|
+
"shared",
|
|
1896
|
+
"perWorkerIncrement"
|
|
1897
|
+
],
|
|
1898
|
+
"description": "Configures how entrypoint server worker ports are assigned. When set to shared, all workers listen on the same port. When set to perWorkerIncrement, each worker will use its own port, starting from port (worker 0)."
|
|
1899
|
+
},
|
|
1892
1900
|
"backlog": {
|
|
1893
1901
|
"type": "integer",
|
|
1894
1902
|
"description": "The maximum length of the queue of pending connections"
|