@platformatic/runtime 3.33.0 → 3.34.1-alpha.3

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
@@ -213,6 +213,10 @@ export type PlatformaticRuntimeConfig = {
213
213
  gracefulShutdown?: {
214
214
  runtime: number | string;
215
215
  application: number | string;
216
+ /**
217
+ * Add Connection: close header to HTTP responses during graceful shutdown
218
+ */
219
+ closeConnections?: boolean;
216
220
  };
217
221
  health?: {
218
222
  enabled?: boolean | string;
@@ -376,6 +380,23 @@ export type PlatformaticRuntimeConfig = {
376
380
  */
377
381
  serviceVersion?: string;
378
382
  };
383
+ /**
384
+ * Custom labels to add to HTTP metrics (http_request_all_duration_seconds). Each label extracts its value from an HTTP request header.
385
+ */
386
+ httpCustomLabels?: {
387
+ /**
388
+ * The label name to use in metrics
389
+ */
390
+ name: string;
391
+ /**
392
+ * The HTTP request header to extract the value from
393
+ */
394
+ header: string;
395
+ /**
396
+ * Default value when header is missing (defaults to "unknown")
397
+ */
398
+ default?: string;
399
+ }[];
379
400
  };
380
401
  telemetry?: {
381
402
  enabled?: boolean | string;
package/lib/errors.js CHANGED
@@ -148,3 +148,7 @@ export const WorkerInterceptorNotReadyError = createError(
148
148
  `${ERROR_PREFIX}_WORKER_INTERCEPTOR_NOT_READY`,
149
149
  'The "%s" application worker interceptor is not ready'
150
150
  )
151
+ export const WorkerInterceptorJoinTimeoutError = createError(
152
+ `${ERROR_PREFIX}_WORKER_INTERCEPTOR_JOIN_TIMEOUT`,
153
+ 'The %s failed to join the mesh network in %dms.'
154
+ )
package/lib/logger.js CHANGED
@@ -139,7 +139,7 @@ function createPrettifier (context) {
139
139
  const current = context.current
140
140
  const level = current.caller ? current.caller : labelColorized.replace(label, label.padStart(6, ' '))
141
141
 
142
- return `${current.prefix} | \u001B[0m ${level}`
142
+ return `${current.prefix} |\u001B[0m${level}`
143
143
  }
144
144
  }
145
145
  })
package/lib/runtime.js CHANGED
@@ -10,13 +10,14 @@ import {
10
10
  parseMemorySize
11
11
  } from '@platformatic/foundation'
12
12
  import { ITC } from '@platformatic/itc'
13
- import { client as metricsClient, collectProcessMetrics } from '@platformatic/metrics'
13
+ import { collectProcessMetrics, client as metricsClient } from '@platformatic/metrics'
14
14
  import fastify from 'fastify'
15
15
  import { EventEmitter, once } from 'node:events'
16
16
  import { existsSync } from 'node:fs'
17
17
  import { readFile } from 'node:fs/promises'
18
18
  import { STATUS_CODES } from 'node:http'
19
19
  import { createRequire } from 'node:module'
20
+ import { availableParallelism } from 'node:os'
20
21
  import { dirname, isAbsolute, join } from 'node:path'
21
22
  import { setImmediate as immediate, setTimeout as sleep } from 'node:timers/promises'
22
23
  import { pathToFileURL } from 'node:url'
@@ -36,8 +37,8 @@ import {
36
37
  MissingEntrypointError,
37
38
  MissingPprofCapture,
38
39
  RuntimeAbortedError,
39
- WorkerNotFoundError,
40
- WorkerInterceptorNotReadyError
40
+ WorkerInterceptorJoinTimeoutError,
41
+ WorkerNotFoundError
41
42
  } from './errors.js'
42
43
  import { abstractLogger, createLogger } from './logger.js'
43
44
  import { startManagementApi } from './management-api.js'
