@platformatic/runtime 3.61.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 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/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 ((workers.static > 1 || workers.minimum > 1) && application.entrypoint && !features.node.reusePort) {
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 index = this.#getNextWorkerIndex(id)
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, index)
2657
- await this.#startWorker(config, applicationConfig, workersCount, id, index, silent, bootstrapAttempt)
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, index, workersCount)} has been successfully restarted ...`
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
- // Get actual worker keys and sort by index descending to stop most recent first (snapshot to avoid mutation during iteration)
3314
- const workerKeys = [...this.#workers.getKeys(applicationId)]
3315
- const workerIndices = workerKeys
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
- const workersToStop = currentWorkers - workers
3320
- for (let i = 0; i < workersToStop && i < workerIndices.length; i++) {
3321
- const workerIndex = workerIndices[i]
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) {
@@ -43,7 +43,7 @@ export class RoundRobinMap extends Map {
43
43
  }
44
44
 
45
45
  getKeys (application) {
46
- return this.#instances[application]?.keys ?? []
46
+ return [...(this.#instances[application]?.keys ?? [])]
47
47
  }
48
48
 
49
49
  next (application) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "3.61.0",
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/composer": "3.61.0",
43
- "@platformatic/db": "3.61.0",
44
- "@platformatic/gateway": "3.61.0",
45
- "@platformatic/node": "3.61.0",
46
- "@platformatic/service": "3.61.0",
47
- "@platformatic/sql-graphql": "3.61.0",
48
- "@platformatic/wattpm-pprof-capture": "3.61.0",
49
- "@platformatic/sql-mapper": "3.61.0"
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.61.0",
81
- "@platformatic/generators": "3.61.0",
82
- "@platformatic/foundation": "3.61.0",
83
- "@platformatic/globals": "3.61.0",
84
- "@platformatic/itc": "3.61.0",
85
- "@platformatic/telemetry": "3.61.0",
86
- "@platformatic/metrics": "3.61.0"
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.61.0.json",
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"