@platformatic/runtime 2.67.0-alpha.1 → 2.67.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
@@ -5,7 +5,7 @@
5
5
  * and run json-schema-to-typescript to regenerate this file.
6
6
  */
7
7
 
8
- export type HttpsSchemasPlatformaticDevPlatformaticRuntime2670Alpha1Json = {
8
+ export type HttpsSchemasPlatformaticDevPlatformaticRuntime2670Json = {
9
9
  [k: string]: unknown;
10
10
  } & {
11
11
  $schema?: string;
package/lib/config.js CHANGED
@@ -251,7 +251,7 @@ platformaticRuntime.configManagerConfig = {
251
251
  upgrade
252
252
  }
253
253
 
254
- async function wrapConfigInRuntimeConfig ({ configManager, args }) {
254
+ async function wrapConfigInRuntimeConfig ({ configManager, args, opts }) {
255
255
  let serviceId = 'main'
256
256
  try {
257
257
  const packageJson = join(configManager.dirname, 'package.json')
@@ -272,7 +272,7 @@ async function wrapConfigInRuntimeConfig ({ configManager, args }) {
272
272
  const wrapperConfig = {
273
273
  $schema: schema.$id,
274
274
  server,
275
- watch: true,
275
+ watch: !args?.production,
276
276
  ...omitProperties(configManager.current.runtime ?? {}, runtimeUnwrappablePropertiesList),
277
277
  entrypoint: serviceId,
278
278
  services: [
@@ -298,7 +298,7 @@ async function wrapConfigInRuntimeConfig ({ configManager, args }) {
298
298
  }
299
299
  })
300
300
 
301
- await cm.parseAndValidate()
301
+ await cm.parseAndValidate(true, [], opts)
302
302
 
303
303
  return cm
304
304
  }
package/lib/errors.js CHANGED
@@ -38,6 +38,7 @@ module.exports = {
38
38
  FailedToUnlinkManagementApiSocket: createError(`${ERROR_PREFIX}_FAILED_TO_UNLINK_MANAGEMENT_API_SOCKET`, 'Failed to unlink management API socket "%s"'),
39
39
  LogFileNotFound: createError(`${ERROR_PREFIX}_LOG_FILE_NOT_FOUND`, 'Log file with index %s not found', 404),
40
40
  WorkerIsRequired: createError(`${ERROR_PREFIX}_REQUIRED_WORKER`, 'The worker parameter is required'),
41
+ InvalidArgumentError: createError(`${ERROR_PREFIX}_INVALID_ARGUMENT`, 'Invalid argument: "%s"'),
41
42
 
42
43
  // TODO: should remove next one as it's not used anymore
43
44
  CannotRemoveServiceOnUpdateError: createError(`${ERROR_PREFIX}_CANNOT_REMOVE_SERVICE_ON_UPDATE`, 'Cannot remove service "%s" when updating a Runtime'),
@@ -150,6 +150,9 @@ class RuntimeGenerator extends BaseGenerator {
150
150
  this.config.port = configManager.env.PORT
151
151
  this.entryPoint = configManager.current.services.find(svc => svc.entrypoint)
152
152
  this.existingServices = configManager.current.services.map(s => s.id)
153
+
154
+ this.updateRuntimeConfig(this.existingConfigRaw)
155
+ this.updateRuntimeEnv(await readFile(join(this.targetDirectory, '.env'), 'utf-8'))
153
156
  }
154
157
  }
155
158
 
@@ -204,11 +207,7 @@ class RuntimeGenerator extends BaseGenerator {
204
207
  ...servicesEnv
205
208
  })
206
209
 
207
- this.addFile({
208
- path: '',
209
- file: '.env',
210
- contents: envObjectToString(this.config.env)
211
- })
210
+ this.updateRuntimeEnv(envObjectToString(this.config.env))
212
211
 
213
212
  this.addFile({
214
213
  path: '',
@@ -227,12 +226,21 @@ class RuntimeGenerator extends BaseGenerator {
227
226
  }
228
227
 
229
228
  async writeFiles () {
229
+ for (const { service } of this.services) {
230
+ await service._beforeWriteFiles?.(this)
231
+ }
232
+
230
233
  await super.writeFiles()
234
+
231
235
  if (!this.config.isUpdating) {
232
236
  for (const { service } of this.services) {
233
237
  await service.writeFiles()
234
238
  }
235
239
  }
240
+
241
+ for (const { service } of this.services) {
242
+ await service._afterWriteFiles?.(this)
243
+ }
236
244
  }
237
245
 
238
246
  async prepareQuestions () {
@@ -275,6 +283,7 @@ class RuntimeGenerator extends BaseGenerator {
275
283
  } else {
276
284
  basePath = join(this.targetDirectory, this.config.autoload || this.servicesFolder)
277
285
  }
286
+ this.servicesBasePath = basePath
278
287
  service.setTargetDirectory(join(basePath, service.config.serviceName))
279
288
  })
280
289
  }
@@ -496,16 +505,59 @@ class RuntimeGenerator extends BaseGenerator {
496
505
 
497
506
  this.setEntryPoint(newEntrypoint)
498
507
  runtimePkgConfigFileData.entrypoint = newEntrypoint
499
- this.addFile({
500
- path: '',
501
- file: this.runtimeConfig,
502
- contents: JSON.stringify(runtimePkgConfigFileData, null, 2)
503
- })
508
+ this.updateRuntimeConfig(runtimePkgConfigFileData)
504
509
  }
505
510
  await this.writeFiles()
506
511
  // save new env
507
512
  await envTool.save()
508
513
  }
514
+
515
+ async generateConfigFile () {
516
+ this.updateRuntimeConfig(await super.generateConfigFile())
517
+ }
518
+
519
+ async generateEnv () {
520
+ const serialized = await super.generateEnv()
521
+
522
+ if (serialized) {
523
+ this.updateRuntimeEnv(serialized)
524
+ }
525
+ }
526
+
527
+ getRuntimeConfigFileObject () {
528
+ return this.files.find(file => file.tags?.includes('runtime-config')) ?? null
529
+ }
530
+
531
+ getRuntimeEnvFileObject () {
532
+ return this.files.find(file => file.tags?.includes('runtime-env')) ?? null
533
+ }
534
+
535
+ updateRuntimeConfig (config) {
536
+ this.addFile({
537
+ path: '',
538
+ file: this.runtimeConfig,
539
+ contents: JSON.stringify(config, null, 2),
540
+ tags: ['runtime-config']
541
+ })
542
+ }
543
+
544
+ updateRuntimeEnv (contents) {
545
+ this.addFile({
546
+ path: '',
547
+ file: '.env',
548
+ contents,
549
+ tags: ['runtime-env']
550
+ })
551
+ }
552
+
553
+ updateConfigEntryPoint (entrypoint) {
554
+ // This can return null if the generator was not supposed to modify the config
555
+ const configObject = this.getRuntimeConfigFileObject()
556
+ const config = JSON.parse(configObject.contents)
557
+ config.entrypoint = entrypoint
558
+
559
+ this.updateRuntimeConfig(config)
560
+ }
509
561
  }
510
562
 
511
563
  class WrappedGenerator extends BaseGenerator {
@@ -93,6 +93,29 @@ async function startPrometheusServer (runtime, opts) {
93
93
  onRequestHook = promServer.basicAuth
94
94
  }
95
95
 
96
+ const readinessEndpoint = opts.readiness?.endpoint ?? DEFAULT_READINESS_ENDPOINT
97
+ const livenessEndpoint = opts.liveness?.endpoint ?? DEFAULT_LIVENESS_ENDPOINT
98
+
99
+ promServer.route({
100
+ url: '/',
101
+ method: 'GET',
102
+ logLevel: 'warn',
103
+ handler (req, reply) {
104
+ reply.type('text/plain')
105
+ let response = `Hello from Platformatic Prometheus Server!\nThe metrics are available at ${metricsEndpoint}.`
106
+
107
+ if (opts.readiness !== false) {
108
+ response += `\nThe readiness endpoint is available at ${readinessEndpoint}.`
109
+ }
110
+
111
+ if (opts.liveness !== false) {
112
+ response += `\nThe liveness endpoint is available at ${livenessEndpoint}.`
113
+ }
114
+
115
+ return response
116
+ }
117
+ })
118
+
96
119
  promServer.route({
97
120
  url: metricsEndpoint,
98
121
  method: 'GET',
@@ -112,7 +135,7 @@ async function startPrometheusServer (runtime, opts) {
112
135
  const failBody = opts.readiness?.fail?.body ?? DEFAULT_READINESS_FAIL_BODY
113
136
 
114
137
  promServer.route({
115
- url: opts.readiness?.endpoint ?? DEFAULT_READINESS_ENDPOINT,
138
+ url: readinessEndpoint,
116
139
  method: 'GET',
117
140
  logLevel: 'warn',
118
141
  handler: async (req, reply) => {
@@ -151,7 +174,7 @@ async function startPrometheusServer (runtime, opts) {
151
174
  const failBody = opts.liveness?.fail?.body ?? DEFAULT_LIVENESS_FAIL_BODY
152
175
 
153
176
  promServer.route({
154
- url: opts.liveness?.endpoint ?? DEFAULT_LIVENESS_ENDPOINT,
177
+ url: livenessEndpoint,
155
178
  method: 'GET',
156
179
  logLevel: 'warn',
157
180
  handler: async (req, reply) => {
package/lib/runtime.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { ITC } = require('@platformatic/itc')
4
- const { ensureLoggableError, executeWithTimeout, deepmerge } = require('@platformatic/utils')
4
+ const { features, ensureLoggableError, executeWithTimeout, deepmerge } = require('@platformatic/utils')
5
5
  const { once, EventEmitter } = require('node:events')
6
6
  const { createReadStream, watch, existsSync } = require('node:fs')
7
7
  const { readdir, readFile, stat, access } = require('node:fs/promises')
@@ -50,6 +50,7 @@ const COLLECT_METRICS_TIMEOUT = 1000
50
50
 
51
51
  const MAX_BOOTSTRAP_ATTEMPTS = 5
52
52
  const IMMEDIATE_RESTART_MAX_THRESHOLD = 10
53
+ const MAX_WORKERS = 100
53
54
 
54
55
  const telemetryPath = require.resolve('@platformatic/telemetry')
55
56
  const openTelemetrySetupPath = join(telemetryPath, '..', 'lib', 'node-telemetry.js')
@@ -129,7 +130,19 @@ class Runtime extends EventEmitter {
129
130
 
130
131
  this.#isProduction = this.#configManager.args?.production ?? false
131
132
  this.#servicesIds = config.services.map(service => service.id)
132
- this.#workers.configure(config.services, this.#configManager.current.workers, this.#isProduction)
133
+
134
+ const workersConfig = []
135
+ for (const service of config.services) {
136
+ const count = service.workers ?? this.#configManager.current.workers
137
+ if (count > 1 && service.entrypoint && !features.node.reusePort) {
138
+ this.logger.warn(`"${service.id}" is set as the entrypoint, but reusePort is not available in your OS; setting workers to 1 instead of ${count}`)
139
+ workersConfig.push({ id: service.id, workers: 1 })
140
+ } else {
141
+ workersConfig.push({ id: service.id, workers: count })
142
+ }
143
+ }
144
+
145
+ this.#workers.configure(workersConfig)
133
146
 
134
147
  if (this.#isProduction) {
135
148
  this.#env['PLT_DEV'] = 'false'
@@ -198,17 +211,7 @@ class Runtime extends EventEmitter {
198
211
  await this.closeAndThrow(e)
199
212
  }
200
213
 
201
- const dispatcherOpts = { ...config.undici }
202
- const interceptors = [this.#meshInterceptor]
203
-
204
- if (config.httpCache) {
205
- this.#sharedHttpCache = await createSharedStore(this.#configManager.dirname, config.httpCache)
206
- interceptors.push(
207
- undiciInterceptors.cache({ store: this.#sharedHttpCache, methods: config.httpCache.methods ?? ['GET', 'HEAD'] })
208
- )
209
- }
210
-
211
- this.#dispatcher = new Agent(dispatcherOpts).compose(interceptors)
214
+ await this.#setDispatcher(config.undici)
212
215
 
213
216
  if (config.scheduler) {
214
217
  this.#scheduler = startScheduler(config.scheduler, this.#dispatcher, logger)
@@ -477,6 +480,22 @@ class Runtime extends EventEmitter {
477
480
  }
478
481
  }
479
482
 
483
+ async updateUndiciInterceptors (undiciConfig) {
484
+ this.#configManager.current.undici = undiciConfig
485
+
486
+ const promises = []
487
+ for (const worker of this.#workers.values()) {
488
+ promises.push(sendViaITC(worker, 'updateUndiciInterceptors', undiciConfig))
489
+ }
490
+
491
+ const results = await Promise.allSettled(promises)
492
+ for (const result of results) {
493
+ if (result.status === 'rejected') {
494
+ throw result.reason
495
+ }
496
+ }
497
+ }
498
+
480
499
  startCollectingMetrics () {
481
500
  this.#metrics = []
482
501
  this.#metricsTimeout = setInterval(async () => {
@@ -1041,6 +1060,24 @@ class Runtime extends EventEmitter {
1041
1060
  return super.emit(event, payload)
1042
1061
  }
1043
1062
 
1063
+ async #setDispatcher (undiciConfig) {
1064
+ const config = this.#configManager.current
1065
+
1066
+ const dispatcherOpts = { ...undiciConfig }
1067
+ const interceptors = [this.#meshInterceptor]
1068
+
1069
+ if (config.httpCache) {
1070
+ this.#sharedHttpCache = await createSharedStore(this.#configManager.dirname, config.httpCache)
1071
+ interceptors.push(
1072
+ undiciInterceptors.cache({
1073
+ store: this.#sharedHttpCache,
1074
+ methods: config.httpCache.methods ?? ['GET', 'HEAD']
1075
+ })
1076
+ )
1077
+ }
1078
+ this.#dispatcher = new Agent(dispatcherOpts).compose(interceptors)
1079
+ }
1080
+
1044
1081
  #updateStatus (status, args) {
1045
1082
  this.#status = status
1046
1083
  this.emit(status, args)
@@ -1279,7 +1316,7 @@ class Runtime extends EventEmitter {
1279
1316
  }
1280
1317
 
1281
1318
  // This must be done here as the dependencies are filled above
1282
- worker[kConfig] = { ...serviceConfig, health }
1319
+ worker[kConfig] = { ...serviceConfig, health, workers: workersCount }
1283
1320
  worker[kWorkerStatus] = 'init'
1284
1321
  this.emit('service:worker:init', eventPayload)
1285
1322
 
@@ -1809,6 +1846,86 @@ class Runtime extends EventEmitter {
1809
1846
  await immediate()
1810
1847
  }
1811
1848
  }
1849
+
1850
+ async getServiceResourcesInfo (id) {
1851
+ const workers = this.#workers.getCount(id)
1852
+ return { workers }
1853
+ }
1854
+
1855
+ async #updateWorkerCount (serviceId, workers) {
1856
+ this.#configManager.current.services.find(s => s.id === serviceId).workers = workers
1857
+ const service = await this.#getServiceById(serviceId)
1858
+ this.#workers.setCount(serviceId, workers)
1859
+ service[kConfig].workers = workers
1860
+
1861
+ const promises = []
1862
+ for (const [workerId, worker] of this.#workers.entries()) {
1863
+ if (workerId.startsWith(`${serviceId}:`)) {
1864
+ promises.push(sendViaITC(worker, 'updateWorkersCount', { serviceId, workers }))
1865
+ }
1866
+ }
1867
+
1868
+ const results = await Promise.allSettled(promises)
1869
+ for (const result of results) {
1870
+ if (result.status === 'rejected') {
1871
+ throw result.reason
1872
+ }
1873
+ }
1874
+ }
1875
+
1876
+ async updateServicesResources (updates) {
1877
+ if (this.#status === 'stopping' || this.#status === 'closed') return
1878
+
1879
+ if (!Array.isArray(updates)) {
1880
+ throw new errors.InvalidArgumentError('updates', 'must be an array')
1881
+ }
1882
+ if (updates.length === 0) {
1883
+ throw new errors.InvalidArgumentError('updates', 'must have at least one element')
1884
+ }
1885
+
1886
+ const config = this.#configManager.current
1887
+
1888
+ for (const update of updates) {
1889
+ const { service: serviceId, workers } = update
1890
+
1891
+ if (typeof workers !== 'number') {
1892
+ throw new errors.InvalidArgumentError('workers', 'must be a number')
1893
+ }
1894
+ if (!serviceId) {
1895
+ throw new errors.InvalidArgumentError('service', 'must be a string')
1896
+ }
1897
+ if (workers <= 0) {
1898
+ throw new errors.InvalidArgumentError('workers', 'must be greater than 0')
1899
+ }
1900
+ if (workers > MAX_WORKERS) {
1901
+ throw new errors.InvalidArgumentError('workers', `must be less than ${MAX_WORKERS}`)
1902
+ }
1903
+ const serviceConfig = config.services.find(s => s.id === serviceId)
1904
+ if (!serviceConfig) {
1905
+ throw new errors.ServiceNotFoundError(serviceId, Array.from(this.#servicesIds).join(', '))
1906
+ }
1907
+
1908
+ const { workers: currentWorkers } = await this.getServiceResourcesInfo(serviceId)
1909
+ if (currentWorkers === workers) {
1910
+ this.logger.warn({ serviceId, workers }, 'No change in the number of workers for service')
1911
+ continue
1912
+ }
1913
+
1914
+ if (currentWorkers < workers) {
1915
+ await this.#updateWorkerCount(serviceId, workers)
1916
+ for (let i = currentWorkers; i < workers; i++) {
1917
+ await this.#setupWorker(config, serviceConfig, workers, serviceId, i)
1918
+ await this.#startWorker(config, serviceConfig, workers, serviceId, i, false, 0)
1919
+ }
1920
+ } else {
1921
+ for (let i = currentWorkers - 1; i >= workers; i--) {
1922
+ // keep the current workers count until the workers are stopped
1923
+ await this.#stopWorker(currentWorkers, serviceId, i, false)
1924
+ }
1925
+ await this.#updateWorkerCount(serviceId, workers)
1926
+ }
1927
+ }
1928
+ }
1812
1929
  }
1813
1930
 
1814
1931
  module.exports = { Runtime }
@@ -1,19 +1,208 @@
1
1
  'use strict'
2
2
 
3
+ const { join } = require('node:path')
4
+ const { workerData, parentPort } = require('node:worker_threads')
3
5
  const { pathToFileURL } = require('node:url')
6
+ const { createRequire } = require('@platformatic/utils')
7
+ const { setGlobalDispatcher, Client, Pool, Agent } = require('undici')
8
+ const { wire } = require('undici-thread-interceptor')
9
+ const { createTelemetryThreadInterceptorHooks } = require('@platformatic/telemetry')
10
+ const { RemoteCacheStore, httpCacheInterceptor } = require('./http-cache')
11
+ const { kInterceptors } = require('./symbols')
4
12
 
5
- async function loadInterceptor (_require, module, options) {
6
- const url = pathToFileURL(_require.resolve(module))
7
- const interceptor = (await import(url)).default
8
- return interceptor(options)
13
+ async function setDispatcher (runtimeConfig) {
14
+ const threadDispatcher = createThreadInterceptor(runtimeConfig)
15
+ const threadInterceptor = threadDispatcher.interceptor
16
+
17
+ let cacheInterceptor = null
18
+ if (runtimeConfig.httpCache) {
19
+ cacheInterceptor = createHttpCacheInterceptor(runtimeConfig)
20
+ }
21
+
22
+ let userInterceptors = []
23
+ if (Array.isArray(runtimeConfig.undici?.interceptors)) {
24
+ const _require = createRequire(join(workerData.dirname, 'package.json'))
25
+ userInterceptors = await loadInterceptors(_require, runtimeConfig.undici.interceptors)
26
+ }
27
+
28
+ const dispatcherOpts = await getDispatcherOpts(runtimeConfig.undici)
29
+
30
+ setGlobalDispatcher(
31
+ new Agent(dispatcherOpts).compose(
32
+ [
33
+ threadInterceptor,
34
+ ...userInterceptors,
35
+ cacheInterceptor
36
+ ].filter(Boolean)
37
+ )
38
+ )
39
+
40
+ return { threadDispatcher }
41
+ }
42
+
43
+ async function updateUndiciInterceptors (undiciConfig) {
44
+ const updatableInterceptors = globalThis[kInterceptors]
45
+ if (!updatableInterceptors) return
46
+
47
+ if (Array.isArray(undiciConfig?.interceptors)) {
48
+ for (const interceptorConfig of undiciConfig.interceptors) {
49
+ const { module, options } = interceptorConfig
50
+
51
+ const interceptorCtx = updatableInterceptors[module]
52
+ if (!interceptorCtx) continue
53
+
54
+ const { createInterceptor, updateInterceptor } = interceptorCtx
55
+ updateInterceptor(createInterceptor(options))
56
+ }
57
+ } else {
58
+ for (const key of ['Agent', 'Pool', 'Client']) {
59
+ const interceptorConfigs = undiciConfig.interceptors[key]
60
+ if (!interceptorConfigs) continue
61
+
62
+ for (const interceptorConfig of interceptorConfigs) {
63
+ const { module, options } = interceptorConfig
64
+
65
+ const interceptorCtx = updatableInterceptors[key][module]
66
+ if (!interceptorCtx) continue
67
+
68
+ const { createInterceptor, updateInterceptor } = interceptorCtx
69
+ updateInterceptor(createInterceptor(options))
70
+ }
71
+ }
72
+ }
9
73
  }
10
74
 
11
- function loadInterceptors (_require, interceptors) {
75
+ function createUpdatableInterceptor (originInterceptor) {
76
+ let originalDispatcher = null
77
+ let originalDispatch = null
78
+
79
+ function updatableInterceptor (dispatch) {
80
+ originalDispatch = dispatch
81
+ originalDispatcher = originInterceptor(dispatch)
82
+
83
+ return function dispatcher (opts, handler) {
84
+ return originalDispatcher(opts, handler)
85
+ }
86
+ }
87
+
88
+ function updateInterceptor (newInterceptor) {
89
+ originalDispatcher = newInterceptor(originalDispatch)
90
+ }
91
+
92
+ return { updatableInterceptor, updateInterceptor }
93
+ }
94
+
95
+ async function loadInterceptors (_require, interceptorsConfigs, key) {
12
96
  return Promise.all(
13
- interceptors.map(async ({ module, options }) => {
14
- return loadInterceptor(_require, module, options)
97
+ interceptorsConfigs.map(async (interceptorConfig) => {
98
+ return loadInterceptor(_require, interceptorConfig, key)
15
99
  })
16
100
  )
17
101
  }
18
102
 
19
- module.exports = { loadInterceptors }
103
+ async function loadInterceptor (_require, interceptorConfig, key) {
104
+ let updatableInterceptors = globalThis[kInterceptors]
105
+ if (!updatableInterceptors) {
106
+ updatableInterceptors = {}
107
+ globalThis[kInterceptors] = updatableInterceptors
108
+ }
109
+
110
+ const { module, options } = interceptorConfig
111
+
112
+ const url = pathToFileURL(_require.resolve(module))
113
+ const createInterceptor = (await import(url)).default
114
+ const interceptor = createInterceptor(options)
115
+
116
+ const {
117
+ updatableInterceptor,
118
+ updateInterceptor
119
+ } = createUpdatableInterceptor(interceptor)
120
+
121
+ const interceptorCtx = { createInterceptor, updateInterceptor }
122
+
123
+ if (key !== undefined) {
124
+ if (!updatableInterceptors[key]) {
125
+ updatableInterceptors[key] = {}
126
+ }
127
+ updatableInterceptors[key][module] = interceptorCtx
128
+ } else {
129
+ updatableInterceptors[module] = interceptorCtx
130
+ }
131
+
132
+ return updatableInterceptor
133
+ }
134
+
135
+ async function getDispatcherOpts (undiciConfig) {
136
+ const dispatcherOpts = { ...undiciConfig }
137
+
138
+ const interceptorsConfigs = undiciConfig?.interceptors
139
+ if (!interceptorsConfigs || Array.isArray(interceptorsConfigs)) {
140
+ return dispatcherOpts
141
+ }
142
+
143
+ const _require = createRequire(join(workerData.dirname, 'package.json'))
144
+
145
+ const clientInterceptors = []
146
+ const poolInterceptors = []
147
+
148
+ for (const key of ['Agent', 'Pool', 'Client']) {
149
+ const interceptorConfig = undiciConfig.interceptors[key]
150
+ if (!interceptorConfig) continue
151
+
152
+ const interceptors = await loadInterceptors(_require, interceptorConfig, key)
153
+ if (key === 'Agent') {
154
+ clientInterceptors.push(...interceptors)
155
+ poolInterceptors.push(...interceptors)
156
+ }
157
+ if (key === 'Pool') {
158
+ poolInterceptors.push(...interceptors)
159
+ }
160
+ if (key === 'Client') {
161
+ clientInterceptors.push(...interceptors)
162
+ }
163
+ }
164
+
165
+ dispatcherOpts.factory = (origin, opts) => {
166
+ return opts && opts.connections === 1
167
+ ? new Client(origin, opts).compose(clientInterceptors)
168
+ : new Pool(origin, opts).compose(poolInterceptors)
169
+ }
170
+
171
+ return dispatcherOpts
172
+ }
173
+
174
+ function createThreadInterceptor (runtimeConfig) {
175
+ const telemetry = runtimeConfig.telemetry
176
+ const hooks = telemetry ? createTelemetryThreadInterceptorHooks() : {}
177
+ const threadDispatcher = wire({
178
+ // Specifying the domain is critical to avoid flooding the DNS
179
+ // with requests for a domain that's never going to exist.
180
+ domain: '.plt.local',
181
+ port: parentPort,
182
+ timeout: runtimeConfig.serviceTimeout,
183
+ ...hooks
184
+ })
185
+ return threadDispatcher
186
+ }
187
+
188
+ function createHttpCacheInterceptor (runtimeConfig) {
189
+ const cacheInterceptor = httpCacheInterceptor({
190
+ store: new RemoteCacheStore({
191
+ onRequest: opts => {
192
+ globalThis.platformatic?.onHttpCacheRequest?.(opts)
193
+ },
194
+ onCacheHit: opts => {
195
+ globalThis.platformatic?.onHttpCacheHit?.(opts)
196
+ },
197
+ onCacheMiss: opts => {
198
+ globalThis.platformatic?.onHttpCacheMiss?.(opts)
199
+ },
200
+ logger: globalThis.platformatic.logger
201
+ }),
202
+ methods: runtimeConfig.httpCache.methods ?? ['GET', 'HEAD'],
203
+ logger: globalThis.platformatic.logger
204
+ })
205
+ return cacheInterceptor
206
+ }
207
+
208
+ module.exports = { setDispatcher, updateUndiciInterceptors }
package/lib/worker/itc.js CHANGED
@@ -1,12 +1,13 @@
1
1
  'use strict'
2
2
 
3
3
  const { once } = require('node:events')
4
- const { parentPort } = require('node:worker_threads')
4
+ const { parentPort, workerData } = require('node:worker_threads')
5
5
 
6
6
  const { ITC } = require('@platformatic/itc')
7
7
  const { Unpromise } = require('@watchable/unpromise')
8
8
 
9
9
  const errors = require('../errors')
10
+ const { updateUndiciInterceptors } = require('./interceptors')
10
11
  const { kITC, kId, kServiceId, kWorkerId } = require('./symbols')
11
12
 
12
13
  async function safeHandleInITC (worker, fn) {
@@ -109,6 +110,18 @@ function setupITC (app, service, dispatcher) {
109
110
  return app.stackable.inject(injectParams)
110
111
  },
111
112
 
113
+ async updateUndiciInterceptors (undiciConfig) {
114
+ await updateUndiciInterceptors(undiciConfig)
115
+ },
116
+
117
+ async updateWorkersCount (data) {
118
+ const { serviceId, workers } = data
119
+ const w = workerData.config.serviceMap.get(serviceId)
120
+ if (w) { w.workers = workers }
121
+ workerData.serviceConfig.workers = workers
122
+ workerData.worker.count = workers
123
+ },
124
+
112
125
  getStatus () {
113
126
  return app.getStatus()
114
127
  },
@@ -2,16 +2,14 @@
2
2
 
3
3
  const { EventEmitter } = require('node:events')
4
4
  const { hostname } = require('node:os')
5
- const { join, resolve } = require('node:path')
6
- const { parentPort, workerData, threadId } = require('node:worker_threads')
5
+ const { resolve } = require('node:path')
6
+ const { workerData, threadId } = require('node:worker_threads')
7
7
  const { pathToFileURL } = require('node:url')
8
8
  const inspector = require('node:inspector')
9
9
  const diagnosticChannel = require('node:diagnostics_channel')
10
10
  const { ServerResponse } = require('node:http')
11
11
 
12
- const { createTelemetryThreadInterceptorHooks } = require('@platformatic/telemetry')
13
12
  const {
14
- createRequire,
15
13
  disablePinoDirectWrite,
16
14
  ensureFlushedWorkerStdio,
17
15
  executeWithTimeout,
@@ -22,14 +20,11 @@ const {
22
20
  } = require('@platformatic/utils')
23
21
  const dotenv = require('dotenv')
24
22
  const pino = require('pino')
25
- const { fetch, setGlobalDispatcher, getGlobalDispatcher, Agent } = require('undici')
26
- const { wire } = require('undici-thread-interceptor')
27
- const undici = require('undici')
23
+ const { fetch } = require('undici')
28
24
 
29
- const { RemoteCacheStore, httpCacheInterceptor } = require('./http-cache')
30
25
  const { PlatformaticApp } = require('./app')
31
26
  const { setupITC } = require('./itc')
32
- const { loadInterceptors } = require('./interceptors')
27
+ const { setDispatcher } = require('./interceptors')
33
28
  const { kId, kITC, kStderrMarker } = require('./symbols')
34
29
 
35
30
  function handleUnhandled (app, type, err) {
@@ -138,85 +133,7 @@ async function main () {
138
133
  Object.assign(process.env, service.env)
139
134
  }
140
135
 
141
- // Setup undici
142
- const interceptors = {}
143
- const composedInterceptors = []
144
-
145
- if (config.undici?.interceptors) {
146
- const _require = createRequire(join(workerData.dirname, 'package.json'))
147
- for (const key of ['Agent', 'Pool', 'Client']) {
148
- if (config.undici.interceptors[key]) {
149
- interceptors[key] = await loadInterceptors(_require, config.undici.interceptors[key])
150
- }
151
- }
152
-
153
- if (Array.isArray(config.undici.interceptors)) {
154
- composedInterceptors.push(...(await loadInterceptors(_require, config.undici.interceptors)))
155
- }
156
- }
157
-
158
- const dispatcherOpts = { ...config.undici }
159
-
160
- if (Object.keys(interceptors).length > 0) {
161
- const clientInterceptors = []
162
- const poolInterceptors = []
163
-
164
- if (interceptors.Agent) {
165
- clientInterceptors.push(...interceptors.Agent)
166
- poolInterceptors.push(...interceptors.Agent)
167
- }
168
-
169
- if (interceptors.Pool) {
170
- poolInterceptors.push(...interceptors.Pool)
171
- }
172
-
173
- if (interceptors.Client) {
174
- clientInterceptors.push(...interceptors.Client)
175
- }
176
-
177
- dispatcherOpts.factory = (origin, opts) => {
178
- return opts && opts.connections === 1
179
- ? new undici.Client(origin, opts).compose(clientInterceptors)
180
- : new undici.Pool(origin, opts).compose(poolInterceptors)
181
- }
182
- }
183
-
184
- setGlobalDispatcher(new Agent(dispatcherOpts))
185
-
186
- const { telemetry } = service
187
- const hooks = telemetry ? createTelemetryThreadInterceptorHooks() : {}
188
- // Setup mesh networker
189
- const threadDispatcher = wire({
190
- // Specifying the domain is critical to avoid flooding the DNS
191
- // with requests for a domain that's never going to exist.
192
- domain: '.plt.local',
193
- port: parentPort,
194
- timeout: config.serviceTimeout,
195
- ...hooks
196
- })
197
-
198
- if (config.httpCache) {
199
- const cacheInterceptor = httpCacheInterceptor({
200
- store: new RemoteCacheStore({
201
- onRequest: opts => {
202
- globalThis.platformatic?.onHttpCacheRequest?.(opts)
203
- },
204
- onCacheHit: opts => {
205
- globalThis.platformatic?.onHttpCacheHit?.(opts)
206
- },
207
- onCacheMiss: opts => {
208
- globalThis.platformatic?.onHttpCacheMiss?.(opts)
209
- },
210
- logger: globalThis.platformatic.logger
211
- }),
212
- methods: config.httpCache.methods ?? ['GET', 'HEAD'],
213
- logger: globalThis.platformatic.logger
214
- })
215
- composedInterceptors.push(cacheInterceptor)
216
- }
217
-
218
- const globalDispatcher = getGlobalDispatcher()
219
- setGlobalDispatcher(globalDispatcher.compose(composedInterceptors))
136
+ const { threadDispatcher } = await setDispatcher(config)
220
137
 
221
138
  // If the service is an entrypoint and runtime server config is defined, use it.
222
139
  let serverConfig = null
@@ -1,7 +1,5 @@
1
1
  'use strict'
2
2
 
3
- const { features } = require('@platformatic/utils')
4
-
5
3
  class RoundRobinMap extends Map {
6
4
  #instances
7
5
 
@@ -14,18 +12,11 @@ class RoundRobinMap extends Map {
14
12
  return { ...this.#instances }
15
13
  }
16
14
 
17
- // In development or for the entrypoint always use 1 worker
18
- configure (services, defaultInstances, production) {
15
+ configure (services) {
19
16
  this.#instances = {}
20
17
 
21
18
  for (const service of services) {
22
- let count = service.workers ?? defaultInstances
23
-
24
- if (!production || (service.entrypoint && !features.node.reusePort)) {
25
- count = 1
26
- }
27
-
28
- this.#instances[service.id] = { next: 0, count }
19
+ this.#instances[service.id] = { next: 0, count: service.workers }
29
20
  }
30
21
  }
31
22
 
@@ -33,6 +24,10 @@ class RoundRobinMap extends Map {
33
24
  return this.#instances[service].count
34
25
  }
35
26
 
27
+ setCount (service, count) {
28
+ this.#instances[service].count = count
29
+ }
30
+
36
31
  next (service) {
37
32
  if (!this.#instances[service]) {
38
33
  return undefined
@@ -8,6 +8,7 @@ const kWorkerId = Symbol.for('plt.runtime.worker.id')
8
8
  const kITC = Symbol.for('plt.runtime.itc')
9
9
  const kHealthCheckTimer = Symbol.for('plt.runtime.worker.healthCheckTimer')
10
10
  const kWorkerStatus = Symbol('plt.runtime.worker.status')
11
+ const kInterceptors = Symbol.for('plt.runtime.worker.interceptors')
11
12
 
12
13
  // This string marker should be safe to use since it belongs to Unicode private area
13
14
  const kStderrMarker = '\ue002'
@@ -21,5 +22,6 @@ module.exports = {
21
22
  kITC,
22
23
  kHealthCheckTimer,
23
24
  kWorkerStatus,
24
- kStderrMarker
25
+ kStderrMarker,
26
+ kInterceptors
25
27
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "2.67.0-alpha.1",
3
+ "version": "2.67.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -37,12 +37,12 @@
37
37
  "typescript": "^5.5.4",
38
38
  "undici-oidc-interceptor": "^0.5.0",
39
39
  "why-is-node-running": "^2.2.2",
40
- "@platformatic/composer": "2.67.0-alpha.1",
41
- "@platformatic/db": "2.67.0-alpha.1",
42
- "@platformatic/node": "2.67.0-alpha.1",
43
- "@platformatic/service": "2.67.0-alpha.1",
44
- "@platformatic/sql-graphql": "2.67.0-alpha.1",
45
- "@platformatic/sql-mapper": "2.67.0-alpha.1"
40
+ "@platformatic/composer": "2.67.0",
41
+ "@platformatic/db": "2.67.0",
42
+ "@platformatic/node": "2.67.0",
43
+ "@platformatic/service": "2.67.0",
44
+ "@platformatic/sql-mapper": "2.67.0",
45
+ "@platformatic/sql-graphql": "2.67.0"
46
46
  },
47
47
  "dependencies": {
48
48
  "@fastify/accepts": "^5.0.0",
@@ -77,21 +77,21 @@
77
77
  "undici": "^7.0.0",
78
78
  "undici-thread-interceptor": "^0.13.1",
79
79
  "ws": "^8.16.0",
80
- "@platformatic/basic": "2.67.0-alpha.1",
81
- "@platformatic/config": "2.67.0-alpha.1",
82
- "@platformatic/generators": "2.67.0-alpha.1",
83
- "@platformatic/itc": "2.67.0-alpha.1",
84
- "@platformatic/telemetry": "2.67.0-alpha.1",
85
- "@platformatic/ts-compiler": "2.67.0-alpha.1",
86
- "@platformatic/utils": "2.67.0-alpha.1"
80
+ "@platformatic/basic": "2.67.0",
81
+ "@platformatic/config": "2.67.0",
82
+ "@platformatic/generators": "2.67.0",
83
+ "@platformatic/itc": "2.67.0",
84
+ "@platformatic/telemetry": "2.67.0",
85
+ "@platformatic/ts-compiler": "2.67.0",
86
+ "@platformatic/utils": "2.67.0"
87
87
  },
88
88
  "scripts": {
89
- "test": "pnpm run lint && borp --concurrency=1 --timeout=300000 && tsd",
89
+ "test": "pnpm run lint && borp --concurrency=1 --timeout=600000 && tsd",
90
90
  "test:main": "borp --concurrency=1 --timeout=300000 test/*.test.js test/*.test.mjs test/versions/*.test.js test/versions/*.test.mjs",
91
91
  "test:api": "borp --concurrency=1 --timeout=300000 test/api/*.test.js test/api/*.test.mjs test/management-api/*.test.js test/management-api/*.test.mjs",
92
92
  "test:cli": "borp --concurrency=1 --timeout=300000 test/cli/*.test.js test/cli/*.test.mjs test/cli/**/*.test.js test/cli/**/*.test.mjs",
93
93
  "test:start": "borp --concurrency=1 --timeout=300000 test/start/*.test.js test/start/*.test.mjs",
94
- "test:multiple-workers": "borp --concurrency=1 --timeout=300000 test/multiple-workers/*.test.js test/multiple-workers/*.test.mjs",
94
+ "test:multiple-workers": "borp --concurrency=1 --timeout=600000 test/multiple-workers/*.test.js test/multiple-workers/*.test.mjs",
95
95
  "test:types": "tsd",
96
96
  "coverage": "pnpm run lint && borp -X fixtures -X test -C --concurrency=1 --timeout=300000 && tsd",
97
97
  "gen-schema": "node lib/schema.js > schema.json",
package/schema.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/runtime/2.67.0-alpha.1.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/runtime/2.67.0.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "type": "object",
5
5
  "properties": {
package/undefined DELETED
@@ -1,5 +0,0 @@
1
- {"level":30,"time":1747909640025,"pid":84550,"hostname":"work","msg":"Starting the service \"service\"..."}
2
- {"level":30,"time":1747909640055,"pid":84550,"hostname":"work","msg":"Started the service \"service\"..."}
3
- {"level":30,"time":1747909640055,"pid":84550,"hostname":"work","msg":"Platformatic is now listening at http://127.0.0.1:41619"}
4
- {"level":30,"time":1747909640066,"pid":84550,"hostname":"work","msg":"Stopping the service \"service\"..."}
5
- {"level":30,"time":1747909640069,"pid":84550,"hostname":"work","msg":"Stopped the service \"service\"..."}