@platformatic/runtime 3.34.1-alpha.3 → 3.35.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/lib/errors.js CHANGED
@@ -40,6 +40,10 @@ export const ApplicationStartTimeoutError = createError(
40
40
  `${ERROR_PREFIX}_APPLICATION_START_TIMEOUT`,
41
41
  "Application with id '%s' failed to start in %dms."
42
42
  )
43
+ export const ApplicationsDependenciesCycleError = createError(
44
+ `${ERROR_PREFIX}_APPLICATIONS_DEPENDENCIES_CYCLE`,
45
+ 'Detected a cycle in the applications dependencies: %s'
46
+ )
43
47
  export const FailedToRetrieveOpenAPISchemaError = createError(
44
48
  `${ERROR_PREFIX}_FAILED_TO_RETRIEVE_OPENAPI_SCHEMA`,
45
49
  'Failed to retrieve OpenAPI schema for application with id "%s": %s'
package/lib/runtime.js CHANGED
@@ -46,6 +46,7 @@ import { createChannelCreationHook } from './policies.js'
46
46
  import { startPrometheusServer } from './prom-server.js'
47
47
  import { startScheduler } from './scheduler.js'
48
48
  import { createSharedStore } from './shared-http-cache.js'
49
+ import { topologicalSort } from './utils.js'
49
50
  import { version } from './version.js'
50
51
  import { DynamicWorkersScaler } from './worker-scaler.js'
51
52
  import { HealthSignalsQueue } from './worker/health-signals.js'
@@ -90,6 +91,7 @@ function parseOrigins (origins) {
90
91
  })
91
92
  }
92
93
 
94
+ // Always run operations in parallel to avoid deadlocks when services have dependencies
93
95
  const MAX_CONCURRENCY = availableParallelism()
94
96
  const MAX_BOOTSTRAP_ATTEMPTS = 5
95
97
  const IMMEDIATE_RESTART_MAX_THRESHOLD = 10
@@ -157,7 +159,9 @@ export class Runtime extends EventEmitter {
157
159
  this.#meshInterceptor = createThreadInterceptor({
158
160
  domain: '.plt.local',
159
161
  timeout: this.#config.applicationTimeout,
160
- onChannelCreation: this.#channelCreationHook
162
+ meshTimeout: this.#context.meshTimeout ?? true,
163
+ onChannelCreation: this.#channelCreationHook,
164
+ onError: this.#onMeshInterceptorError.bind(this)
161
165
  })
162
166
  this.logger = abstractLogger // This is replaced by the real logger in init() and eventually removed in close()
163
167
  this.#status = undefined
@@ -543,9 +547,21 @@ export class Runtime extends EventEmitter {
543
547
  return removed
544
548
  }
545
549
 
