@platformatic/runtime 3.14.0 → 3.16.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 CHANGED
@@ -20,6 +20,7 @@ export type PlatformaticRuntimeConfig = {
20
20
  id: string;
21
21
  config?: string;
22
22
  useHttp?: boolean;
23
+ reuseTcpPorts?: boolean;
23
24
  workers?:
24
25
  | number
25
26
  | string
@@ -182,6 +183,7 @@ export type PlatformaticRuntimeConfig = {
182
183
  rejectUnauthorized?: boolean;
183
184
  };
184
185
  };
186
+ reuseTcpPorts?: boolean;
185
187
  startTimeout?: number;
186
188
  restartOnError?: boolean | number;
187
189
  exitOnUnhandledErrors?: boolean;
package/index.js CHANGED
@@ -127,6 +127,7 @@ export async function loadApplicationsCommands () {
127
127
  }
128
128
 
129
129
  export async function create (configOrRoot, sourceOrConfig, context) {
130
+ const setupSignals = context?.setupSignals ?? true
130
131
  const config = await loadConfiguration(configOrRoot, sourceOrConfig, context)
131
132
 
132
133
  if (inspector.url() && !config[kMetadata].env.VSCODE_INSPECTOR_OPTIONS) {
@@ -134,12 +135,20 @@ export async function create (configOrRoot, sourceOrConfig, context) {
134
135
  }
135
136
 
136
137
  let runtime = new Runtime(config, context)
137
- handleSignal(runtime, config)
138
+ if (setupSignals) {
139
+ handleSignal(runtime, config)
140
+ }
138
141
 
139
142
  // Handle port handling
140
143
  if (context?.start) {
141
144
  let port = config.server?.port
142
145
 
146
+ await runtime.init()
147
+
148
+ if (context.reloaded) {
149
+ runtime.logger.info('The application has been successfully reloaded.')
150
+ }
151
+
143
152
  while (true) {
144
153
  try {
145
154
  await runtime.start()
@@ -160,7 +169,9 @@ export async function create (configOrRoot, sourceOrConfig, context) {
160
169
 
161
170
  config.server.port = ++port
162
171
  runtime = new Runtime(config, context)
163
- handleSignal(runtime, config)
172
+ if (setupSignals) {
173
+ handleSignal(runtime, config)
174
+ }
164
175
  }
165
176
  }
166
177
  }
package/lib/logger.js CHANGED
@@ -5,19 +5,150 @@ import pretty from 'pino-pretty'
5
5
 
6
6
  export { abstractLogger } from '@platformatic/foundation'
7
7
 
8
- const customPrettifiers = {
9
- name (name, _, obj) {
10
- if (typeof obj.worker !== 'undefined') {
11
- name += ':' + obj.worker
12
- obj.worker = undefined // Do not show the worker in a separate line
13
- }
8
+ // A valid color in the ANSI 256 color palette - Adiacent colors are purposely different
9
+ const colors = [
10
+ 196, // bright red
11
+ 46, // bright green
12
+ 33, // light blue
13
+ 226, // bright yellow
14
+ 201, // bright magenta
15
+ 51, // cyan
16
+ 208, // orange
17
+ 118, // lime
18
+ 39, // deep sky blue
19
+ 220, // gold
20
+ 129, // violet
21
+ 82, // spring green
22
+ 33, // blue
23
+ 214, // amber
24
+ 99, // orchid
25
+ 190, // light yellow-green
26
+ 45, // turquoise
27
+ 197, // rose
28
+ 50, // aqua
29
+ 202, // orange-red
30
+ 141, // lavender
31
+ 154, // pale green
32
+ 93, // pink
33
+ 33, // light blue again (for spacing)
34
+ 220, // gold
35
+ 201, // magenta
36
+ 46, // green
37
+ 27, // navy blue
38
+ 214, // amber
39
+ 99, // orchid
40
+ 190, // light yellow-green
41
+ 39, // cyan-blue
42
+ 200, // violet
43
+ 82, // neon green
44
+ 208, // orange
45
+ 135, // purple
46
+ 118, // lime
47
+ 33, // bright blue
48
+ 220, // gold
49
+ 201, // bright magenta
50
+ 46, // bright green
51
+ 21, // bright blue
52
+ 202, // orange-red
53
+ 141, // purple
54
+ 118, // spring green
55
+ 208, // orange
56
+ 93, // pink
57
+ 190, // yellow-green
58
+ 39, // cyan
59
+ 196, // bright red
60
+ 226 // bright yellow
61
+ ]
62
+
63
+ function createLoggerContext () {
64
+ const context = {
65
+ colors: {},
66
+ maxLength: 0,
67
+ updatePrefixes (ids) {
68
+ context.colors = {}
69
+ context.maxLength = 0
70
+
71
+ for (const id of ids) {
72
+ context.maxLength = Math.max(context.maxLength, id.length)
73
+ let hash = 0
74
+
75
+ if (!pretty.isColorSupported && process.env.FORCE_COLOR !== 'true') {
76
+ context.colors[id] = ''
77
+ continue
78
+ }
79
+
80
+ // Calculate the hash of the id to pick a color
81
+ for (const char of id) {
82
+ for (let i = 0; i < char.length; i++) {
83
+ hash = ((hash << 5) - hash) ^ char.charCodeAt(i)
84
+ hash = Math.abs(hash) % Number.MAX_SAFE_INTEGER
85
+ }
86
+ }
14
87
 
15
- return name
88
+ context.colors[id] = `\u001B[38;5;${colors[hash % colors.length]}m`
89
+ }
90
+ }
16
91
  }
92
+
93
+ return context
94
+ }
95
+
96
+ function createPrettifier (context) {
97
+ return pretty({
98
+ messageFormat (log, key) {
99
+ const { name, pid, hostname, caller, worker } = log
100
+
101
+ context.current = {
102
+ name,
103
+ pid,
104
+ hostname,
105
+ caller,
106
+ worker
107
+ }
108
+
109
+ let prefix = ''
110
+ let color = ''
111
+
112
+ if (name) {
113
+ prefix = name.match(/:\d+$/) ? name : `${name}:${worker}`
114
+ color = context.colors[prefix] ?? ''
115
+ }
116
+
117
+ context.current.prefix = `(${pid}) ` + prefix.padStart(context.maxLength, ' ')
118
+ context.current.color = color
119
+
120
+ // We need to nullify all these so that prettifierMetadata in pino-pretty returns an empty string
121
+ log.name = undefined
122
+ log.pid = undefined
123
+ log.hostname = undefined
124
+ log.caller = undefined
125
+ log.worker = undefined
126
+
127
+ return log[key]
128
+ },
129
+ customPrettifiers: {
130
+ time (time) {
131
+ return `${context.current.color}[${time}]`
132
+ },
133
+ level (_u1, _u2, _u3, { label, labelColorized }) {
134
+ // No applications registered yet, no need to pad
135
+ if (context.maxLength === 0) {
136
+ return context.current.prefix + labelColorized
137
+ }
138
+
139
+ const current = context.current
140
+ const level = current.caller ? current.caller : labelColorized.replace(label, label.padStart(6, ' '))
141
+
142
+ return `${current.prefix} | \u001B[0m ${level}`
143
+ }
144
+ }
145
+ })
17
146
  }
18
147
 
19
148
  // Create the runtime logger
20
149
  export async function createLogger (config) {
150
+ const context = createLoggerContext()
151
+
21
152
  const loggerConfig = { ...config.logger, transport: undefined }
22
153
  if (config.logger.base === null) {
23
154
  loggerConfig.base = undefined
@@ -28,7 +159,7 @@ export async function createLogger (config) {
28
159
  if (config.logger.transport) {
29
160
  cliStream = pino.transport(config.logger.transport)
30
161
  } else if ((process.env.FORCE_TTY || isatty(1)) && usePrettyPrint()) {
31
- cliStream = pretty({ customPrettifiers })
162
+ cliStream = createPrettifier(context)
32
163
  } else {
33
164
  cliStream = pino.destination(1)
34
165
  }
@@ -42,7 +173,7 @@ export async function createLogger (config) {
42
173
  }
43
174
 
44
175
  if (!config.managementApi) {
45
- return [pino(loggerConfig, cliStream), cliStream]
176
+ return [pino(loggerConfig, cliStream), cliStream, context]
46
177
  }
47
178
 
48
179
  const multiStream = pino.multistream([{ stream: cliStream, level: loggerConfig.level }])
@@ -55,5 +186,5 @@ export async function createLogger (config) {
55
186
  logsLimitCount = 1
56
187
  }
57
188
 
58
- return [pino(loggerConfig, multiStream), multiStream]
189
+ return [pino(loggerConfig, multiStream), multiStream, context]
59
190
  }
@@ -1,11 +1,18 @@
1
1
  import fastifyAccepts from '@fastify/accepts'
2
2
  import fastifyWebsocket from '@fastify/websocket'
3
- import { createDirectory, safeRemove } from '@platformatic/foundation'
3
+ import {
4
+ applications as applicationSchema,
5
+ createDirectory,
6
+ kMetadata,
7
+ safeRemove,
8
+ validate
9
+ } from '@platformatic/foundation'
4
10
  import fastify from 'fastify'
5
11
  import { platform, tmpdir } from 'node:os'
6
12
  import { join } from 'node:path'
7
13
  import { setTimeout as sleep } from 'node:timers/promises'
8
14
  import { createWebSocketStream } from 'ws'
15
+ import { prepareApplication } from './config.js'
9
16
 
10
17
  const PLATFORMATIC_TMP_DIR = join(tmpdir(), 'platformatic', 'runtimes')
11
18
 
@@ -14,6 +21,26 @@ export async function managementApiPlugin (app, opts) {
14
21
 
15
22
  const runtime = opts.runtime
16
23
 
24
+ async function deleteApplications (ids, reply) {
25
+ const validIds = runtime.getApplicationsIds()
26
+
27
+ for (const id of ids) {
28
+ if (!validIds.includes(id)) {
29
+ reply.code(404)
30
+
31
+ return {
32
+ error: 'Not Found',
33
+ message: `Application with id "${id}" not found.`,
34
+ statusCode: 404
35
+ }
36
+ }
37
+ }
38
+
39
+ const removed = await runtime.removeApplications(ids)
40
+ reply.code(202)
41
+ return removed
42
+ }
43
+
17
44
  app.get('/status', async () => {
18
45
  const status = runtime.getRuntimeStatus()
19
46
  return { status }
@@ -23,8 +50,16 @@ export async function managementApiPlugin (app, opts) {
23
50
  return runtime.getRuntimeMetadata()
24
51
  })
25
52
 
26
- app.get('/config', async () => {
27
- return runtime.getRuntimeConfig()
53
+ app.get('/config', async request => {
54
+ const metadata = request.query.metadata === 'true'
55
+ const rawConfig = await runtime.getRuntimeConfig(metadata)
56
+
57
+ if (metadata) {
58
+ const { [kMetadata]: __metadata, ...config } = rawConfig
59
+ return { ...config, __metadata }
60
+ }
61
+
62
+ return rawConfig
28
63
  })
29
64
 
30
65
  app.get('/env', async () => {
@@ -46,12 +81,60 @@ export async function managementApiPlugin (app, opts) {
46
81
  return runtime.getApplications()
47
82
  })
48
83
 
84
+ app.post('/applications', async (request, reply) => {
85
+ let applications = request.body
86
+
87
+ if (!Array.isArray(applications)) {
88
+ applications = [applications]
89
+ }
90
+
91
+ const config = runtime.getRuntimeConfig(true)
92
+
93
+ try {
94
+ validate(applicationSchema, applications, {}, true, config[kMetadata].root)
95
+ } catch (err) {
96
+ reply.code(400)
97
+
98
+ return {
99
+ statusCode: 400,
100
+ error: 'Bad Request',
101
+ message: 'Invalid applications configuration.',
102
+ validationErrors: err.validationErrors
103
+ }
104
+ }
105
+
106
+ for (let i = 0; i < applications.length; i++) {
107
+ applications[i] = await prepareApplication(config, applications[i])
108
+ }
109
+
110
+ const created = await runtime.addApplications(applications, request.query.start !== 'false')
111
+ reply.code(201)
112
+ return created
113
+ })
114
+
115
+ app.delete('/applications', async (request, reply) => {
116
+ if (!Array.isArray(request.body)) {
117
+ reply.code(404)
118
+ return {
119
+ statusCode: 404,
120
+ error: 'Bad Request',
121
+ message: 'Invalid applications IDs.'
122
+ }
123
+ }
124
+
125
+ return deleteApplications(request.body, reply)
126
+ })
127
+
49
128
  app.get('/applications/:id', async request => {
50
129
  const { id } = request.params
51
130
  app.log.debug('get application details', { id })
52
131
  return runtime.getApplicationDetails(id)
53
132
  })
54
133
 
134
+ app.delete('/applications/:id', async (request, reply) => {
135
+ return deleteApplications([request.params.id], reply)
136
+ })
137
+
55
138
  app.get('/applications/:id/config', async request => {
56
139
  const { id } = request.params
57
140
  app.log.debug('get application config', { id })
@@ -195,7 +278,7 @@ export async function managementApiPlugin (app, opts) {
195
278
  })
196
279
  })
197
280
 
198
- app.get('/logs/live', { websocket: true }, async (socket, req) => {
281
+ app.get('/logs/live', { websocket: true }, async socket => {
199
282
  runtime.addLoggerDestination(createWebSocketStream(socket))
200
283
  })
201
284
  }
package/lib/runtime.js CHANGED
@@ -47,9 +47,9 @@ import { startPrometheusServer } from './prom-server.js'
47
47
  import { startScheduler } from './scheduler.js'
48
48
  import { createSharedStore } from './shared-http-cache.js'
49
49
  import { version } from './version.js'
50
+ import { HealthSignalsQueue } from './worker/health-signals.js'
50
51
  import { sendViaITC, waitEventFromITC } from './worker/itc.js'
51
52
  import { RoundRobinMap } from './worker/round-robin-map.js'
52
- import { HealthSignalsQueue } from './worker/health-signals.js'
53
53
  import {
54
54
  kApplicationId,
55
55
  kConfig,
@@ -60,11 +60,11 @@ import {
60
60
  kITC,
61
61
  kLastHealthCheckELU,
62
62
  kStderrMarker,
63
+ kWorkerHealthSignals,
63
64
  kWorkerId,
64
65
  kWorkersBroadcast,
65
66
  kWorkerStartTime,
66
- kWorkerStatus,
67
- kWorkerHealthSignals
67
+ kWorkerStatus
68
68
  } from './worker/symbols.js'
69
69
 
70
70
  const kWorkerFile = join(import.meta.dirname, 'worker/main.js')
@@ -84,6 +84,7 @@ export class Runtime extends EventEmitter {
84
84
  error
85
85
 
86
86
  #loggerDestination
87
+ #loggerContext
87
88
  #stdio
88
89
 
89
90
  #status // starting, started, stopping, stopped, closed
@@ -201,9 +202,10 @@ export class Runtime extends EventEmitter {
201
202
  }
202
203
 
203
204
  // Create the logger
204
- const [logger, destination] = await createLogger(config)
205
+ const [logger, destination, context] = await createLogger(config)
205
206
  this.logger = logger
206
207
  this.#loggerDestination = destination
208
+ this.#loggerContext = context
207
209
 
208
210
  this.#createWorkersBroadcastChannel()
209
211
 
@@ -358,6 +360,7 @@ export class Runtime extends EventEmitter {
358
360
 
359
361
  this.logger = abstractLogger
360
362
  this.#loggerDestination = null
363
+ this.#loggerContext = null
361
364
  }
362
365
 
363
366
  this.#updateStatus('closed')
@@ -466,12 +469,21 @@ export class Runtime extends EventEmitter {
466
469
  await executeInParallel(this.#setupApplication.bind(this), setupInvocations, this.#concurrency)
467
470
 
468
471
  for (const application of applications) {
472
+ this.logger.info(`Added application "${application.id}"${application.entrypoint ? ' (entrypoint)' : ''}.`)
469
473
  this.emitAndNotify('application:added', application)
470
474
  }
471
475
 
472
476
  if (start) {
473
477
  await this.startApplications(toStart)
474
478
  }
479
+
480
+ const created = []
481
+ for (const { id } of applications) {
482
+ created.push(await this.getApplicationDetails(id))
483
+ }
484
+
485
+ this.#updateLoggingPrefixes()
486
+ return created
475
487
  }
476
488
 
477
489
  async removeApplications (applications, silent = false) {
@@ -479,6 +491,13 @@ export class Runtime extends EventEmitter {
479
491
  throw new CannotRemoveEntrypointError()
480
492
  }
481
493
 
494
+ const removed = []
495
+ for (const application of applications) {
496
+ const details = await this.getApplicationDetails(application)
497
+ details.status = 'removed'
498
+ removed.push(details)
499
+ }
500
+
482
501
  await this.stopApplications(applications, silent, true)
483
502
 
484
503
  for (const application of applications) {
@@ -487,8 +506,12 @@ export class Runtime extends EventEmitter {
487
506
  }
488
507
 
489
508
  for (const application of applications) {
509
+ this.logger.info(`Removed application "${application}".`)
490
510
  this.emitAndNotify('application:removed', application)
491
511
  }
512
+
513
+ this.#updateLoggingPrefixes()
514
+ return removed
492
515
  }
493
516
 
494
517
  async startApplications (applicationsToStart, silent = false) {
@@ -1182,7 +1205,7 @@ export class Runtime extends EventEmitter {
1182
1205
  throw e
1183
1206
  }
1184
1207
 
1185
- const { entrypoint, localUrl } = application[kConfig]
1208
+ const { entrypoint, localUrl, config, path } = application[kConfig]
1186
1209
 
1187
1210
  const status = await sendViaITC(application, 'getStatus')
1188
1211
  const { type, version, dependencies } = await sendViaITC(application, 'getApplicationInfo')
@@ -1190,6 +1213,8 @@ export class Runtime extends EventEmitter {
1190
1213
  const applicationDetails = {
1191
1214
  id,
1192
1215
  type,
1216
+ config,
1217
+ path,
1193
1218
  status,
1194
1219
  dependencies,
1195
1220
  version,
@@ -1587,7 +1612,7 @@ export class Runtime extends EventEmitter {
1587
1612
  await this.startApplication(applicationId)
1588
1613
  }
1589
1614
 
1590
- this.logger.info(`The application "${applicationId}" has been successfully reloaded ...`)
1615
+ this.logger.info(`The application "${applicationId}" has been successfully reloaded.`)
1591
1616
  this.emitAndNotify('application:worker:reloaded', eventPayload)
1592
1617
 
1593
1618
  if (applicationConfig.entrypoint) {
@@ -1666,13 +1691,7 @@ export class Runtime extends EventEmitter {
1666
1691
 
1667
1692
  const healthConfig = worker[kConfig].health
1668
1693
 
1669
- let {
1670
- maxELU,
1671
- maxHeapUsed,
1672
- maxHeapTotal,
1673
- maxUnhealthyChecks,
1674
- interval
1675
- } = worker[kConfig].health
1694
+ let { maxELU, maxHeapUsed, maxHeapTotal, maxUnhealthyChecks, interval } = worker[kConfig].health
1676
1695
 
1677
1696
  if (typeof maxHeapTotal === 'string') {
1678
1697
  maxHeapTotal = parseMemorySize(maxHeapTotal)
@@ -1682,7 +1701,7 @@ export class Runtime extends EventEmitter {
1682
1701
  interval = 1000
1683
1702
  this.logger.warn(
1684
1703
  `The health check interval for the "${errorLabel}" is set to ${healthConfig.interval}ms. ` +
1685
- 'The minimum health check interval is 1s. It will be set to 1000ms.'
1704
+ 'The minimum health check interval is 1s. It will be set to 1000ms.'
1686
1705
  )
1687
1706
  }
1688
1707
 
@@ -1741,14 +1760,14 @@ export class Runtime extends EventEmitter {
1741
1760
 
1742
1761
  this.logger.error(
1743
1762
  { elu: health.elu, maxELU, memoryUsage: health.heapUsed, maxMemoryUsage: maxHeapUsed },
1744
- `The ${errorLabel} is unhealthy. Replacing it ...`
1763
+ `The ${errorLabel} is unhealthy. Replacing it ...`
1745
1764
  )
1746
1765
 
1747
1766
  await this.#replaceWorker(config, applicationConfig, workersCount, id, index, worker)
1748
1767
  } catch (e) {
1749
1768
  this.logger.error(
1750
1769
  { elu: health.elu, maxELU, memoryUsage: health.heapUsed, maxMemoryUsage: maxHeapUsed },
1751
- `Cannot replace the ${errorLabel}. Forcefully terminating it ...`
1770
+ `Cannot replace the ${errorLabel}. Forcefully terminating it ...`
1752
1771
  )
1753
1772
 
1754
1773
  worker.terminate()
@@ -2320,6 +2339,8 @@ export class Runtime extends EventEmitter {
2320
2339
  throw result.reason
2321
2340
  }
2322
2341
  }
2342
+
2343
+ this.#updateLoggingPrefixes()
2323
2344
  }
2324
2345
 
2325
2346
  async #updateApplicationConfigHealth (applicationId, health) {
@@ -2694,4 +2715,17 @@ export class Runtime extends EventEmitter {
2694
2715
  worker[kWorkerHealthSignals] ??= new HealthSignalsQueue()
2695
2716
  worker[kWorkerHealthSignals].add(signals)
2696
2717
  }
2718
+
2719
+ #updateLoggingPrefixes () {
2720
+ if (!this.#loggerContext) {
2721
+ return
2722
+ }
2723
+
2724
+ const ids = []
2725
+ for (const worker of this.#workers.values()) {
2726
+ ids.push(`${worker[kFullId]}`)
2727
+ }
2728
+
2729
+ this.#loggerContext.updatePrefixes(ids)
2730
+ }
2697
2731
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "3.14.0",
3
+ "version": "3.16.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -35,14 +35,14 @@
35
35
  "typescript": "^5.5.4",
36
36
  "undici-oidc-interceptor": "^0.5.0",
37
37
  "why-is-node-running": "^2.2.2",
38
- "@platformatic/db": "3.14.0",
39
- "@platformatic/gateway": "3.14.0",
40
- "@platformatic/composer": "3.14.0",
41
- "@platformatic/node": "3.14.0",
42
- "@platformatic/service": "3.14.0",
43
- "@platformatic/sql-graphql": "3.14.0",
44
- "@platformatic/sql-mapper": "3.14.0",
45
- "@platformatic/wattpm-pprof-capture": "3.14.0"
38
+ "@platformatic/composer": "3.16.0",
39
+ "@platformatic/db": "3.16.0",
40
+ "@platformatic/gateway": "3.16.0",
41
+ "@platformatic/node": "3.16.0",
42
+ "@platformatic/sql-mapper": "3.16.0",
43
+ "@platformatic/sql-graphql": "3.16.0",
44
+ "@platformatic/wattpm-pprof-capture": "3.16.0",
45
+ "@platformatic/service": "3.16.0"
46
46
  },
47
47
  "dependencies": {
48
48
  "@fastify/accepts": "^5.0.0",
@@ -73,12 +73,12 @@
73
73
  "undici": "^7.0.0",
74
74
  "undici-thread-interceptor": "^0.15.0",
75
75
  "ws": "^8.16.0",
76
- "@platformatic/basic": "3.14.0",
77
- "@platformatic/metrics": "3.14.0",
78
- "@platformatic/foundation": "3.14.0",
79
- "@platformatic/generators": "3.14.0",
80
- "@platformatic/telemetry": "3.14.0",
81
- "@platformatic/itc": "3.14.0"
76
+ "@platformatic/basic": "3.16.0",
77
+ "@platformatic/generators": "3.16.0",
78
+ "@platformatic/itc": "3.16.0",
79
+ "@platformatic/foundation": "3.16.0",
80
+ "@platformatic/metrics": "3.16.0",
81
+ "@platformatic/telemetry": "3.16.0"
82
82
  },
83
83
  "engines": {
84
84
  "node": ">=22.19.0"
package/schema.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.14.0.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.16.0.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "title": "Platformatic Runtime Config",
5
5
  "type": "object",
@@ -64,6 +64,10 @@
64
64
  "useHttp": {
65
65
  "type": "boolean"
66
66
  },
67
+ "reuseTcpPorts": {
68
+ "type": "boolean",
69
+ "default": true
70
+ },
67
71
  "workers": {
68
72
  "anyOf": [
69
73
  {
@@ -342,6 +346,10 @@
342
346
  "useHttp": {
343
347
  "type": "boolean"
344
348
  },
349
+ "reuseTcpPorts": {
350
+ "type": "boolean",
351
+ "default": true
352
+ },
345
353
  "workers": {
346
354
  "anyOf": [
347
355
  {
@@ -618,6 +626,10 @@
618
626
  "useHttp": {
619
627
  "type": "boolean"
620
628
  },
629
+ "reuseTcpPorts": {
630
+ "type": "boolean",
631
+ "default": true
632
+ },
621
633
  "workers": {
622
634
  "anyOf": [
623
635
  {
@@ -894,6 +906,10 @@
894
906
  "useHttp": {
895
907
  "type": "boolean"
896
908
  },
909
+ "reuseTcpPorts": {
910
+ "type": "boolean",
911
+ "default": true
912
+ },
897
913
  "workers": {
898
914
  "anyOf": [
899
915
  {
@@ -1464,6 +1480,10 @@
1464
1480
  },
1465
1481
  "additionalProperties": false
1466
1482
  },
1483
+ "reuseTcpPorts": {
1484
+ "type": "boolean",
1485
+ "default": true
1486
+ },
1467
1487
  "startTimeout": {
1468
1488
  "default": 30000,
1469
1489
  "type": "number",