@platformatic/runtime 2.6.1 → 2.8.0-alpha.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/lib/schema.js CHANGED
@@ -27,6 +27,10 @@ const services = {
27
27
  },
28
28
  useHttp: {
29
29
  type: 'boolean'
30
+ },
31
+ workers: {
32
+ type: 'number',
33
+ minimum: 1
30
34
  }
31
35
  }
32
36
  }
@@ -49,6 +53,9 @@ const platformaticRuntimeSchema = {
49
53
  entrypoint: {
50
54
  type: 'string'
51
55
  },
56
+ basePath: {
57
+ type: 'string'
58
+ },
52
59
  autoload: {
53
60
  type: 'object',
54
61
  additionalProperties: false,
@@ -87,6 +94,11 @@ const platformaticRuntimeSchema = {
87
94
  }
88
95
  },
89
96
  services,
97
+ workers: {
98
+ type: 'number',
99
+ minimum: 1,
100
+ default: 1
101
+ },
90
102
  web: services,
91
103
  logger,
92
104
  server,
package/lib/start.js CHANGED
@@ -18,6 +18,16 @@ const { Runtime } = require('./runtime')
18
18
  const errors = require('./errors')
19
19
  const { getRuntimeLogsDir, loadConfig } = require('./utils')
20
20
 
21
+ async function restartRuntime (runtime) {
22
+ runtime.logger.info('Received SIGUSR2, restarting all services ...')
23
+
24
+ try {
25
+ await runtime.restart()
26
+ } catch (err) {
27
+ runtime.logger.error({ err: ensureLoggableError(err) }, 'Failed to restart services.')
28
+ }
29
+ }
30
+
21
31
  async function buildRuntime (configManager, env) {
22
32
  env = env || process.env
23
33
 
@@ -35,14 +45,10 @@ async function buildRuntime (configManager, env) {
35
45
  const runtime = new Runtime(configManager, runtimeLogsDir, env)
36
46
 
37
47
  /* c8 ignore next 3 */
38
- process.on('SIGUSR2', async () => {
39
- runtime.logger.info('Received SIGUSR2, restarting all services ...')
40
-
41
- try {
42
- await runtime.restart()
43
- } catch (err) {
44
- runtime.logger.error({ err: ensureLoggableError(err) }, 'Failed to restart services.')
45
- }
48
+ const restartListener = restartRuntime.bind(null, runtime)
49
+ process.on('SIGUSR2', restartListener)
50
+ runtime.on('closed', () => {
51
+ process.removeListener('SIGUSR2', restartListener)
46
52
  })
47
53
 
48
54
  try {
@@ -109,7 +115,7 @@ async function setupAndStartRuntime (config) {
109
115
  })
110
116
  )
111
117
  logger.warn(`Port: ${originalPort} is already in use!`)
112
- logger.warn(`Starting service on port: ${runtimeConfig.current.server.port}`)
118
+ logger.warn(`Changing the port to ${runtimeConfig.current.server.port}`)
113
119
  }
114
120
  return { address, runtime }
115
121
  }
package/lib/worker/app.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const { existsSync } = require('node:fs')
4
4
  const { EventEmitter } = require('node:events')
5
5
  const { resolve } = require('node:path')
6
+ const { workerData } = require('node:worker_threads')
6
7
  const { ConfigManager } = require('@platformatic/config')
7
8
  const { FileWatcher } = require('@platformatic/utils')
8
9
  const { getGlobalDispatcher, setGlobalDispatcher } = require('undici')
@@ -21,9 +22,20 @@ class PlatformaticApp extends EventEmitter {
21
22
  #debouncedRestart
22
23
  #context
23
24
 
24
- constructor (appConfig, telemetryConfig, loggerConfig, serverConfig, metricsConfig, hasManagementApi, watch) {
25
+ constructor (
26
+ appConfig,
27
+ workerId,
28
+ telemetryConfig,
29
+ loggerConfig,
30
+ serverConfig,
31
+ metricsConfig,
32
+ hasManagementApi,
33
+ watch
34
+ ) {
25
35
  super()
26
36
  this.appConfig = appConfig
37
+ this.serviceId = this.appConfig.id
38
+ this.workerId = workerId
27
39
  this.#watch = watch
28
40
  this.#starting = false
29
41
  this.#started = false
@@ -32,7 +44,8 @@ class PlatformaticApp extends EventEmitter {
32
44
  this.#fileWatcher = null
33
45
 
34
46
  this.#context = {
35
- serviceId: this.appConfig.id,
47
+ serviceId: this.serviceId,
48
+ workerId: this.workerId,
36
49
  directory: this.appConfig.path,
37
50
  isEntrypoint: this.appConfig.entrypoint,
38
51
  isProduction: this.appConfig.isProduction,
@@ -40,6 +53,7 @@ class PlatformaticApp extends EventEmitter {
40
53
  metricsConfig,
41
54
  loggerConfig,
42
55
  serverConfig,
56
+ worker: workerData?.worker,
43
57
  hasManagementApi: !!hasManagementApi,
44
58
  localServiceEnvVars: this.appConfig.localServiceEnvVars
45
59
  }
package/lib/worker/itc.js CHANGED
@@ -7,7 +7,7 @@ const { ITC } = require('@platformatic/itc')
7
7
  const { Unpromise } = require('@watchable/unpromise')
8
8
 
9
9
  const errors = require('../errors')
10
- const { kITC, kId } = require('./symbols')
10
+ const { kITC, kId, kServiceId, kWorkerId } = require('./symbols')
11
11
 
12
12
  async function safeHandleInITC (worker, fn) {
13
13
  try {
@@ -23,7 +23,11 @@ async function safeHandleInITC (worker, fn) {
23
23
  ])
24
24
 
25
25
  if (typeof exitCode === 'number') {
26
- throw new errors.ServiceExitedError(worker[kId], exitCode)
26
+ if (typeof worker[kWorkerId] !== 'undefined') {
27
+ throw new errors.WorkerExitedError(worker[kWorkerId], worker[kServiceId], exitCode)
28
+ } else {
29
+ throw new errors.ServiceExitedError(worker[kId], exitCode)
30
+ }
27
31
  } else {
28
32
  ac.abort()
29
33
  }
@@ -156,6 +160,7 @@ function setupITC (app, service, dispatcher) {
156
160
  itc.notify('changed')
157
161
  })
158
162
 
163
+ itc.listen()
159
164
  return itc
160
165
  }
161
166
 
@@ -1,10 +1,13 @@
1
1
  'use strict'
2
2
 
3
3
  const { createRequire } = require('node:module')
4
+ const { hostname } = require('node:os')
4
5
  const { join } = require('node:path')
5
6
  const { parentPort, workerData, threadId } = require('node:worker_threads')
6
7
  const { pathToFileURL } = require('node:url')
7
8
  const inspector = require('node:inspector')
9
+ const diagnosticChannel = require('node:diagnostics_channel')
10
+ const { ServerResponse } = require('node:http')
8
11
 
9
12
  const pino = require('pino')
10
13
  const { fetch, setGlobalDispatcher, Agent } = require('undici')
@@ -28,14 +31,17 @@ globalThis.fetch = fetch
28
31
  globalThis[kId] = threadId
29
32
 
30
33
  let app
34
+
31
35
  const config = workerData.config
32
36
  globalThis.platformatic = Object.assign(globalThis.platformatic ?? {}, { logger: createLogger() })
33
37
 
34
38
  function handleUnhandled (type, err) {
35
- globalThis.platformatic.logger.error(
36
- { err: ensureLoggableError(err) },
37
- `Service ${workerData.serviceConfig.id} threw an ${type}.`
38
- )
39
+ const label =
40
+ workerData.worker.count > 1
41
+ ? `worker ${workerData.worker.index} of the service "${workerData.serviceConfig.id}"`
42
+ : `service "${workerData.serviceConfig.id}"`
43
+
44
+ globalThis.platformatic.logger.error({ err: ensureLoggableError(err) }, `The ${label} threw an ${type}.`)
39
45
 
40
46
  executeWithTimeout(app?.stop(), 1000)
41
47
  .catch()
@@ -46,7 +52,13 @@ function handleUnhandled (type, err) {
46
52
 
47
53
  function createLogger () {
48
54
  const destination = new MessagePortWritable({ port: workerData.loggingPort })
49
- const loggerInstance = pino({ level: 'trace', name: workerData.serviceConfig.id }, destination)
55
+ const pinoOptions = { level: 'trace', name: workerData.serviceConfig.id }
56
+
57
+ if (typeof workerData.worker?.index !== 'undefined') {
58
+ pinoOptions.base = { pid: process.pid, hostname: hostname(), worker: workerData.worker.index }
59
+ }
60
+
61
+ const loggerInstance = pino(pinoOptions, destination)
50
62
 
51
63
  Reflect.defineProperty(process, 'stdout', { value: createPinoWritable(loggerInstance, 'info') })
52
64
  Reflect.defineProperty(process, 'stderr', { value: createPinoWritable(loggerInstance, 'error') })
@@ -131,6 +143,7 @@ async function main () {
131
143
  // Create the application
132
144
  app = new PlatformaticApp(
133
145
  service,
146
+ workerData.worker.count > 1 ? workerData.worker.index : undefined,
134
147
  telemetryConfig,
135
148
  config.logger,
136
149
  serverConfig,
@@ -141,15 +154,70 @@ async function main () {
141
154
 
142
155
  await app.init()
143
156
 
157
+ if (service.entrypoint && config.basePath) {
158
+ const meta = await app.stackable.getMeta()
159
+ if (!meta.wantsAbsoluteUrls) {
160
+ stripBasePath(config.basePath)
161
+ }
162
+ }
163
+
144
164
  // Setup interaction with parent port
145
165
  const itc = setupITC(app, service, threadDispatcher)
166
+ globalThis[kITC] = itc
146
167
 
147
168
  // Get the dependencies
148
169
  const dependencies = config.autoload ? await app.getBootstrapDependencies() : []
149
170
  itc.notify('init', { dependencies })
150
- itc.listen()
171
+ }
151
172
 
152
- globalThis[kITC] = itc
173
+ function stripBasePath (basePath) {
174
+ const kBasePath = Symbol('kBasePath')
175
+
176
+ diagnosticChannel.subscribe('http.server.request.start', ({ request, response }) => {
177
+ if (request.url.startsWith(basePath)) {
178
+ request.url = request.url.slice(basePath.length)
179
+
180
+ if (request.url.charAt(0) !== '/') {
181
+ request.url = '/' + request.url
182
+ }
183
+
184
+ response[kBasePath] = basePath
185
+ }
186
+ })
187
+
188
+ const originWriteHead = ServerResponse.prototype.writeHead
189
+ const originSetHeader = ServerResponse.prototype.setHeader
190
+
191
+ ServerResponse.prototype.writeHead = function (statusCode, statusMessage, headers) {
192
+ if (this[kBasePath] !== undefined) {
193
+ if (headers === undefined && typeof statusMessage === 'object') {
194
+ headers = statusMessage
195
+ statusMessage = undefined
196
+ }
197
+
198
+ if (headers) {
199
+ for (const key in headers) {
200
+ if (
201
+ key.toLowerCase() === 'location' &&
202
+ !headers[key].startsWith(basePath)
203
+ ) {
204
+ headers[key] = basePath + headers[key]
205
+ }
206
+ }
207
+ }
208
+ }
209
+
210
+ return originWriteHead.call(this, statusCode, statusMessage, headers)
211
+ }
212
+
213
+ ServerResponse.prototype.setHeader = function (name, value) {
214
+ if (this[kBasePath]) {
215
+ if (name.toLowerCase() === 'location' && !value.startsWith(basePath)) {
216
+ value = basePath + value
217
+ }
218
+ }
219
+ originSetHeader.call(this, name, value)
220
+ }
153
221
  }
154
222
 
155
223
  // No need to catch this because there is the unhadledRejection handler on top.
@@ -0,0 +1,61 @@
1
+ 'use strict'
2
+
3
+ class RoundRobinMap extends Map {
4
+ #instances
5
+
6
+ constructor (iterable, instances) {
7
+ super(iterable)
8
+ this.#instances = instances
9
+ }
10
+
11
+ get configuration () {
12
+ return { ...this.#instances }
13
+ }
14
+
15
+ // In development or for the entrypoint always use 1 worker
16
+ configure (services, defaultInstances, production) {
17
+ this.#instances = {}
18
+
19
+ for (const service of services) {
20
+ let count = service.workers ?? defaultInstances
21
+
22
+ if (service.entrypoint || !production) {
23
+ count = 1
24
+ }
25
+
26
+ this.#instances[service.id] = { next: 0, count }
27
+ }
28
+ }
29
+
30
+ getCount (service) {
31
+ return this.#instances[service].count
32
+ }
33
+
34
+ next (service) {
35
+ if (!this.#instances[service]) {
36
+ return undefined
37
+ }
38
+
39
+ let worker
40
+ let { next, count } = this.#instances[service]
41
+
42
+ // Try count times to get the next worker. This is to handle the case where a worker is being restarted.
43
+ for (let i = 0; i < count; i++) {
44
+ const current = next++
45
+ if (next >= count) {
46
+ next = 0
47
+ }
48
+
49
+ worker = this.get(`${service}:${current}`)
50
+
51
+ if (worker) {
52
+ break
53
+ }
54
+ }
55
+
56
+ this.#instances[service].next = next
57
+ return worker
58
+ }
59
+ }
60
+
61
+ module.exports = { RoundRobinMap }
@@ -2,6 +2,11 @@
2
2
 
3
3
  const kConfig = Symbol.for('plt.runtime.config')
4
4
  const kId = Symbol.for('plt.runtime.id') // This is also used to detect if we are running in a Platformatic runtime thread
5
+ const kServiceId = Symbol.for('plt.runtime.service.id')
6
+ const kWorkerId = Symbol.for('plt.runtime.worker.id')
5
7
  const kITC = Symbol.for('plt.runtime.itc')
8
+ const kLoggerDestination = Symbol.for('plt.runtime.loggerDestination')
9
+ const kLoggingPort = Symbol.for('plt.runtime.logginPort')
10
+ const kWorkerStatus = Symbol('plt.runtime.worker.status')
6
11
 
7
- module.exports = { kConfig, kId, kITC }
12
+ module.exports = { kConfig, kId, kServiceId, kWorkerId, kITC, kLoggerDestination, kLoggingPort, kWorkerStatus }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "2.6.1",
3
+ "version": "2.8.0-alpha.1",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -35,11 +35,12 @@
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/composer": "2.6.1",
39
- "@platformatic/db": "2.6.1",
40
- "@platformatic/service": "2.6.1",
41
- "@platformatic/sql-mapper": "2.6.1",
42
- "@platformatic/sql-graphql": "2.6.1"
38
+ "@platformatic/composer": "2.8.0-alpha.1",
39
+ "@platformatic/db": "2.8.0-alpha.1",
40
+ "@platformatic/node": "2.8.0-alpha.1",
41
+ "@platformatic/service": "2.8.0-alpha.1",
42
+ "@platformatic/sql-graphql": "2.8.0-alpha.1",
43
+ "@platformatic/sql-mapper": "2.8.0-alpha.1"
43
44
  },
44
45
  "dependencies": {
45
46
  "@fastify/error": "^4.0.0",
@@ -70,17 +71,17 @@
70
71
  "undici": "^6.9.0",
71
72
  "undici-thread-interceptor": "^0.7.0",
72
73
  "ws": "^8.16.0",
73
- "@platformatic/basic": "2.6.1",
74
- "@platformatic/config": "2.6.1",
75
- "@platformatic/generators": "2.6.1",
76
- "@platformatic/itc": "2.6.1",
77
- "@platformatic/ts-compiler": "2.6.1",
78
- "@platformatic/telemetry": "2.6.1",
79
- "@platformatic/utils": "2.6.1"
74
+ "@platformatic/basic": "2.8.0-alpha.1",
75
+ "@platformatic/generators": "2.8.0-alpha.1",
76
+ "@platformatic/config": "2.8.0-alpha.1",
77
+ "@platformatic/telemetry": "2.8.0-alpha.1",
78
+ "@platformatic/itc": "2.8.0-alpha.1",
79
+ "@platformatic/ts-compiler": "2.8.0-alpha.1",
80
+ "@platformatic/utils": "2.8.0-alpha.1"
80
81
  },
81
82
  "scripts": {
82
- "test": "npm run lint && borp --concurrency=1 --timeout=180000 && tsd",
83
- "coverage": "npm run lint && borp -X fixtures -X test -C --concurrency=1 --timeout=180000 && tsd",
83
+ "test": "npm run lint && borp --concurrency=1 --timeout=300000 && tsd",
84
+ "coverage": "npm run lint && borp -X fixtures -X test -C --concurrency=1 --timeout=300000 && tsd",
84
85
  "gen-schema": "node lib/schema.js > schema.json",
85
86
  "gen-types": "json2ts > config.d.ts < schema.json",
86
87
  "build": "pnpm run gen-schema && pnpm run gen-types",
package/schema.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/runtime/2.6.1.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/runtime/2.8.0-alpha.1.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "type": "object",
5
5
  "properties": {
@@ -13,6 +13,9 @@
13
13
  "entrypoint": {
14
14
  "type": "string"
15
15
  },
16
+ "basePath": {
17
+ "type": "string"
18
+ },
16
19
  "autoload": {
17
20
  "type": "object",
18
21
  "additionalProperties": false,
@@ -88,10 +91,19 @@
88
91
  },
89
92
  "useHttp": {
90
93
  "type": "boolean"
94
+ },
95
+ "workers": {
96
+ "type": "number",
97
+ "minimum": 1
91
98
  }
92
99
  }
93
100
  }
94
101
  },
102
+ "workers": {
103
+ "type": "number",
104
+ "minimum": 1,
105
+ "default": 1
106
+ },
95
107
  "web": {
96
108
  "type": "array",
97
109
  "items": {
@@ -126,6 +138,10 @@
126
138
  },
127
139
  "useHttp": {
128
140
  "type": "boolean"
141
+ },
142
+ "workers": {
143
+ "type": "number",
144
+ "minimum": 1
129
145
  }
130
146
  }
131
147
  }