@platformatic/runtime 3.0.0-alpha.6 → 3.0.0-alpha.8
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 +1 -0
- package/lib/config.js +81 -2
- package/lib/management-api.js +57 -26
- package/lib/runtime.js +124 -93
- package/lib/worker/controller.js +23 -8
- package/lib/worker/itc.js +38 -32
- package/lib/worker/main.js +7 -9
- package/package.json +17 -16
- package/schema.json +28 -1
- package/lib/dependencies.js +0 -63
package/config.d.ts
CHANGED
package/lib/config.js
CHANGED
|
@@ -16,12 +16,59 @@ import {
|
|
|
16
16
|
InspectAndInspectBrkError,
|
|
17
17
|
InspectorHostError,
|
|
18
18
|
InspectorPortError,
|
|
19
|
+
InvalidArgumentError,
|
|
19
20
|
InvalidEntrypointError,
|
|
20
21
|
MissingEntrypointError
|
|
21
22
|
} from './errors.js'
|
|
22
23
|
import { schema } from './schema.js'
|
|
23
24
|
import { upgrade } from './upgrade.js'
|
|
24
25
|
|
|
26
|
+
// Validate and coerce workers values early to avoid runtime hangs when invalid
|
|
27
|
+
function coercePositiveInteger (value) {
|
|
28
|
+
if (typeof value === 'number') {
|
|
29
|
+
if (!Number.isInteger(value) || value < 1) return null
|
|
30
|
+
return value
|
|
31
|
+
}
|
|
32
|
+
if (typeof value === 'string') {
|
|
33
|
+
// Trim to handle accidental spaces
|
|
34
|
+
const trimmed = value.trim()
|
|
35
|
+
if (trimmed.length === 0) return null
|
|
36
|
+
const num = Number(trimmed)
|
|
37
|
+
if (!Number.isFinite(num) || !Number.isInteger(num) || num < 1) return null
|
|
38
|
+
return num
|
|
39
|
+
}
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function raiseInvalidWorkersError (location, received, hint) {
|
|
44
|
+
const extra = hint ? ` (${hint})` : ''
|
|
45
|
+
throw new InvalidArgumentError(`${location} workers must be a positive integer; received "${received}"${extra}`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function autoDetectPprofCapture (config) {
|
|
49
|
+
const require = createRequire(import.meta.url)
|
|
50
|
+
|
|
51
|
+
let pprofCapturePath
|
|
52
|
+
try {
|
|
53
|
+
pprofCapturePath = require.resolve('@platformatic/wattpm-pprof-capture')
|
|
54
|
+
} catch (err) {
|
|
55
|
+
// No-op
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Add to preload if not already present
|
|
59
|
+
if (!config.preload) {
|
|
60
|
+
config.preload = []
|
|
61
|
+
} else if (typeof config.preload === 'string') {
|
|
62
|
+
config.preload = [config.preload]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (pprofCapturePath && !config.preload.includes(pprofCapturePath)) {
|
|
66
|
+
config.preload.push(pprofCapturePath)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return config
|
|
70
|
+
}
|
|
71
|
+
|
|
25
72
|
export async function wrapInRuntimeConfig (config, context) {
|
|
26
73
|
let applicationId = 'main'
|
|
27
74
|
try {
|
|
@@ -145,7 +192,14 @@ export async function transform (config, _, context) {
|
|
|
145
192
|
config = join(entryPath, configFilename)
|
|
146
193
|
}
|
|
147
194
|
|
|
148
|
-
const application = {
|
|
195
|
+
const application = {
|
|
196
|
+
id,
|
|
197
|
+
config,
|
|
198
|
+
path: entryPath,
|
|
199
|
+
useHttp: !!mapping.useHttp,
|
|
200
|
+
health: mapping.health,
|
|
201
|
+
dependencies: mapping.dependencies
|
|
202
|
+
}
|
|
149
203
|
const existingApplicationId = applications.findIndex(application => application.id === id)
|
|
150
204
|
|
|
151
205
|
if (existingApplicationId !== -1) {
|
|
@@ -161,6 +215,17 @@ export async function transform (config, _, context) {
|
|
|
161
215
|
|
|
162
216
|
let hasValidEntrypoint = false
|
|
163
217
|
|
|
218
|
+
// Root-level workers
|
|
219
|
+
if (typeof config.workers !== 'undefined') {
|
|
220
|
+
const coerced = coercePositiveInteger(config.workers)
|
|
221
|
+
if (coerced === null) {
|
|
222
|
+
const raw = config.workers
|
|
223
|
+
const hint = typeof raw === 'string' && /\{.*\}/.test(raw) ? 'check your environment variable' : ''
|
|
224
|
+
raiseInvalidWorkersError('Runtime', config.workers, hint)
|
|
225
|
+
}
|
|
226
|
+
config.workers = coerced
|
|
227
|
+
}
|
|
228
|
+
|
|
164
229
|
for (let i = 0; i < applications.length; ++i) {
|
|
165
230
|
const application = applications[i]
|
|
166
231
|
|
|
@@ -207,8 +272,19 @@ export async function transform (config, _, context) {
|
|
|
207
272
|
application.type = 'unknown'
|
|
208
273
|
}
|
|
209
274
|
|
|
275
|
+
// Validate and coerce per-service workers
|
|
276
|
+
if (typeof application.workers !== 'undefined') {
|
|
277
|
+
const coerced = coercePositiveInteger(application.workers)
|
|
278
|
+
if (coerced === null) {
|
|
279
|
+
const raw = config.application?.[i]?.workers
|
|
280
|
+
const hint = typeof raw === 'string' && /\{.*\}/.test(raw) ? 'check your environment variable' : ''
|
|
281
|
+
raiseInvalidWorkersError(`Service "${application.id}"`, application.workers, hint)
|
|
282
|
+
}
|
|
283
|
+
application.workers = coerced
|
|
284
|
+
}
|
|
285
|
+
|
|
210
286
|
application.entrypoint = application.id === config.entrypoint
|
|
211
|
-
application.dependencies
|
|
287
|
+
application.dependencies ??= []
|
|
212
288
|
application.localUrl = `http://${application.id}.plt.local`
|
|
213
289
|
|
|
214
290
|
if (typeof application.watch === 'undefined') {
|
|
@@ -277,5 +353,8 @@ export async function transform (config, _, context) {
|
|
|
277
353
|
}
|
|
278
354
|
}
|
|
279
355
|
|
|
356
|
+
// Auto-detect and add pprof capture if available
|
|
357
|
+
autoDetectPprofCapture(config)
|
|
358
|
+
|
|
280
359
|
return config
|
|
281
360
|
}
|
package/lib/management-api.js
CHANGED
|
@@ -4,6 +4,7 @@ import { createDirectory, safeRemove } from '@platformatic/foundation'
|
|
|
4
4
|
import fastify from 'fastify'
|
|
5
5
|
import { platform, tmpdir } from 'node:os'
|
|
6
6
|
import { join } from 'node:path'
|
|
7
|
+
import { setTimeout as sleep } from 'node:timers/promises'
|
|
7
8
|
import { createWebSocketStream } from 'ws'
|
|
8
9
|
|
|
9
10
|
const PLATFORMATIC_TMP_DIR = join(tmpdir(), 'platformatic', 'runtimes')
|
|
@@ -111,6 +112,23 @@ export async function managementApiPlugin (app, opts) {
|
|
|
111
112
|
reply.code(res.statusCode).headers(res.headers).send(res.body)
|
|
112
113
|
})
|
|
113
114
|
|
|
115
|
+
app.post('/applications/:id/pprof/start', async (request, reply) => {
|
|
116
|
+
const { id } = request.params
|
|
117
|
+
app.log.debug('start profiling', { id })
|
|
118
|
+
|
|
119
|
+
const options = request.body || {}
|
|
120
|
+
await runtime.startApplicationProfiling(id, options)
|
|
121
|
+
reply.code(200).send({})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
app.post('/applications/:id/pprof/stop', async (request, reply) => {
|
|
125
|
+
const { id } = request.params
|
|
126
|
+
app.log.debug('stop profiling', { id })
|
|
127
|
+
|
|
128
|
+
const profileData = await runtime.stopApplicationProfiling(id)
|
|
129
|
+
reply.type('application/octet-stream').code(200).send(profileData)
|
|
130
|
+
})
|
|
131
|
+
|
|
114
132
|
app.get('/metrics', { logLevel: 'debug' }, async (req, reply) => {
|
|
115
133
|
const accepts = req.accepts()
|
|
116
134
|
|
|
@@ -152,40 +170,53 @@ export async function managementApiPlugin (app, opts) {
|
|
|
152
170
|
})
|
|
153
171
|
}
|
|
154
172
|
|
|
155
|
-
export async function startManagementApi (runtime
|
|
173
|
+
export async function startManagementApi (runtime) {
|
|
156
174
|
const runtimePID = process.pid
|
|
157
175
|
|
|
158
|
-
|
|
159
|
-
|
|
176
|
+
const runtimePIDDir = join(PLATFORMATIC_TMP_DIR, runtimePID.toString())
|
|
177
|
+
if (platform() !== 'win32') {
|
|
178
|
+
await createDirectory(runtimePIDDir, true)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let socketPath = null
|
|
182
|
+
if (platform() === 'win32') {
|
|
183
|
+
socketPath = '\\\\.\\pipe\\platformatic-' + runtimePID.toString()
|
|
184
|
+
} else {
|
|
185
|
+
socketPath = join(runtimePIDDir, 'socket')
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const managementApi = fastify()
|
|
189
|
+
managementApi.register(fastifyWebsocket)
|
|
190
|
+
managementApi.register(managementApiPlugin, { runtime, prefix: '/api/v1' })
|
|
191
|
+
|
|
192
|
+
managementApi.addHook('onClose', async () => {
|
|
160
193
|
if (platform() !== 'win32') {
|
|
161
|
-
await
|
|
194
|
+
await safeRemove(runtimePIDDir)
|
|
162
195
|
}
|
|
196
|
+
})
|
|
163
197
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
socketPath = '\\\\.\\pipe\\platformatic-' + runtimePID.toString()
|
|
167
|
-
} else {
|
|
168
|
-
socketPath = join(runtimePIDDir, 'socket')
|
|
169
|
-
}
|
|
198
|
+
// When the runtime closes, close the management API as well
|
|
199
|
+
runtime.on('closed', managementApi.close.bind(managementApi))
|
|
170
200
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
201
|
+
/*
|
|
202
|
+
If runtime are started multiple times in a short
|
|
203
|
+
period of time (like in tests), there is a chance that the pipe is still in use
|
|
204
|
+
as the manament API server is closed after the runtime is closed (see event handler above).
|
|
174
205
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
206
|
+
Since it's a very rare case, we simply retry couple of times.
|
|
207
|
+
*/
|
|
208
|
+
for (let i = 0; i < 5; i++) {
|
|
209
|
+
try {
|
|
210
|
+
await managementApi.listen({ path: socketPath })
|
|
211
|
+
break
|
|
212
|
+
} catch (e) {
|
|
213
|
+
if (i === 5) {
|
|
214
|
+
throw e
|
|
178
215
|
}
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
// When the runtime closes, close the management API as well
|
|
182
|
-
runtime.on('closed', managementApi.close.bind(managementApi))
|
|
183
216
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
/* c8 ignore next 4 */
|
|
187
|
-
} catch (err) {
|
|
188
|
-
console.error(err)
|
|
189
|
-
process.exit(1)
|
|
217
|
+
await sleep(100)
|
|
218
|
+
}
|
|
190
219
|
}
|
|
220
|
+
|
|
221
|
+
return managementApi
|
|
191
222
|
}
|
package/lib/runtime.js
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
deepmerge,
|
|
3
3
|
ensureError,
|
|
4
4
|
ensureLoggableError,
|
|
5
|
+
executeInParallel,
|
|
5
6
|
executeWithTimeout,
|
|
6
7
|
features,
|
|
7
8
|
kMetadata,
|
|
@@ -22,7 +23,6 @@ import { Worker } from 'node:worker_threads'
|
|
|
22
23
|
import SonicBoom from 'sonic-boom'
|
|
23
24
|
import { Agent, request, interceptors as undiciInterceptors } from 'undici'
|
|
24
25
|
import { createThreadInterceptor } from 'undici-thread-interceptor'
|
|
25
|
-
import { checkDependencies, topologicalSort } from './dependencies.js'
|
|
26
26
|
import {
|
|
27
27
|
ApplicationAlreadyStartedError,
|
|
28
28
|
ApplicationNotFoundError,
|
|
@@ -59,18 +59,20 @@ import {
|
|
|
59
59
|
|
|
60
60
|
const kWorkerFile = join(import.meta.dirname, 'worker/main.js')
|
|
61
61
|
const kInspectorOptions = Symbol('plt.runtime.worker.inspectorOptions')
|
|
62
|
-
const kForwardEvents = Symbol('plt.runtime.worker.forwardEvents')
|
|
63
62
|
|
|
64
63
|
const MAX_LISTENERS_COUNT = 100
|
|
65
64
|
const MAX_METRICS_QUEUE_LENGTH = 5 * 60 // 5 minutes in seconds
|
|
66
65
|
const COLLECT_METRICS_TIMEOUT = 1000
|
|
67
66
|
|
|
67
|
+
const MAX_CONCURRENCY = 5
|
|
68
68
|
const MAX_BOOTSTRAP_ATTEMPTS = 5
|
|
69
69
|
const IMMEDIATE_RESTART_MAX_THRESHOLD = 10
|
|
70
70
|
const MAX_WORKERS = 100
|
|
71
71
|
|
|
72
72
|
export class Runtime extends EventEmitter {
|
|
73
73
|
logger
|
|
74
|
+
error
|
|
75
|
+
|
|
74
76
|
#loggerDestination
|
|
75
77
|
#stdio
|
|
76
78
|
|
|
@@ -81,6 +83,7 @@ export class Runtime extends EventEmitter {
|
|
|
81
83
|
#context
|
|
82
84
|
#sharedContext
|
|
83
85
|
#isProduction
|
|
86
|
+
#concurrency
|
|
84
87
|
#entrypointId
|
|
85
88
|
#url
|
|
86
89
|
|
|
@@ -112,6 +115,7 @@ export class Runtime extends EventEmitter {
|
|
|
112
115
|
this.#env = config[kMetadata].env
|
|
113
116
|
this.#context = context ?? {}
|
|
114
117
|
this.#isProduction = this.#context.isProduction ?? this.#context.production ?? false
|
|
118
|
+
this.#concurrency = this.#context.concurrency ?? MAX_CONCURRENCY
|
|
115
119
|
this.#workers = new RoundRobinMap()
|
|
116
120
|
this.#url = undefined
|
|
117
121
|
this.#meshInterceptor = createThreadInterceptor({ domain: '.plt.local', timeout: this.#config.applicationTimeout })
|
|
@@ -150,7 +154,6 @@ export class Runtime extends EventEmitter {
|
|
|
150
154
|
}
|
|
151
155
|
|
|
152
156
|
const config = this.#config
|
|
153
|
-
const autoloadEnabled = config.autoload
|
|
154
157
|
|
|
155
158
|
if (config.managementApi) {
|
|
156
159
|
this.#managementApi = await startManagementApi(this, this.#root)
|
|
@@ -190,61 +193,7 @@ export class Runtime extends EventEmitter {
|
|
|
190
193
|
this.#env['PLT_ENVIRONMENT'] = 'development'
|
|
191
194
|
}
|
|
192
195
|
|
|
193
|
-
|
|
194
|
-
for (const applicationConfig of config.applications) {
|
|
195
|
-
// If there is no application path, check if the application was resolved
|
|
196
|
-
if (!applicationConfig.path) {
|
|
197
|
-
if (applicationConfig.url) {
|
|
198
|
-
// Try to backfill the path for external applications
|
|
199
|
-
applicationConfig.path = join(this.#root, config.resolvedApplicationsBasePath, applicationConfig.id)
|
|
200
|
-
|
|
201
|
-
if (!existsSync(applicationConfig.path)) {
|
|
202
|
-
const executable = globalThis.platformatic?.executable ?? 'platformatic'
|
|
203
|
-
this.logger.error(
|
|
204
|
-
`The path for application "%s" does not exist. Please run "${executable} resolve" and try again.`,
|
|
205
|
-
applicationConfig.id
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
await this.closeAndThrow(new RuntimeAbortedError())
|
|
209
|
-
}
|
|
210
|
-
} else {
|
|
211
|
-
this.logger.error(
|
|
212
|
-
'The application "%s" has no path defined. Please check your configuration and try again.',
|
|
213
|
-
applicationConfig.id
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
await this.closeAndThrow(new RuntimeAbortedError())
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
await this.#setupApplication(applicationConfig)
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
try {
|
|
224
|
-
checkDependencies(config.applications)
|
|
225
|
-
|
|
226
|
-
// Make sure the list exists before computing the dependencies, otherwise some applications might not be stopped
|
|
227
|
-
if (autoloadEnabled) {
|
|
228
|
-
this.#workers = topologicalSort(this.#workers, config)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// When autoloading is disabled, add a warning if an application is defined before its dependencies
|
|
232
|
-
if (!autoloadEnabled) {
|
|
233
|
-
for (let i = 0; i < config.applications.length; i++) {
|
|
234
|
-
const current = config.applications[i]
|
|
235
|
-
|
|
236
|
-
for (const dep of current.dependencies ?? []) {
|
|
237
|
-
if (config.applications.findIndex(s => s.id === dep.id) > i) {
|
|
238
|
-
this.logger.warn(
|
|
239
|
-
`Application "${current.id}" depends on application "${dep.id}", but it is defined and it will be started before it. Please check your configuration file.`
|
|
240
|
-
)
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
} catch (e) {
|
|
246
|
-
await this.closeAndThrow(e)
|
|
247
|
-
}
|
|
196
|
+
await this.#setupApplications()
|
|
248
197
|
|
|
249
198
|
await this.#setDispatcher(config.undici)
|
|
250
199
|
|
|
@@ -266,12 +215,14 @@ export class Runtime extends EventEmitter {
|
|
|
266
215
|
this.#updateStatus('starting')
|
|
267
216
|
this.#createWorkersBroadcastChannel()
|
|
268
217
|
|
|
269
|
-
// Important: do not use Promise.all here since it won't properly manage dependencies
|
|
270
218
|
try {
|
|
219
|
+
const startInvocations = []
|
|
271
220
|
for (const application of this.getApplicationsIds()) {
|
|
272
|
-
|
|
221
|
+
startInvocations.push([application, silent])
|
|
273
222
|
}
|
|
274
223
|
|
|
224
|
+
await executeInParallel(this.startApplication.bind(this), startInvocations, this.#concurrency)
|
|
225
|
+
|
|
275
226
|
if (this.#config.inspectorOptions) {
|
|
276
227
|
const { port } = this.#config.inspectorOptions
|
|
277
228
|
|
|
@@ -340,16 +291,35 @@ export class Runtime extends EventEmitter {
|
|
|
340
291
|
await this.stopApplication(this.#entrypointId, silent)
|
|
341
292
|
}
|
|
342
293
|
|
|
343
|
-
|
|
344
|
-
|
|
294
|
+
const stopInvocations = []
|
|
295
|
+
|
|
296
|
+
const allApplications = await this.getApplications(true)
|
|
297
|
+
|
|
298
|
+
// Construct the reverse dependency graph
|
|
299
|
+
const dependents = {}
|
|
300
|
+
for (const application of allApplications.applications) {
|
|
301
|
+
for (const dependency of application.dependencies ?? []) {
|
|
302
|
+
let applicationDependents = dependents[dependency]
|
|
303
|
+
if (!applicationDependents) {
|
|
304
|
+
applicationDependents = new Set()
|
|
305
|
+
dependents[dependency] = applicationDependents
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
applicationDependents.add(application.id)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
for (const application of this.getApplicationsIds()) {
|
|
345
313
|
// The entrypoint has been stopped above
|
|
346
314
|
if (application === this.#entrypointId) {
|
|
347
315
|
continue
|
|
348
316
|
}
|
|
349
317
|
|
|
350
|
-
|
|
318
|
+
stopInvocations.push([application, silent, Array.from(dependents[application] ?? [])])
|
|
351
319
|
}
|
|
352
320
|
|
|
321
|
+
await executeInParallel(this.stopApplication.bind(this), stopInvocations, this.#concurrency)
|
|
322
|
+
|
|
353
323
|
await this.#meshInterceptor.close()
|
|
354
324
|
this.#workersBroadcastChannel?.close()
|
|
355
325
|
|
|
@@ -397,6 +367,7 @@ export class Runtime extends EventEmitter {
|
|
|
397
367
|
|
|
398
368
|
async closeAndThrow (error) {
|
|
399
369
|
this.#updateStatus('errored', error)
|
|
370
|
+
this.error = error
|
|
400
371
|
|
|
401
372
|
// Wait for the next tick so that any pending logging is properly flushed
|
|
402
373
|
await sleep(1)
|
|
@@ -452,9 +423,7 @@ export class Runtime extends EventEmitter {
|
|
|
452
423
|
|
|
453
424
|
emit (event, payload) {
|
|
454
425
|
for (const worker of this.#workers.values()) {
|
|
455
|
-
|
|
456
|
-
worker[kITC].notify('runtime:event', { event, payload })
|
|
457
|
-
}
|
|
426
|
+
worker[kITC].notify('runtime:event', { event, payload })
|
|
458
427
|
}
|
|
459
428
|
|
|
460
429
|
this.logger.trace({ event, payload }, 'Runtime event')
|
|
@@ -502,7 +471,7 @@ export class Runtime extends EventEmitter {
|
|
|
502
471
|
this.emit('application:started', id)
|
|
503
472
|
}
|
|
504
473
|
|
|
505
|
-
async stopApplication (id, silent = false) {
|
|
474
|
+
async stopApplication (id, silent = false, dependents = []) {
|
|
506
475
|
const config = this.#config
|
|
507
476
|
const applicationConfig = config.applications.find(s => s.id === id)
|
|
508
477
|
|
|
@@ -515,9 +484,12 @@ export class Runtime extends EventEmitter {
|
|
|
515
484
|
this.emit('application:stopping', id)
|
|
516
485
|
|
|
517
486
|
if (typeof workersCount === 'number') {
|
|
487
|
+
const stopInvocations = []
|
|
518
488
|
for (let i = 0; i < workersCount; i++) {
|
|
519
|
-
|
|
489
|
+
stopInvocations.push([workersCount, id, i, silent, undefined, dependents])
|
|
520
490
|
}
|
|
491
|
+
|
|
492
|
+
await executeInParallel(this.#stopWorker.bind(this), stopInvocations, this.#concurrency)
|
|
521
493
|
}
|
|
522
494
|
|
|
523
495
|
this.emit('application:stopped', id)
|
|
@@ -540,6 +512,18 @@ export class Runtime extends EventEmitter {
|
|
|
540
512
|
}
|
|
541
513
|
}
|
|
542
514
|
|
|
515
|
+
async startApplicationProfiling (id, options = {}, ensureStarted = true) {
|
|
516
|
+
const service = await this.#getApplicationById(id, ensureStarted)
|
|
517
|
+
|
|
518
|
+
return sendViaITC(service, 'startProfiling', options)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async stopApplicationProfiling (id, ensureStarted = true) {
|
|
522
|
+
const service = await this.#getApplicationById(id, ensureStarted)
|
|
523
|
+
|
|
524
|
+
return sendViaITC(service, 'stopProfiling')
|
|
525
|
+
}
|
|
526
|
+
|
|
543
527
|
async updateUndiciInterceptors (undiciConfig) {
|
|
544
528
|
this.#config.undici = undiciConfig
|
|
545
529
|
|
|
@@ -754,6 +738,10 @@ export class Runtime extends EventEmitter {
|
|
|
754
738
|
return report
|
|
755
739
|
}
|
|
756
740
|
|
|
741
|
+
setConcurrency (concurrency) {
|
|
742
|
+
this.#concurrency = concurrency
|
|
743
|
+
}
|
|
744
|
+
|
|
757
745
|
async getUrl () {
|
|
758
746
|
return this.#url
|
|
759
747
|
}
|
|
@@ -1014,11 +1002,13 @@ export class Runtime extends EventEmitter {
|
|
|
1014
1002
|
return this.#config.applications.map(application => application.id)
|
|
1015
1003
|
}
|
|
1016
1004
|
|
|
1017
|
-
async getApplications () {
|
|
1005
|
+
async getApplications (allowUnloaded = false) {
|
|
1018
1006
|
return {
|
|
1019
1007
|
entrypoint: this.#entrypointId,
|
|
1020
1008
|
production: this.#isProduction,
|
|
1021
|
-
applications: await Promise.all(
|
|
1009
|
+
applications: await Promise.all(
|
|
1010
|
+
this.getApplicationsIds().map(id => this.getApplicationDetails(id, allowUnloaded))
|
|
1011
|
+
)
|
|
1022
1012
|
}
|
|
1023
1013
|
}
|
|
1024
1014
|
|
|
@@ -1070,19 +1060,19 @@ export class Runtime extends EventEmitter {
|
|
|
1070
1060
|
throw e
|
|
1071
1061
|
}
|
|
1072
1062
|
|
|
1073
|
-
const { entrypoint,
|
|
1063
|
+
const { entrypoint, localUrl } = application[kConfig]
|
|
1074
1064
|
|
|
1075
1065
|
const status = await sendViaITC(application, 'getStatus')
|
|
1076
|
-
const { type, version } = await sendViaITC(application, 'getApplicationInfo')
|
|
1066
|
+
const { type, version, dependencies } = await sendViaITC(application, 'getApplicationInfo')
|
|
1077
1067
|
|
|
1078
1068
|
const applicationDetails = {
|
|
1079
1069
|
id,
|
|
1080
1070
|
type,
|
|
1081
1071
|
status,
|
|
1072
|
+
dependencies,
|
|
1082
1073
|
version,
|
|
1083
1074
|
localUrl,
|
|
1084
|
-
entrypoint
|
|
1085
|
-
dependencies
|
|
1075
|
+
entrypoint
|
|
1086
1076
|
}
|
|
1087
1077
|
|
|
1088
1078
|
if (this.#isProduction) {
|
|
@@ -1175,17 +1165,59 @@ export class Runtime extends EventEmitter {
|
|
|
1175
1165
|
this.logger.info(`Platformatic is now listening at ${this.#url}`)
|
|
1176
1166
|
}
|
|
1177
1167
|
|
|
1168
|
+
async #setupApplications () {
|
|
1169
|
+
const config = this.#config
|
|
1170
|
+
const setupInvocations = []
|
|
1171
|
+
|
|
1172
|
+
// Parse all applications and verify we're not missing any path or resolved application
|
|
1173
|
+
for (const applicationConfig of config.applications) {
|
|
1174
|
+
// If there is no application path, check if the application was resolved
|
|
1175
|
+
if (!applicationConfig.path) {
|
|
1176
|
+
if (applicationConfig.url) {
|
|
1177
|
+
// Try to backfill the path for external applications
|
|
1178
|
+
applicationConfig.path = join(this.#root, config.resolvedApplicationsBasePath, applicationConfig.id)
|
|
1179
|
+
|
|
1180
|
+
if (!existsSync(applicationConfig.path)) {
|
|
1181
|
+
const executable = globalThis.platformatic?.executable ?? 'platformatic'
|
|
1182
|
+
this.logger.error(
|
|
1183
|
+
`The path for application "%s" does not exist. Please run "${executable} resolve" and try again.`,
|
|
1184
|
+
applicationConfig.id
|
|
1185
|
+
)
|
|
1186
|
+
|
|
1187
|
+
await this.closeAndThrow(new RuntimeAbortedError())
|
|
1188
|
+
}
|
|
1189
|
+
} else {
|
|
1190
|
+
this.logger.error(
|
|
1191
|
+
'The application "%s" has no path defined. Please check your configuration and try again.',
|
|
1192
|
+
applicationConfig.id
|
|
1193
|
+
)
|
|
1194
|
+
|
|
1195
|
+
await this.closeAndThrow(new RuntimeAbortedError())
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
setupInvocations.push([applicationConfig])
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
await executeInParallel(this.#setupApplication.bind(this), setupInvocations, this.#concurrency)
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1178
1205
|
async #setupApplication (applicationConfig) {
|
|
1179
|
-
if (this.#status === 'stopping' || this.#status === 'closed')
|
|
1206
|
+
if (this.#status === 'stopping' || this.#status === 'closed') {
|
|
1207
|
+
return
|
|
1208
|
+
}
|
|
1180
1209
|
|
|
1181
1210
|
const config = this.#config
|
|
1182
1211
|
const workersCount = await this.#workers.getCount(applicationConfig.id)
|
|
1183
1212
|
const id = applicationConfig.id
|
|
1213
|
+
const setupInvocations = []
|
|
1184
1214
|
|
|
1185
1215
|
for (let i = 0; i < workersCount; i++) {
|
|
1186
|
-
|
|
1216
|
+
setupInvocations.push([config, applicationConfig, workersCount, id, i])
|
|
1187
1217
|
}
|
|
1188
1218
|
|
|
1219
|
+
await executeInParallel(this.#setupWorker.bind(this), setupInvocations, this.#concurrency)
|
|
1220
|
+
|
|
1189
1221
|
this.emit('application:init', id)
|
|
1190
1222
|
}
|
|
1191
1223
|
|
|
@@ -1287,6 +1319,7 @@ export class Runtime extends EventEmitter {
|
|
|
1287
1319
|
|
|
1288
1320
|
// Track application exiting
|
|
1289
1321
|
const eventPayload = { application: applicationId, worker: index, workersCount }
|
|
1322
|
+
|
|
1290
1323
|
worker.once('exit', code => {
|
|
1291
1324
|
if (worker[kWorkerStatus] === 'exited') {
|
|
1292
1325
|
return
|
|
@@ -1338,7 +1371,6 @@ export class Runtime extends EventEmitter {
|
|
|
1338
1371
|
worker[kApplicationId] = applicationId
|
|
1339
1372
|
worker[kWorkerId] = workersCount > 1 ? index : undefined
|
|
1340
1373
|
worker[kWorkerStatus] = 'boot'
|
|
1341
|
-
worker[kForwardEvents] = false
|
|
1342
1374
|
|
|
1343
1375
|
if (inspectorOptions) {
|
|
1344
1376
|
worker[kInspectorOptions] = {
|
|
@@ -1352,12 +1384,7 @@ export class Runtime extends EventEmitter {
|
|
|
1352
1384
|
worker[kITC] = new ITC({
|
|
1353
1385
|
name: workerId + '-runtime',
|
|
1354
1386
|
port: worker,
|
|
1355
|
-
handlers:
|
|
1356
|
-
...this.#workerITCHandlers,
|
|
1357
|
-
setEventsForwarding (value) {
|
|
1358
|
-
worker[kForwardEvents] = value
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1387
|
+
handlers: this.#workerITCHandlers
|
|
1361
1388
|
})
|
|
1362
1389
|
worker[kITC].listen()
|
|
1363
1390
|
|
|
@@ -1403,15 +1430,13 @@ export class Runtime extends EventEmitter {
|
|
|
1403
1430
|
this.#meshInterceptor.route(applicationId, worker)
|
|
1404
1431
|
}
|
|
1405
1432
|
|
|
1406
|
-
//
|
|
1407
|
-
|
|
1408
|
-
applicationConfig.dependencies = dependencies
|
|
1433
|
+
// Wait for initialization
|
|
1434
|
+
await waitEventFromITC(worker, 'init')
|
|
1409
1435
|
|
|
1410
1436
|
if (applicationConfig.entrypoint) {
|
|
1411
1437
|
this.#entrypointId = applicationId
|
|
1412
1438
|
}
|
|
1413
1439
|
|
|
1414
|
-
// This must be done here as the dependencies are filled above
|
|
1415
1440
|
worker[kConfig] = { ...applicationConfig, health, workers: workersCount }
|
|
1416
1441
|
worker[kWorkerStatus] = 'init'
|
|
1417
1442
|
this.emit('application:worker:init', eventPayload)
|
|
@@ -1587,6 +1612,7 @@ export class Runtime extends EventEmitter {
|
|
|
1587
1612
|
}
|
|
1588
1613
|
} catch (err) {
|
|
1589
1614
|
const error = ensureError(err)
|
|
1615
|
+
worker[kITC].notify('application:worker:start:processed')
|
|
1590
1616
|
|
|
1591
1617
|
// TODO: handle port allocation error here
|
|
1592
1618
|
if (error.code === 'EADDRINUSE' || error.code === 'EACCES') throw error
|
|
@@ -1608,7 +1634,7 @@ export class Runtime extends EventEmitter {
|
|
|
1608
1634
|
this.emit('application:worker:start:error', { ...eventPayload, error })
|
|
1609
1635
|
|
|
1610
1636
|
if (error.code !== 'PLT_RUNTIME_APPLICATION_START_TIMEOUT') {
|
|
1611
|
-
this.logger.error({ err: ensureLoggableError(error) }, `Failed to start ${label}
|
|
1637
|
+
this.logger.error({ err: ensureLoggableError(error) }, `Failed to start ${label}: ${error.message}`)
|
|
1612
1638
|
}
|
|
1613
1639
|
|
|
1614
1640
|
const restartOnError = config.restartOnError
|
|
@@ -1619,6 +1645,7 @@ export class Runtime extends EventEmitter {
|
|
|
1619
1645
|
|
|
1620
1646
|
if (bootstrapAttempt++ >= MAX_BOOTSTRAP_ATTEMPTS || restartOnError === 0) {
|
|
1621
1647
|
this.logger.error(`Failed to start ${label} after ${MAX_BOOTSTRAP_ATTEMPTS} attempts.`)
|
|
1648
|
+
this.emit('application:worker:start:failed', { ...eventPayload, error })
|
|
1622
1649
|
throw error
|
|
1623
1650
|
}
|
|
1624
1651
|
|
|
@@ -1636,7 +1663,7 @@ export class Runtime extends EventEmitter {
|
|
|
1636
1663
|
}
|
|
1637
1664
|
}
|
|
1638
1665
|
|
|
1639
|
-
async #stopWorker (workersCount, id, index, silent, worker
|
|
1666
|
+
async #stopWorker (workersCount, id, index, silent, worker, dependents) {
|
|
1640
1667
|
if (!worker) {
|
|
1641
1668
|
worker = await this.#getWorkerById(id, index, false, false)
|
|
1642
1669
|
}
|
|
@@ -1667,11 +1694,15 @@ export class Runtime extends EventEmitter {
|
|
|
1667
1694
|
|
|
1668
1695
|
// Always send the stop message, it will shut down workers that only had ITC and interceptors setup
|
|
1669
1696
|
try {
|
|
1670
|
-
await executeWithTimeout(sendViaITC(worker, 'stop'), exitTimeout)
|
|
1697
|
+
await executeWithTimeout(sendViaITC(worker, 'stop', { force: !!this.error, dependents }), exitTimeout)
|
|
1671
1698
|
} catch (error) {
|
|
1672
|
-
this.emit('application:worker:stop:
|
|
1699
|
+
this.emit('application:worker:stop:error', eventPayload)
|
|
1673
1700
|
this.logger.info({ error: ensureLoggableError(error) }, `Failed to stop ${label}. Killing a worker thread.`)
|
|
1674
1701
|
} finally {
|
|
1702
|
+
worker[kITC].notify('application:worker:stop:processed')
|
|
1703
|
+
// Wait for the processed message to be received
|
|
1704
|
+
await sleep(1)
|
|
1705
|
+
|
|
1675
1706
|
worker[kITC].close()
|
|
1676
1707
|
}
|
|
1677
1708
|
|
|
@@ -1800,7 +1831,7 @@ export class Runtime extends EventEmitter {
|
|
|
1800
1831
|
throw e
|
|
1801
1832
|
}
|
|
1802
1833
|
|
|
1803
|
-
await this.#stopWorker(workersCount, applicationId, index, false, worker)
|
|
1834
|
+
await this.#stopWorker(workersCount, applicationId, index, false, worker, [])
|
|
1804
1835
|
}
|
|
1805
1836
|
|
|
1806
1837
|
async #getApplicationById (applicationId, ensureStarted = false, mustExist = true) {
|
|
@@ -2323,7 +2354,7 @@ export class Runtime extends EventEmitter {
|
|
|
2323
2354
|
for (let i = currentWorkers - 1; i >= workers; i--) {
|
|
2324
2355
|
const worker = await this.#getWorkerById(applicationId, i, false, false)
|
|
2325
2356
|
await sendViaITC(worker, 'removeFromMesh')
|
|
2326
|
-
await this.#stopWorker(currentWorkers, applicationId, i, false, worker)
|
|
2357
|
+
await this.#stopWorker(currentWorkers, applicationId, i, false, worker, [])
|
|
2327
2358
|
report.stopped.push(i)
|
|
2328
2359
|
}
|
|
2329
2360
|
await this.#updateApplicationConfigWorkers(applicationId, workers)
|
package/lib/worker/controller.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ensureLoggableError,
|
|
3
3
|
FileWatcher,
|
|
4
|
+
kHandledError,
|
|
4
5
|
listRecognizedConfigurationFiles,
|
|
5
6
|
loadConfiguration,
|
|
6
7
|
loadConfigurationModule
|
|
@@ -9,6 +10,7 @@ import debounce from 'debounce'
|
|
|
9
10
|
import { EventEmitter } from 'node:events'
|
|
10
11
|
import { existsSync } from 'node:fs'
|
|
11
12
|
import { resolve } from 'node:path'
|
|
13
|
+
import { getActiveResourcesInfo } from 'node:process'
|
|
12
14
|
import { workerData } from 'node:worker_threads'
|
|
13
15
|
import { getGlobalDispatcher, setGlobalDispatcher } from 'undici'
|
|
14
16
|
import { ApplicationAlreadyStartedError, RuntimeNotStartedError } from '../errors.js'
|
|
@@ -55,9 +57,11 @@ export class Controller extends EventEmitter {
|
|
|
55
57
|
this.#lastELU = performance.eventLoopUtilization()
|
|
56
58
|
|
|
57
59
|
this.#context = {
|
|
60
|
+
controller: this,
|
|
58
61
|
applicationId: this.applicationId,
|
|
59
62
|
workerId: this.workerId,
|
|
60
63
|
directory: this.appConfig.path,
|
|
64
|
+
dependencies: this.appConfig.dependencies,
|
|
61
65
|
isEntrypoint: this.appConfig.entrypoint,
|
|
62
66
|
isProduction: this.appConfig.isProduction,
|
|
63
67
|
telemetryConfig,
|
|
@@ -83,10 +87,7 @@ export class Controller extends EventEmitter {
|
|
|
83
87
|
}
|
|
84
88
|
}
|
|
85
89
|
|
|
86
|
-
|
|
87
|
-
return this.capability.getBootstrapDependencies?.() ?? []
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
+
// Note: capability's init() is executed within start
|
|
90
91
|
async init () {
|
|
91
92
|
try {
|
|
92
93
|
const appConfig = this.appConfig
|
|
@@ -142,10 +143,17 @@ export class Controller extends EventEmitter {
|
|
|
142
143
|
|
|
143
144
|
try {
|
|
144
145
|
await this.capability.init?.()
|
|
146
|
+
this.emit('init')
|
|
145
147
|
} catch (err) {
|
|
146
148
|
this.#logAndThrow(err)
|
|
147
149
|
}
|
|
148
150
|
|
|
151
|
+
this.emit('starting')
|
|
152
|
+
|
|
153
|
+
if (this.capability.status === 'stopped') {
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
149
157
|
if (this.#watch) {
|
|
150
158
|
const watchConfig = await this.capability.getWatchConfig()
|
|
151
159
|
|
|
@@ -174,21 +182,23 @@ export class Controller extends EventEmitter {
|
|
|
174
182
|
|
|
175
183
|
this.#started = true
|
|
176
184
|
this.#starting = false
|
|
177
|
-
this.emit('
|
|
185
|
+
this.emit('started')
|
|
178
186
|
}
|
|
179
187
|
|
|
180
|
-
async stop (force = false) {
|
|
188
|
+
async stop (force = false, dependents = []) {
|
|
181
189
|
if (!force && (!this.#started || this.#starting)) {
|
|
182
190
|
throw new RuntimeNotStartedError()
|
|
183
191
|
}
|
|
184
192
|
|
|
193
|
+
this.emit('stopping')
|
|
185
194
|
await this.#stopFileWatching()
|
|
195
|
+
await this.capability.waitForDependentsStop(dependents)
|
|
186
196
|
await this.capability.stop()
|
|
187
197
|
|
|
188
198
|
this.#started = false
|
|
189
199
|
this.#starting = false
|
|
190
200
|
this.#listening = false
|
|
191
|
-
this.emit('
|
|
201
|
+
this.emit('stopped')
|
|
192
202
|
}
|
|
193
203
|
|
|
194
204
|
async listen () {
|
|
@@ -213,6 +223,7 @@ export class Controller extends EventEmitter {
|
|
|
213
223
|
globalThis.platformatic.onHttpStatsSize(url, size || 0)
|
|
214
224
|
}
|
|
215
225
|
}
|
|
226
|
+
globalThis.platformatic.onActiveResourcesEventLoop(getActiveResourcesInfo().length)
|
|
216
227
|
return this.capability.getMetrics({ format })
|
|
217
228
|
}
|
|
218
229
|
|
|
@@ -260,7 +271,11 @@ export class Controller extends EventEmitter {
|
|
|
260
271
|
}
|
|
261
272
|
|
|
262
273
|
#logAndThrow (err) {
|
|
263
|
-
globalThis.platformatic.logger.error(
|
|
274
|
+
globalThis.platformatic.logger.error(
|
|
275
|
+
{ err: ensureLoggableError(err) },
|
|
276
|
+
err[kHandledError] ? err.message : 'The application threw an error.'
|
|
277
|
+
)
|
|
278
|
+
|
|
264
279
|
throw err
|
|
265
280
|
}
|
|
266
281
|
|
package/lib/worker/itc.js
CHANGED
|
@@ -69,8 +69,8 @@ export async function waitEventFromITC (worker, event) {
|
|
|
69
69
|
return safeHandleInITC(worker, () => once(worker[kITC], event))
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
export function setupITC (
|
|
73
|
-
const messaging = new MessagingITC(
|
|
72
|
+
export function setupITC (controller, application, dispatcher, sharedContext) {
|
|
73
|
+
const messaging = new MessagingITC(controller.appConfig.id, workerData.config)
|
|
74
74
|
|
|
75
75
|
Object.assign(globalThis.platformatic ?? {}, {
|
|
76
76
|
messaging: {
|
|
@@ -80,55 +80,61 @@ export function setupITC (instance, application, dispatcher, sharedContext) {
|
|
|
80
80
|
})
|
|
81
81
|
|
|
82
82
|
const itc = new ITC({
|
|
83
|
-
name:
|
|
83
|
+
name: controller.appConfig.id + '-worker',
|
|
84
84
|
port: parentPort,
|
|
85
85
|
handlers: {
|
|
86
86
|
async start () {
|
|
87
|
-
const status =
|
|
87
|
+
const status = controller.getStatus()
|
|
88
88
|
|
|
89
89
|
if (status === 'starting') {
|
|
90
|
-
await once(
|
|
90
|
+
await once(controller, 'start')
|
|
91
91
|
} else {
|
|
92
92
|
// This gives a chance to a capability to perform custom logic
|
|
93
93
|
globalThis.platformatic.events.emit('start')
|
|
94
94
|
|
|
95
95
|
try {
|
|
96
|
-
await
|
|
96
|
+
await controller.start()
|
|
97
97
|
} catch (e) {
|
|
98
|
-
await
|
|
99
|
-
|
|
98
|
+
await controller.stop(true)
|
|
99
|
+
|
|
100
|
+
// Reply to the runtime that the start failed, so it can cleanup
|
|
101
|
+
once(itc, 'application:worker:start:processed').then(() => {
|
|
102
|
+
closeITC(dispatcher, itc, messaging).catch(() => {})
|
|
103
|
+
})
|
|
100
104
|
|
|
101
105
|
throw ensureLoggableError(e)
|
|
102
106
|
}
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
if (application.entrypoint) {
|
|
106
|
-
await
|
|
110
|
+
await controller.listen()
|
|
107
111
|
}
|
|
108
112
|
|
|
109
|
-
dispatcher.replaceServer(await
|
|
110
|
-
return application.entrypoint ?
|
|
113
|
+
dispatcher.replaceServer(await controller.capability.getDispatchTarget())
|
|
114
|
+
return application.entrypoint ? controller.capability.getUrl() : null
|
|
111
115
|
},
|
|
112
116
|
|
|
113
|
-
async stop () {
|
|
114
|
-
const status =
|
|
117
|
+
async stop ({ force, dependents }) {
|
|
118
|
+
const status = controller.getStatus()
|
|
115
119
|
|
|
116
|
-
if (status === 'starting') {
|
|
117
|
-
await once(
|
|
120
|
+
if (!force && status === 'starting') {
|
|
121
|
+
await once(controller, 'start')
|
|
118
122
|
}
|
|
119
123
|
|
|
120
|
-
if (status.startsWith('start')) {
|
|
124
|
+
if (force || status.startsWith('start')) {
|
|
121
125
|
// This gives a chance to a capability to perform custom logic
|
|
122
126
|
globalThis.platformatic.events.emit('stop')
|
|
123
127
|
|
|
124
|
-
await
|
|
128
|
+
await controller.stop(force, dependents)
|
|
125
129
|
}
|
|
126
130
|
|
|
127
|
-
|
|
131
|
+
once(itc, 'application:worker:stop:processed').then(() => {
|
|
132
|
+
closeITC(dispatcher, itc, messaging).catch(() => {})
|
|
133
|
+
})
|
|
128
134
|
},
|
|
129
135
|
|
|
130
136
|
async build () {
|
|
131
|
-
return
|
|
137
|
+
return controller.capability.build()
|
|
132
138
|
},
|
|
133
139
|
|
|
134
140
|
async removeFromMesh () {
|
|
@@ -136,7 +142,7 @@ export function setupITC (instance, application, dispatcher, sharedContext) {
|
|
|
136
142
|
},
|
|
137
143
|
|
|
138
144
|
inject (injectParams) {
|
|
139
|
-
return
|
|
145
|
+
return controller.capability.inject(injectParams)
|
|
140
146
|
},
|
|
141
147
|
|
|
142
148
|
async updateUndiciInterceptors (undiciConfig) {
|
|
@@ -150,27 +156,27 @@ export function setupITC (instance, application, dispatcher, sharedContext) {
|
|
|
150
156
|
},
|
|
151
157
|
|
|
152
158
|
getStatus () {
|
|
153
|
-
return
|
|
159
|
+
return controller.getStatus()
|
|
154
160
|
},
|
|
155
161
|
|
|
156
162
|
getApplicationInfo () {
|
|
157
|
-
return
|
|
163
|
+
return controller.capability.getInfo()
|
|
158
164
|
},
|
|
159
165
|
|
|
160
166
|
async getApplicationConfig () {
|
|
161
|
-
const current = await
|
|
167
|
+
const current = await controller.capability.getConfig()
|
|
162
168
|
// Remove all undefined keys from the config
|
|
163
169
|
return JSON.parse(JSON.stringify(current))
|
|
164
170
|
},
|
|
165
171
|
|
|
166
172
|
async getApplicationEnv () {
|
|
167
173
|
// Remove all undefined keys from the config
|
|
168
|
-
return JSON.parse(JSON.stringify({ ...process.env, ...(await
|
|
174
|
+
return JSON.parse(JSON.stringify({ ...process.env, ...(await controller.capability.getEnv()) }))
|
|
169
175
|
},
|
|
170
176
|
|
|
171
177
|
async getApplicationOpenAPISchema () {
|
|
172
178
|
try {
|
|
173
|
-
return await
|
|
179
|
+
return await controller.capability.getOpenapiSchema()
|
|
174
180
|
} catch (err) {
|
|
175
181
|
throw new FailedToRetrieveOpenAPISchemaError(application.id, err.message)
|
|
176
182
|
}
|
|
@@ -178,7 +184,7 @@ export function setupITC (instance, application, dispatcher, sharedContext) {
|
|
|
178
184
|
|
|
179
185
|
async getApplicationGraphQLSchema () {
|
|
180
186
|
try {
|
|
181
|
-
return await
|
|
187
|
+
return await controller.capability.getGraphqlSchema()
|
|
182
188
|
} catch (err) {
|
|
183
189
|
throw new FailedToRetrieveGraphQLSchemaError(application.id, err.message)
|
|
184
190
|
}
|
|
@@ -186,7 +192,7 @@ export function setupITC (instance, application, dispatcher, sharedContext) {
|
|
|
186
192
|
|
|
187
193
|
async getApplicationMeta () {
|
|
188
194
|
try {
|
|
189
|
-
return await
|
|
195
|
+
return await controller.capability.getMeta()
|
|
190
196
|
} catch (err) {
|
|
191
197
|
throw new FailedToRetrieveMetaError(application.id, err.message)
|
|
192
198
|
}
|
|
@@ -194,7 +200,7 @@ export function setupITC (instance, application, dispatcher, sharedContext) {
|
|
|
194
200
|
|
|
195
201
|
async getMetrics (format) {
|
|
196
202
|
try {
|
|
197
|
-
return await
|
|
203
|
+
return await controller.getMetrics({ format })
|
|
198
204
|
} catch (err) {
|
|
199
205
|
throw new FailedToRetrieveMetricsError(application.id, err.message)
|
|
200
206
|
}
|
|
@@ -202,7 +208,7 @@ export function setupITC (instance, application, dispatcher, sharedContext) {
|
|
|
202
208
|
|
|
203
209
|
async getHealth () {
|
|
204
210
|
try {
|
|
205
|
-
return await
|
|
211
|
+
return await controller.getHealth()
|
|
206
212
|
} catch (err) {
|
|
207
213
|
throw new FailedToRetrieveHealthError(application.id, err.message)
|
|
208
214
|
}
|
|
@@ -210,7 +216,7 @@ export function setupITC (instance, application, dispatcher, sharedContext) {
|
|
|
210
216
|
|
|
211
217
|
async getCustomHealthCheck () {
|
|
212
218
|
try {
|
|
213
|
-
return await
|
|
219
|
+
return await controller.capability.getCustomHealthCheck()
|
|
214
220
|
} catch (err) {
|
|
215
221
|
throw new FailedToPerformCustomHealthCheckError(application.id, err.message)
|
|
216
222
|
}
|
|
@@ -218,7 +224,7 @@ export function setupITC (instance, application, dispatcher, sharedContext) {
|
|
|
218
224
|
|
|
219
225
|
async getCustomReadinessCheck () {
|
|
220
226
|
try {
|
|
221
|
-
return await
|
|
227
|
+
return await controller.capability.getCustomReadinessCheck()
|
|
222
228
|
} catch (err) {
|
|
223
229
|
throw new FailedToPerformCustomReadinessCheckError(application.id, err.message)
|
|
224
230
|
}
|
|
@@ -234,7 +240,7 @@ export function setupITC (instance, application, dispatcher, sharedContext) {
|
|
|
234
240
|
}
|
|
235
241
|
})
|
|
236
242
|
|
|
237
|
-
|
|
243
|
+
controller.on('changed', () => {
|
|
238
244
|
itc.notify('changed')
|
|
239
245
|
})
|
|
240
246
|
|
package/lib/worker/main.js
CHANGED
|
@@ -166,7 +166,7 @@ async function main () {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
// Create the application
|
|
169
|
-
const
|
|
169
|
+
const controller = new Controller(
|
|
170
170
|
application,
|
|
171
171
|
workerData.worker.count > 1 ? workerData.worker.index : undefined,
|
|
172
172
|
application.telemetry,
|
|
@@ -177,13 +177,13 @@ async function main () {
|
|
|
177
177
|
!!config.watch
|
|
178
178
|
)
|
|
179
179
|
|
|
180
|
-
process.on('uncaughtException', handleUnhandled.bind(null,
|
|
181
|
-
process.on('unhandledRejection', handleUnhandled.bind(null,
|
|
180
|
+
process.on('uncaughtException', handleUnhandled.bind(null, controller, 'uncaught exception'))
|
|
181
|
+
process.on('unhandledRejection', handleUnhandled.bind(null, controller, 'unhandled rejection'))
|
|
182
182
|
|
|
183
|
-
await
|
|
183
|
+
await controller.init()
|
|
184
184
|
|
|
185
185
|
if (application.entrypoint && config.basePath) {
|
|
186
|
-
const meta = await
|
|
186
|
+
const meta = await controller.capability.getMeta()
|
|
187
187
|
if (!meta.gateway.wantsAbsoluteUrls) {
|
|
188
188
|
stripBasePath(config.basePath)
|
|
189
189
|
}
|
|
@@ -197,12 +197,10 @@ async function main () {
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
// Setup interaction with parent port
|
|
200
|
-
const itc = setupITC(
|
|
200
|
+
const itc = setupITC(controller, application, threadDispatcher, sharedContext)
|
|
201
201
|
globalThis[kITC] = itc
|
|
202
202
|
|
|
203
|
-
|
|
204
|
-
const dependencies = await app.getBootstrapDependencies()
|
|
205
|
-
itc.notify('init', { dependencies })
|
|
203
|
+
itc.notify('init')
|
|
206
204
|
}
|
|
207
205
|
|
|
208
206
|
function stripBasePath (basePath) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/runtime",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.8",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -34,18 +34,19 @@
|
|
|
34
34
|
"typescript": "^5.5.4",
|
|
35
35
|
"undici-oidc-interceptor": "^0.5.0",
|
|
36
36
|
"why-is-node-running": "^2.2.2",
|
|
37
|
-
"@platformatic/
|
|
38
|
-
"@platformatic/
|
|
39
|
-
"@platformatic/
|
|
40
|
-
"@platformatic/
|
|
41
|
-
"@platformatic/
|
|
42
|
-
"@platformatic/
|
|
37
|
+
"@platformatic/composer": "3.0.0-alpha.8",
|
|
38
|
+
"@platformatic/gateway": "3.0.0-alpha.8",
|
|
39
|
+
"@platformatic/db": "3.0.0-alpha.8",
|
|
40
|
+
"@platformatic/node": "3.0.0-alpha.8",
|
|
41
|
+
"@platformatic/service": "3.0.0-alpha.8",
|
|
42
|
+
"@platformatic/sql-graphql": "3.0.0-alpha.8",
|
|
43
|
+
"@platformatic/sql-mapper": "3.0.0-alpha.8",
|
|
44
|
+
"@platformatic/wattpm-pprof-capture": "3.0.0-alpha.8"
|
|
43
45
|
},
|
|
44
46
|
"dependencies": {
|
|
45
47
|
"@fastify/accepts": "^5.0.0",
|
|
46
48
|
"@fastify/error": "^4.0.0",
|
|
47
49
|
"@fastify/websocket": "^11.0.0",
|
|
48
|
-
"@hapi/topo": "^6.0.2",
|
|
49
50
|
"@opentelemetry/api": "^1.9.0",
|
|
50
51
|
"@platformatic/undici-cache-memory": "^0.8.1",
|
|
51
52
|
"@watchable/unpromise": "^1.0.2",
|
|
@@ -69,18 +70,18 @@
|
|
|
69
70
|
"undici": "^7.0.0",
|
|
70
71
|
"undici-thread-interceptor": "^0.14.0",
|
|
71
72
|
"ws": "^8.16.0",
|
|
72
|
-
"@platformatic/basic": "3.0.0-alpha.
|
|
73
|
-
"@platformatic/
|
|
74
|
-
"@platformatic/
|
|
75
|
-
"@platformatic/
|
|
76
|
-
"@platformatic/
|
|
77
|
-
"@platformatic/
|
|
73
|
+
"@platformatic/basic": "3.0.0-alpha.8",
|
|
74
|
+
"@platformatic/generators": "3.0.0-alpha.8",
|
|
75
|
+
"@platformatic/itc": "3.0.0-alpha.8",
|
|
76
|
+
"@platformatic/telemetry": "3.0.0-alpha.8",
|
|
77
|
+
"@platformatic/metrics": "3.0.0-alpha.8",
|
|
78
|
+
"@platformatic/foundation": "3.0.0-alpha.8"
|
|
78
79
|
},
|
|
79
80
|
"engines": {
|
|
80
81
|
"node": ">=22.18.0"
|
|
81
82
|
},
|
|
82
83
|
"scripts": {
|
|
83
|
-
"test": "
|
|
84
|
+
"test": "npm run test:main && npm run test:api && npm run test:cli && npm run test:start && npm run test:multiple-workers",
|
|
84
85
|
"test:main": "node --test --test-reporter=cleaner-spec-reporter --test-concurrency=1 --test-timeout=2000000 test/*.test.js test/versions/*.test.js",
|
|
85
86
|
"test:api": "node --test --test-reporter=cleaner-spec-reporter --test-concurrency=1 --test-timeout=2000000 test/api/*.test.js test/management-api/*.test.js",
|
|
86
87
|
"test:cli": "node --test --test-reporter=cleaner-spec-reporter --test-concurrency=1 --test-timeout=2000000 test/cli/*.test.js test/cli/**/*.test.js",
|
|
@@ -88,7 +89,7 @@
|
|
|
88
89
|
"test:multiple-workers": "node --test --test-reporter=cleaner-spec-reporter --test-concurrency=1 --test-timeout=2000000 test/multiple-workers/*.test.js",
|
|
89
90
|
"gen-schema": "node lib/schema.js > schema.json",
|
|
90
91
|
"gen-types": "json2ts > config.d.ts < schema.json",
|
|
91
|
-
"build": "
|
|
92
|
+
"build": "npm run gen-schema && npm run gen-types",
|
|
92
93
|
"lint": "eslint"
|
|
93
94
|
}
|
|
94
95
|
}
|
package/schema.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.0.0-alpha.
|
|
2
|
+
"$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.0.0-alpha.8.json",
|
|
3
3
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
4
|
"title": "Platformatic Runtime Config",
|
|
5
5
|
"type": "object",
|
|
@@ -186,6 +186,12 @@
|
|
|
186
186
|
}
|
|
187
187
|
]
|
|
188
188
|
},
|
|
189
|
+
"dependencies": {
|
|
190
|
+
"type": "array",
|
|
191
|
+
"items": {
|
|
192
|
+
"type": "string"
|
|
193
|
+
}
|
|
194
|
+
},
|
|
189
195
|
"arguments": {
|
|
190
196
|
"type": "array",
|
|
191
197
|
"items": {
|
|
@@ -347,6 +353,13 @@
|
|
|
347
353
|
},
|
|
348
354
|
"additionalProperties": false
|
|
349
355
|
},
|
|
356
|
+
"dependencies": {
|
|
357
|
+
"type": "array",
|
|
358
|
+
"items": {
|
|
359
|
+
"type": "string"
|
|
360
|
+
},
|
|
361
|
+
"default": []
|
|
362
|
+
},
|
|
350
363
|
"arguments": {
|
|
351
364
|
"type": "array",
|
|
352
365
|
"items": {
|
|
@@ -576,6 +589,13 @@
|
|
|
576
589
|
},
|
|
577
590
|
"additionalProperties": false
|
|
578
591
|
},
|
|
592
|
+
"dependencies": {
|
|
593
|
+
"type": "array",
|
|
594
|
+
"items": {
|
|
595
|
+
"type": "string"
|
|
596
|
+
},
|
|
597
|
+
"default": []
|
|
598
|
+
},
|
|
579
599
|
"arguments": {
|
|
580
600
|
"type": "array",
|
|
581
601
|
"items": {
|
|
@@ -805,6 +825,13 @@
|
|
|
805
825
|
},
|
|
806
826
|
"additionalProperties": false
|
|
807
827
|
},
|
|
828
|
+
"dependencies": {
|
|
829
|
+
"type": "array",
|
|
830
|
+
"items": {
|
|
831
|
+
"type": "string"
|
|
832
|
+
},
|
|
833
|
+
"default": []
|
|
834
|
+
},
|
|
808
835
|
"arguments": {
|
|
809
836
|
"type": "array",
|
|
810
837
|
"items": {
|
package/lib/dependencies.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { Sorter } from '@hapi/topo'
|
|
2
|
-
import { closest } from 'fastest-levenshtein'
|
|
3
|
-
import { MissingDependencyError } from './errors.js'
|
|
4
|
-
import { RoundRobinMap } from './worker/round-robin-map.js'
|
|
5
|
-
|
|
6
|
-
function missingDependencyErrorMessage (clientName, application, applications) {
|
|
7
|
-
const allNames = applications
|
|
8
|
-
.map(s => s.id)
|
|
9
|
-
.filter(id => id !== application.id)
|
|
10
|
-
.sort()
|
|
11
|
-
const closestName = closest(clientName, allNames)
|
|
12
|
-
let errorMsg = `application '${application.id}' has unknown dependency: '${clientName}'.`
|
|
13
|
-
if (closestName) {
|
|
14
|
-
errorMsg += ` Did you mean '${closestName}'?`
|
|
15
|
-
}
|
|
16
|
-
if (allNames.length) {
|
|
17
|
-
errorMsg += ` Known applications are: ${allNames.join(', ')}.`
|
|
18
|
-
}
|
|
19
|
-
return errorMsg
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function checkDependencies (applications) {
|
|
23
|
-
const allApplications = new Set(applications.map(s => s.id))
|
|
24
|
-
|
|
25
|
-
for (const application of applications) {
|
|
26
|
-
for (const dependency of application.dependencies) {
|
|
27
|
-
if (dependency.local && !allApplications.has(dependency.id)) {
|
|
28
|
-
throw new MissingDependencyError(missingDependencyErrorMessage(dependency.id, application, applications))
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function topologicalSort (workers, config) {
|
|
35
|
-
const topo = new Sorter()
|
|
36
|
-
|
|
37
|
-
for (const application of config.applications) {
|
|
38
|
-
const localDependencyIds = Array.from(application.dependencies)
|
|
39
|
-
.filter(dep => dep.local)
|
|
40
|
-
.map(dep => dep.id)
|
|
41
|
-
|
|
42
|
-
topo.add(application, {
|
|
43
|
-
group: application.id,
|
|
44
|
-
after: localDependencyIds,
|
|
45
|
-
manual: true
|
|
46
|
-
})
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
config.applications = topo.sort()
|
|
50
|
-
|
|
51
|
-
return new RoundRobinMap(
|
|
52
|
-
Array.from(workers.entries()).sort((a, b) => {
|
|
53
|
-
if (a[0] === b[0]) {
|
|
54
|
-
return 0
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const aIndex = config.applications.findIndex(s => s.id === a[0])
|
|
58
|
-
const bIndex = config.applications.findIndex(s => s.id === b[0])
|
|
59
|
-
return aIndex - bIndex
|
|
60
|
-
}),
|
|
61
|
-
workers.configuration
|
|
62
|
-
)
|
|
63
|
-
}
|