546
- async startApplications (applicationsToStart, silent = false) {
550
+ async startApplications (applications, silent = false) {
551
+ // For each worker, get its dependencies from the first worker
552
+ const dependencies = new Map()
553
+ for (const applicationId of applications) {
554
+ const worker = await this.#getWorkerByIdOrNext(applicationId, 0)
555
+
556
+ dependencies.set(applicationId, await sendViaITC(worker, 'getDependencies'))
557
+ }
558
+
559
+ // Now, topological sort the applications based on their dependencies.
560
+ // If circular dependencies are detected, an error with proper error code is thrown.
561
+ applications = topologicalSort(dependencies)
562
+
547
563
  const startInvocations = []
548
- for (const application of applicationsToStart) {
564
+ for (const application of applications) {
549
565
  startInvocations.push([application, silent])
550
566
  }
551
567
 
@@ -1835,7 +1851,16 @@ export class Runtime extends EventEmitter {
1835
1851
  worker[kInterceptorReadyPromise] = this.#meshInterceptor.route(applicationId, worker)
1836
1852
 
1837
1853
  // Wait for initialization
1838
- await waitEventFromITC(worker, 'init')
1854
+ try {
1855
+ await waitEventFromITC(worker, 'init')
1856
+ } catch (e) {
1857
+ if (e.code !== 'PLT_RUNTIME_APPLICATION_WORKER_EXIT') {
1858
+ this.logger.error({ err: ensureLoggableError(e) }, `Failed to initialize the ${errorLabel}. Replacing it ...`)
1859
+ }
1860
+
1861
+ this.#workers.delete(workerId)
1862
+ return this.#setupWorker(config, applicationConfig, workersCount, applicationId, index, enabled)
1863
+ }
1839
1864
 
1840
1865
  if (applicationConfig.entrypoint) {
1841
1866
  this.#entrypointId = applicationId
@@ -2988,4 +3013,16 @@ export class Runtime extends EventEmitter {
2988
3013
 
2989
3014
  this.#loggerContext.updatePrefixes(ids)
2990
3015
  }
3016
+
3017
+ #onMeshInterceptorError (error) {
3018
+ const worker = error.port
3019
+
3020
+ this.logger.error(
3021
+ { err: ensureLoggableError(error.cause) },
3022
+ `The ${this.#workerExtendedLabel(worker[kApplicationId], worker[kWorkerId])} threw an error during mesh network setup. Replacing it ...`
3023
+ )
3024
+
3025
+ this.emit('application:worker:init:failed', { application: worker[kApplicationId], worker: worker[kWorkerId] })
3026
+ worker.terminate()
3027
+ }
2991
3028
  }
package/lib/utils.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto'
2
2
  import { tmpdir } from 'node:os'
3
3
  import { join } from 'node:path'
4
+ import { ApplicationsDependenciesCycleError } from './errors.js'
4
5
 
5
6
  export function getArrayDifference (a, b) {
6
7
  return a.filter(element => {
@@ -17,3 +18,35 @@ export function getRuntimeTmpDir (runtimeDir) {
17
18
  const runtimeDirHash = createHash('md5').update(runtimeDir).digest('hex')
18
19
  return join(platformaticTmpDir, runtimeDirHash)
19
20
  }
21
+
22
+ // Graph: Map<string, string[]>
23
+ export function topologicalSort (graph) {
24
+ const result = []
25
+ const visited = new Set()
26
+ const path = []
27
+
28
+ function visit (node) {
29
+ if (visited.has(node)) {
30
+ return
31
+ }
32
+
33
+ if (path.includes(node)) {
34
+ throw new ApplicationsDependenciesCycleError(path.concat([node]).join(' -> '))
35
+ }
36
+
37
+ path.push(node)
38
+ for (const dep of graph.get(node)) {
39
+ visit(dep)
40
+ }
41
+ path.pop()
42
+
43
+ visited.add(node)
44
+ result.push(node)
45
+ }
46
+
47
+ for (const node of graph.keys()) {
48
+ visit(node)
49
+ }
50
+
51
+ return result
52
+ }
package/lib/worker/itc.js CHANGED
@@ -2,8 +2,8 @@ import { ensureLoggableError, executeInParallel, executeWithTimeout, kTimeout }
2
2
  import { ITC } from '@platformatic/itc'
3
3
  import { Unpromise } from '@watchable/unpromise'
4
4
  import { once } from 'node:events'
5
- import { Duplex } from 'node:stream'
6
5
  import { createRequire } from 'node:module'
6
+ import { Duplex } from 'node:stream'
7
7
  import { parentPort, workerData } from 'node:worker_threads'
8
8
  import {
9
9
  ApplicationExitedError,
@@ -49,7 +49,7 @@ function startSubprocessRepl (port, childManager, clientWs, controller) {
49
49
  childManager.on('repl:exit', handleReplExit)
50
50
 
51
51
  // Forward input from MessagePort to child process
52
- port.on('message', (message) => {
52
+ port.on('message', message => {
53
53
  if (message.type === 'input') {
54
54
  childManager.send(clientWs, 'replInput', { data: message.data }).catch(() => {
55
55
  // Ignore errors if the child process has exited
@@ -212,6 +212,10 @@ export function setupITC (controller, application, dispatcher, sharedContext) {
212
212
  })
213
213
  },
214
214
 
215
+ async getDependencies () {
216
+ return controller.capability.getDependencies()
217
+ },
218
+
215
219
  async build () {
216
220
  return controller.capability.build()
217
221
  },
@@ -357,13 +361,13 @@ export function setupITC (controller, application, dispatcher, sharedContext) {
357
361
  // Create a duplex stream that wraps the MessagePort
358
362
  const replStream = new Duplex({
359
363
  read () {},
360
- write (chunk, encoding, callback) {
364
+ write (chunk, _, callback) {
361
365
  port.postMessage({ type: 'output', data: chunk.toString() })
362
366
  callback()
363
367
  }
364
368
  })
365
369
 
366
- port.on('message', (message) => {
370
+ port.on('message', message => {
367
371
  if (message.type === 'input') {
368
372
  replStream.push(message.data)
369
373
  } else if (message.type === 'close') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "3.34.1-alpha.3",
3
+ "version": "3.35.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/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"
38
+ "@platformatic/composer": "3.35.0",
39
+ "@platformatic/db": "3.35.0",
40
+ "@platformatic/gateway": "3.35.0",
41
+ "@platformatic/node": "3.35.0",
42
+ "@platformatic/service": "3.35.0",
43
+ "@platformatic/sql-graphql": "3.35.0",
44
+ "@platformatic/sql-mapper": "3.35.0",
45
+ "@platformatic/wattpm-pprof-capture": "3.35.0"
46
46
  },
47
47
  "dependencies": {
48
48
  "@fastify/accepts": "^5.0.0",
@@ -69,14 +69,14 @@
69
69
  "sonic-boom": "^4.2.0",
70
70
  "systeminformation": "^5.27.11",
71
71
  "undici": "^7.0.0",
72
- "undici-thread-interceptor": "^1.0.0",
72
+ "undici-thread-interceptor": "^1.3.0",
73
73
  "ws": "^8.16.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"
74
+ "@platformatic/basic": "3.35.0",
75
+ "@platformatic/foundation": "3.35.0",
76
+ "@platformatic/generators": "3.35.0",
77
+ "@platformatic/itc": "3.35.0",
78
+ "@platformatic/telemetry": "3.35.0",
79
+ "@platformatic/metrics": "3.35.0"
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.34.1-alpha.3.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.35.0.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "title": "Platformatic Runtime Config",
5
5
  "type": "object",