@@ -56,6 +57,7 @@ import {
56
57
  kFullId,
57
58
  kHealthCheckTimer,
58
59
  kId,
60
+ kInterceptorReadyPromise,
59
61
  kITC,
60
62
  kLastHealthCheckELU,
61
63
  kStderrMarker,
@@ -63,8 +65,7 @@ import {
63
65
  kWorkerId,
64
66
  kWorkersBroadcast,
65
67
  kWorkerStartTime,
66
- kWorkerStatus,
67
- kInterceptorReadyPromise
68
+ kWorkerStatus
68
69
  } from './worker/symbols.js'
69
70
 
70
71
  const kWorkerFile = join(import.meta.dirname, 'worker/main.js')
@@ -89,7 +90,7 @@ function parseOrigins (origins) {
89
90
  })
90
91
  }
91
92
 
92
- const MAX_CONCURRENCY = 5
93
+ const MAX_CONCURRENCY = availableParallelism()
93
94
  const MAX_BOOTSTRAP_ATTEMPTS = 5
94
95
  const IMMEDIATE_RESTART_MAX_THRESHOLD = 10
95
96
  const MAX_WORKERS = 100
@@ -497,7 +498,7 @@ export class Runtime extends EventEmitter {
497
498
  await executeInParallel(this.#setupApplication.bind(this), setupInvocations, this.#concurrency)
498
499
 
499
500
  for (const application of applications) {
500
- this.logger.info(`Added application "${application.id}"${application.entrypoint ? ' (entrypoint)' : ''}.`)
501
+ this.logger.debug(`Added application "${application.id}"${application.entrypoint ? ' (entrypoint)' : ''}.`)
501
502
  this.emitAndNotify('application:added', application)
502
503
  }
503
504
 
@@ -534,7 +535,7 @@ export class Runtime extends EventEmitter {
534
535
  }
535
536
 
536
537
  for (const application of applications) {
537
- this.logger.info(`Removed application "${application}".`)
538
+ this.logger.warn(`Removed application "${application}".`)
538
539
  this.emitAndNotify('application:removed', application)
539
540
  }
540
541
 
@@ -2058,7 +2059,7 @@ export class Runtime extends EventEmitter {
2058
2059
 
2059
2060
  if (workerUrl === kTimeout) {
2060
2061
  this.emitAndNotify('application:worker:startTimeout', eventPayload)
2061
- this.logger.info(`The ${label} failed to start in ${config.startTimeout}ms. Forcefully killing the thread.`)
2062
+ this.logger.error(`The ${label} failed to start in ${config.startTimeout}ms. Forcefully killing the thread.`)
2062
2063
  worker.terminate()
2063
2064
  throw new ApplicationStartTimeoutError(id, config.startTimeout)
2064
2065
  }
@@ -2072,7 +2073,11 @@ export class Runtime extends EventEmitter {
2072
2073
  this.#url = workerUrl
2073
2074
  }
2074
2075
 
2075
- await this.#waitForWorkerInterceptor(worker)
2076
+ // Wait for the interceptor to be ready
2077
+ const interceptorResult = await executeWithTimeout(worker[kInterceptorReadyPromise], config.startTimeout)
2078
+ if (interceptorResult === kTimeout) {
2079
+ throw new WorkerInterceptorJoinTimeoutError(label, config.startTimeout)
2080
+ }
2076
2081
 
2077
2082
  worker[kWorkerStatus] = 'started'
2078
2083
  worker[kWorkerStartTime] = Date.now()
@@ -2983,24 +2988,4 @@ export class Runtime extends EventEmitter {
2983
2988
 
2984
2989
  this.#loggerContext.updatePrefixes(ids)
2985
2990
  }
2986
-
2987
- async #waitForWorkerInterceptor (worker, timeout = 10000) {
2988
- const workerId = worker[kId]
2989
- const applicationId = worker[kApplicationId]
2990
-
2991
- const interceptorReadyTimeout = setTimeout(() => {
2992
- this.logger.error(
2993
- { applicationId, workerId },
2994
- 'The worker interceptor is not ready after 10s'
2995
- )
2996
- throw new WorkerInterceptorNotReadyError(applicationId)
2997
- }, timeout)
2998
-
2999
- try {
3000
- await worker[kInterceptorReadyPromise]
3001
- worker[kInterceptorReadyPromise] = null
3002
- } finally {
3003
- clearTimeout(interceptorReadyTimeout)
3004
- }
3005
- }
3006
2991
  }
package/lib/worker/itc.js CHANGED
@@ -20,6 +20,58 @@ import { updateUndiciInterceptors } from './interceptors.js'
20
20
  import { MessagingITC } from './messaging.js'
21
21
  import { kApplicationId, kITC, kId, kWorkerId } from './symbols.js'
22
22
 
23
+ function startSubprocessRepl (port, childManager, clientWs, controller) {
24
+ // Start the REPL in the child process
25
+ childManager.send(clientWs, 'startRepl').catch(err => {
26
+ port.postMessage({ type: 'output', data: `Error starting REPL: ${err.message}\n` })
27
+ port.postMessage({ type: 'exit' })
28
+ port.close()
29
+ })
30
+
31
+ // Listen for repl:output notifications from the child process
32
+ function handleReplOutput ({ data }) {
33
+ port.postMessage({ type: 'output', data })
34
+ }
35
+
36
+ // Listen for repl:exit notifications from the child process
37
+ function handleReplExit () {
38
+ cleanup()
39
+ port.postMessage({ type: 'exit' })
40
+ port.close()
41
+ }
42
+
43
+ function cleanup () {
44
+ childManager.removeListener('repl:output', handleReplOutput)
45
+ childManager.removeListener('repl:exit', handleReplExit)
46
+ }
47
+
48
+ childManager.on('repl:output', handleReplOutput)
49
+ childManager.on('repl:exit', handleReplExit)
50
+
51
+ // Forward input from MessagePort to child process
52
+ port.on('message', (message) => {
53
+ if (message.type === 'input') {
54
+ childManager.send(clientWs, 'replInput', { data: message.data }).catch(() => {
55
+ // Ignore errors if the child process has exited
56
+ })
57
+ } else if (message.type === 'close') {
58
+ childManager.send(clientWs, 'replClose').catch(() => {
59
+ // Ignore errors if the child process has exited
60
+ })
61
+ cleanup()
62
+ }
63
+ })
64
+
65
+ port.on('close', () => {
66
+ childManager.send(clientWs, 'replClose').catch(() => {
67
+ // Ignore errors if the child process has exited
68
+ })
69
+ cleanup()
70
+ })
71
+
72
+ return { started: true }
73
+ }
74
+
23
75
  async function safeHandleInITC (worker, fn) {
24
76
  try {
25
77
  // Make sure to catch when the worker exits, otherwise we're stuck forever
@@ -241,6 +293,19 @@ export function setupITC (controller, application, dispatcher, sharedContext) {
241
293
  },
242
294
 
243
295
  async getHealth () {
296
+ // Check if running in subprocess mode - forward through ChildManager
297
+ const childManager = controller.capability?.getChildManager?.()
298
+ const clientWs = controller.capability?.clientWs
299
+
300
+ if (childManager && clientWs) {
301
+ try {
302
+ return await childManager.send(clientWs, 'getHealth')
303
+ } catch (err) {
304
+ throw new FailedToRetrieveHealthError(application.id, err.message)
305
+ }
306
+ }
307
+
308
+ // Existing thread implementation
244
309
  try {
245
310
  return await controller.getHealth()
246
311
  } catch (err) {
@@ -273,6 +338,14 @@ export function setupITC (controller, application, dispatcher, sharedContext) {
273
338
  },
274
339
 
275
340
  startRepl (port) {
341
+ // Check if running in subprocess mode - forward through ChildManager
342
+ const childManager = controller.capability?.getChildManager?.()
343
+ const clientWs = controller.capability?.clientWs
344
+
345
+ if (childManager && clientWs) {
346
+ return startSubprocessRepl(port, childManager, clientWs, controller)
347
+ }
348
+
276
349
  // We are loading the repl module dynamically here to avoid loading it
277
350
  // when not needed (since it pulls in domain, which is quite expensive
278
351
  // as it monkey patches EventEmitter).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "3.33.0",
3
+ "version": "3.34.1-alpha.3",
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.33.0",
39
- "@platformatic/gateway": "3.33.0",
40
- "@platformatic/composer": "3.33.0",
41
- "@platformatic/node": "3.33.0",
42
- "@platformatic/service": "3.33.0",
43
- "@platformatic/sql-mapper": "3.33.0",
44
- "@platformatic/sql-graphql": "3.33.0",
45
- "@platformatic/wattpm-pprof-capture": "3.33.0"
38
+ "@platformatic/composer": "3.34.1-alpha.3",
39
+ "@platformatic/db": "3.34.1-alpha.3",
40
+ "@platformatic/node": "3.34.1-alpha.3",
41
+ "@platformatic/service": "3.34.1-alpha.3",
42
+ "@platformatic/gateway": "3.34.1-alpha.3",
43
+ "@platformatic/sql-graphql": "3.34.1-alpha.3",
44
+ "@platformatic/sql-mapper": "3.34.1-alpha.3",
45
+ "@platformatic/wattpm-pprof-capture": "3.34.1-alpha.3"
46
46
  },
47
47
  "dependencies": {
48
48
  "@fastify/accepts": "^5.0.0",
@@ -71,12 +71,12 @@
71
71
  "undici": "^7.0.0",
72
72
  "undici-thread-interceptor": "^1.0.0",
73
73
  "ws": "^8.16.0",
74
- "@platformatic/basic": "3.33.0",
75
- "@platformatic/foundation": "3.33.0",
76
- "@platformatic/generators": "3.33.0",
77
- "@platformatic/itc": "3.33.0",
78
- "@platformatic/metrics": "3.33.0",
79
- "@platformatic/telemetry": "3.33.0"
74
+ "@platformatic/basic": "3.34.1-alpha.3",
75
+ "@platformatic/foundation": "3.34.1-alpha.3",
76
+ "@platformatic/itc": "3.34.1-alpha.3",
77
+ "@platformatic/generators": "3.34.1-alpha.3",
78
+ "@platformatic/metrics": "3.34.1-alpha.3",
79
+ "@platformatic/telemetry": "3.34.1-alpha.3"
80
80
  },
81
81
  "engines": {
82
82
  "node": ">=22.19.0"
package/schema.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.33.0.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.34.1-alpha.3.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "title": "Platformatic Runtime Config",
5
5
  "type": "object",
@@ -1757,6 +1757,11 @@
1757
1757
  }
1758
1758
  ],
1759
1759
  "default": 10000
1760
+ },
1761
+ "closeConnections": {
1762
+ "type": "boolean",
1763
+ "default": true,
1764
+ "description": "Add Connection: close header to HTTP responses during graceful shutdown"
1760
1765
  }
1761
1766
  },
1762
1767
  "default": {},
@@ -2297,6 +2302,32 @@
2297
2302
  "endpoint"
2298
2303
  ],
2299
2304
  "additionalProperties": false
2305
+ },
2306
+ "httpCustomLabels": {
2307
+ "type": "array",
2308
+ "description": "Custom labels to add to HTTP metrics (http_request_all_duration_seconds). Each label extracts its value from an HTTP request header.",
2309
+ "items": {
2310
+ "type": "object",
2311
+ "properties": {
2312
+ "name": {
2313
+ "type": "string",
2314
+ "description": "The label name to use in metrics"
2315
+ },
2316
+ "header": {
2317
+ "type": "string",
2318
+ "description": "The HTTP request header to extract the value from"
2319
+ },
2320
+ "default": {
2321
+ "type": "string",
2322
+ "description": "Default value when header is missing (defaults to \"unknown\")"
2323
+ }
2324
+ },
2325
+ "required": [
2326
+ "name",
2327
+ "header"
2328
+ ],
2329
+ "additionalProperties": false
2330
+ }
2300
2331
  }
2301
2332
  },
2302
2333
  "additionalProperties": false