@platformatic/runtime 2.67.0-alpha.2 → 2.67.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
@@ -5,7 +5,7 @@
5
5
  * and run json-schema-to-typescript to regenerate this file.
6
6
  */
7
7
 
8
- export type HttpsSchemasPlatformaticDevPlatformaticRuntime2670Alpha2Json = {
8
+ export type HttpsSchemasPlatformaticDevPlatformaticRuntime2671Json = {
9
9
  [k: string]: unknown;
10
10
  } & {
11
11
  $schema?: string;
package/lib/config.js CHANGED
@@ -140,14 +140,14 @@ async function _transformConfig (configManager, args) {
140
140
  const basic = await import('@platformatic/basic')
141
141
  service.isPLTService = false
142
142
  try {
143
- const imported = await basic.importStackableAndConfig(service.path)
144
- service.type = imported.stackable.default.configType
143
+ const { stackable } = await basic.importStackableAndConfig(service.path)
144
+ service.type = stackable.default.configType
145
145
  const _require = createRequire(service.path)
146
146
  // This is needed to work around Rust bug on dylibs:
147
147
  // https://github.com/rust-lang/rust/issues/91979
148
148
  // https://github.com/rollup/rollup/issues/5761
149
149
  // TODO(mcollina): we should expose this inside every stackable configuration.
150
- imported.stackable.default.modulesToLoad?.forEach(m => {
150
+ stackable.default.modulesToLoad?.forEach(m => {
151
151
  const toLoad = _require.resolve(m)
152
152
  loadModule(_require, toLoad).catch(() => {})
153
153
  })
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 {
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'
@@ -1303,7 +1316,7 @@ class Runtime extends EventEmitter {
1303
1316
  }
1304
1317
 
1305
1318
  // This must be done here as the dependencies are filled above
1306
- worker[kConfig] = { ...serviceConfig, health }
1319
+ worker[kConfig] = { ...serviceConfig, health, workers: workersCount }
1307
1320
  worker[kWorkerStatus] = 'init'
1308
1321
  this.emit('service:worker:init', eventPayload)
1309
1322
 
@@ -1833,6 +1846,86 @@ class Runtime extends EventEmitter {
1833
1846
  await immediate()
1834
1847
  }
1835
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
+ }
1836
1929
  }
1837
1930
 
1838
1931
  module.exports = { Runtime }
package/lib/worker/itc.js CHANGED
@@ -1,7 +1,7 @@
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')
@@ -114,6 +114,14 @@ function setupITC (app, service, dispatcher) {
114
114
  await updateUndiciInterceptors(undiciConfig)
115
115
  },
116
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
+
117
125
  getStatus () {
118
126
  return app.getStatus()
119
127
  },
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "2.67.0-alpha.2",
3
+ "version": "2.67.1",
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/db": "2.67.0-alpha.2",
41
- "@platformatic/node": "2.67.0-alpha.2",
42
- "@platformatic/service": "2.67.0-alpha.2",
43
- "@platformatic/sql-graphql": "2.67.0-alpha.2",
44
- "@platformatic/composer": "2.67.0-alpha.2",
45
- "@platformatic/sql-mapper": "2.67.0-alpha.2"
40
+ "@platformatic/db": "2.67.1",
41
+ "@platformatic/composer": "2.67.1",
42
+ "@platformatic/node": "2.67.1",
43
+ "@platformatic/service": "2.67.1",
44
+ "@platformatic/sql-mapper": "2.67.1",
45
+ "@platformatic/sql-graphql": "2.67.1"
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.2",
81
- "@platformatic/config": "2.67.0-alpha.2",
82
- "@platformatic/generators": "2.67.0-alpha.2",
83
- "@platformatic/itc": "2.67.0-alpha.2",
84
- "@platformatic/telemetry": "2.67.0-alpha.2",
85
- "@platformatic/ts-compiler": "2.67.0-alpha.2",
86
- "@platformatic/utils": "2.67.0-alpha.2"
80
+ "@platformatic/basic": "2.67.1",
81
+ "@platformatic/config": "2.67.1",
82
+ "@platformatic/generators": "2.67.1",
83
+ "@platformatic/itc": "2.67.1",
84
+ "@platformatic/telemetry": "2.67.1",
85
+ "@platformatic/ts-compiler": "2.67.1",
86
+ "@platformatic/utils": "2.67.1"
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.2.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/runtime/2.67.1.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "type": "object",
5
5
  "properties": {