@platformatic/basic 3.0.0-alpha.6 → 3.0.0-rc.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 +1 -0
- package/index.d.ts +0 -7
- package/lib/capability.js +177 -10
- package/lib/worker/child-process.js +20 -1
- package/package.json +6 -6
- package/schema.json +12 -1
- package/lib/worker/child-transport.js +0 -59
package/config.d.ts
CHANGED
package/index.d.ts
CHANGED
|
@@ -2,12 +2,6 @@ export interface StartOptions {
|
|
|
2
2
|
listen?: boolean
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
-
export interface Dependency {
|
|
6
|
-
id: string
|
|
7
|
-
url?: string
|
|
8
|
-
local: boolean
|
|
9
|
-
}
|
|
10
|
-
|
|
11
5
|
export type BaseContext = Partial<{
|
|
12
6
|
applicationId: string
|
|
13
7
|
isEntrypoint: boolean
|
|
@@ -75,7 +69,6 @@ export class BaseCapability<Config = Record<string, any>, Options = BaseOptions>
|
|
|
75
69
|
body: object
|
|
76
70
|
}>
|
|
77
71
|
log (options: { message: string; level: string }): Promise<void>
|
|
78
|
-
getBootstrapDependencies (): Promise<Dependency[]>
|
|
79
72
|
getWatchConfig (): Promise<{
|
|
80
73
|
enabled: boolean
|
|
81
74
|
path: string
|
package/lib/capability.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
buildPinoOptions,
|
|
3
|
+
deepmerge,
|
|
4
|
+
executeWithTimeout,
|
|
5
|
+
kHandledError,
|
|
6
|
+
kMetadata,
|
|
7
|
+
kTimeout
|
|
8
|
+
} from '@platformatic/foundation'
|
|
2
9
|
import { client, collectMetrics, ensureMetricsGroup } from '@platformatic/metrics'
|
|
3
10
|
import { parseCommandString } from 'execa'
|
|
4
11
|
import { spawn } from 'node:child_process'
|
|
@@ -14,16 +21,44 @@ import { ChildManager } from './worker/child-manager.js'
|
|
|
14
21
|
const kITC = Symbol.for('plt.runtime.itc')
|
|
15
22
|
|
|
16
23
|
export class BaseCapability extends EventEmitter {
|
|
17
|
-
|
|
18
|
-
|
|
24
|
+
status
|
|
25
|
+
type
|
|
26
|
+
version
|
|
27
|
+
root
|
|
28
|
+
config
|
|
29
|
+
context
|
|
30
|
+
standardStreams
|
|
31
|
+
|
|
32
|
+
applicationId
|
|
33
|
+
workerId
|
|
34
|
+
telemetryConfig
|
|
35
|
+
serverConfig
|
|
36
|
+
openapiSchema
|
|
37
|
+
graphqlSchema
|
|
38
|
+
connectionString
|
|
39
|
+
basePath
|
|
40
|
+
isEntrypoint
|
|
41
|
+
isProduction
|
|
42
|
+
dependencies
|
|
43
|
+
customHealthCheck
|
|
44
|
+
customReadinessCheck
|
|
45
|
+
clientWs
|
|
46
|
+
runtimeConfig
|
|
47
|
+
stdout
|
|
48
|
+
stderr
|
|
19
49
|
subprocessForceClose
|
|
20
50
|
subprocessTerminationSignal
|
|
51
|
+
logger
|
|
52
|
+
metricsRegistr
|
|
53
|
+
|
|
21
54
|
#subprocessStarted
|
|
22
55
|
#metricsCollected
|
|
56
|
+
#pendingDependenciesWaits
|
|
23
57
|
|
|
24
58
|
constructor (type, version, root, config, context, standardStreams = {}) {
|
|
25
59
|
super()
|
|
26
60
|
|
|
61
|
+
this.status = ''
|
|
27
62
|
this.type = type
|
|
28
63
|
this.version = version
|
|
29
64
|
this.root = root
|
|
@@ -42,7 +77,7 @@ export class BaseCapability extends EventEmitter {
|
|
|
42
77
|
this.basePath = null
|
|
43
78
|
this.isEntrypoint = this.context.isEntrypoint
|
|
44
79
|
this.isProduction = this.context.isProduction
|
|
45
|
-
this
|
|
80
|
+
this.dependencies = this.context.dependencies ?? []
|
|
46
81
|
this.customHealthCheck = null
|
|
47
82
|
this.customReadinessCheck = null
|
|
48
83
|
this.clientWs = null
|
|
@@ -51,11 +86,11 @@ export class BaseCapability extends EventEmitter {
|
|
|
51
86
|
this.stderr = standardStreams?.stderr ?? process.stderr
|
|
52
87
|
this.subprocessForceClose = false
|
|
53
88
|
this.subprocessTerminationSignal = 'SIGINT'
|
|
54
|
-
|
|
55
89
|
this.logger = this._initializeLogger()
|
|
56
90
|
|
|
57
91
|
// Setup globals
|
|
58
92
|
this.registerGlobals({
|
|
93
|
+
capability: this,
|
|
59
94
|
applicationId: this.applicationId,
|
|
60
95
|
workerId: this.workerId,
|
|
61
96
|
logLevel: this.logger.level,
|
|
@@ -79,10 +114,25 @@ export class BaseCapability extends EventEmitter {
|
|
|
79
114
|
this.metricsRegistry = new client.Registry()
|
|
80
115
|
this.registerGlobals({ prometheus: { client, registry: this.metricsRegistry } })
|
|
81
116
|
}
|
|
117
|
+
|
|
118
|
+
this.#metricsCollected = false
|
|
119
|
+
this.#pendingDependenciesWaits = new Set()
|
|
82
120
|
}
|
|
83
121
|
|
|
84
|
-
init () {
|
|
85
|
-
|
|
122
|
+
async init () {
|
|
123
|
+
if (this.status) {
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Wait for explicit dependencies to start
|
|
128
|
+
await this.waitForDependenciesStart(this.dependencies)
|
|
129
|
+
|
|
130
|
+
if (this.status === 'stopped') {
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await this.updateContext()
|
|
135
|
+
this.status = 'init'
|
|
86
136
|
}
|
|
87
137
|
|
|
88
138
|
updateContext (_context) {
|
|
@@ -93,8 +143,12 @@ export class BaseCapability extends EventEmitter {
|
|
|
93
143
|
throw new Error('BaseCapability.start must be overriden by the subclasses')
|
|
94
144
|
}
|
|
95
145
|
|
|
96
|
-
stop () {
|
|
97
|
-
|
|
146
|
+
async stop () {
|
|
147
|
+
if (this.#pendingDependenciesWaits.size > 0) {
|
|
148
|
+
await Promise.allSettled(this.#pendingDependenciesWaits)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
this.status = 'stopped'
|
|
98
152
|
}
|
|
99
153
|
|
|
100
154
|
build () {
|
|
@@ -110,6 +164,106 @@ export class BaseCapability extends EventEmitter {
|
|
|
110
164
|
throw new Error('BaseCapability.inject must be overriden by the subclasses')
|
|
111
165
|
}
|
|
112
166
|
|
|
167
|
+
async waitForDependenciesStart (dependencies = []) {
|
|
168
|
+
if (!globalThis[kITC]) {
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const pending = new Set(dependencies)
|
|
173
|
+
|
|
174
|
+
// Ask the runtime the status of the dependencies and don't wait if they are already started
|
|
175
|
+
const workers = await globalThis[kITC].send('getWorkers')
|
|
176
|
+
|
|
177
|
+
for (const worker of Object.values(workers)) {
|
|
178
|
+
if (this.dependencies.includes(worker.application) && worker.status === 'started') {
|
|
179
|
+
pending.delete(worker.application)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!pending.size) {
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.logger.info({ dependencies: Array.from(pending) }, 'Waiting for dependencies to start.')
|
|
188
|
+
|
|
189
|
+
const { promise, resolve, reject } = Promise.withResolvers()
|
|
190
|
+
|
|
191
|
+
function runtimeEventHandler ({ event, payload }) {
|
|
192
|
+
if (event !== 'application:worker:started') {
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
pending.delete(payload.application)
|
|
197
|
+
|
|
198
|
+
if (pending.size === 0) {
|
|
199
|
+
cleanupEvents()
|
|
200
|
+
resolve()
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function stopHandler () {
|
|
205
|
+
cleanupEvents()
|
|
206
|
+
|
|
207
|
+
const error = new Error('One of the service dependencies was unable to start.')
|
|
208
|
+
error.dependencies = dependencies
|
|
209
|
+
error[kHandledError] = true
|
|
210
|
+
reject(error)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const cleanupEvents = () => {
|
|
214
|
+
globalThis[kITC].removeListener('runtime:event', runtimeEventHandler)
|
|
215
|
+
this.context.controller.removeListener('stopping', stopHandler)
|
|
216
|
+
this.#pendingDependenciesWaits.delete(promise)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
globalThis[kITC].on('runtime:event', runtimeEventHandler)
|
|
220
|
+
this.context.controller.on('stopping', stopHandler)
|
|
221
|
+
this.#pendingDependenciesWaits.add(promise)
|
|
222
|
+
|
|
223
|
+
return promise
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async waitForDependentsStop (dependents = []) {
|
|
227
|
+
if (!globalThis[kITC]) {
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const pending = new Set(dependents)
|
|
232
|
+
|
|
233
|
+
// Ask the runtime the status of the dependencies and don't wait if they are already stopped
|
|
234
|
+
const workers = await globalThis[kITC].send('getWorkers')
|
|
235
|
+
|
|
236
|
+
for (const worker of Object.values(workers)) {
|
|
237
|
+
if (this.dependencies.includes(worker.application) && worker.status === 'started') {
|
|
238
|
+
pending.delete(worker.application)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!pending.size) {
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
this.logger.info({ dependents: Array.from(pending) }, 'Waiting for dependents to stop.')
|
|
247
|
+
|
|
248
|
+
const { promise, resolve } = Promise.withResolvers()
|
|
249
|
+
|
|
250
|
+
function runtimeEventHandler ({ event, payload }) {
|
|
251
|
+
if (event !== 'application:worker:stopped') {
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
pending.delete(payload.application)
|
|
256
|
+
|
|
257
|
+
if (pending.size === 0) {
|
|
258
|
+
globalThis[kITC].removeListener('runtime:event', runtimeEventHandler)
|
|
259
|
+
resolve()
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
globalThis[kITC].on('runtime:event', runtimeEventHandler)
|
|
264
|
+
return promise
|
|
265
|
+
}
|
|
266
|
+
|
|
113
267
|
getUrl () {
|
|
114
268
|
return this.url
|
|
115
269
|
}
|
|
@@ -145,7 +299,7 @@ export class BaseCapability extends EventEmitter {
|
|
|
145
299
|
}
|
|
146
300
|
|
|
147
301
|
async getInfo () {
|
|
148
|
-
return { type: this.type, version: this.version }
|
|
302
|
+
return { type: this.type, version: this.version, dependencies: this.dependencies }
|
|
149
303
|
}
|
|
150
304
|
|
|
151
305
|
getDispatchFunc () {
|
|
@@ -304,6 +458,11 @@ export class BaseCapability extends EventEmitter {
|
|
|
304
458
|
this.basePath = path
|
|
305
459
|
})
|
|
306
460
|
|
|
461
|
+
this.childManager.on('event', event => {
|
|
462
|
+
globalThis[kITC].notify('event', event)
|
|
463
|
+
this.emit('application:worker:event', config)
|
|
464
|
+
})
|
|
465
|
+
|
|
307
466
|
// This is not really important for the URL but sometimes it also a sign
|
|
308
467
|
// that the process has been replaced and thus we need to update the client WebSocket
|
|
309
468
|
this.childManager.on('url', (url, clientWs) => {
|
|
@@ -389,6 +548,7 @@ export class BaseCapability extends EventEmitter {
|
|
|
389
548
|
isEntrypoint: this.isEntrypoint,
|
|
390
549
|
runtimeBasePath: this.runtimeConfig?.basePath ?? null,
|
|
391
550
|
wantsAbsoluteUrls: meta.gateway?.wantsAbsoluteUrls ?? false,
|
|
551
|
+
exitOnUnhandledErrors: this.runtimeConfig.exitOnUnhandledErrors ?? true,
|
|
392
552
|
/* c8 ignore next 2 - else */
|
|
393
553
|
port: (this.isEntrypoint ? this.serverConfig?.port || 0 : undefined) ?? true,
|
|
394
554
|
host: (this.isEntrypoint ? this.serverConfig?.hostname : undefined) ?? true,
|
|
@@ -559,6 +719,13 @@ export class BaseCapability extends EventEmitter {
|
|
|
559
719
|
globalThis.platformatic.onHttpStatsSize = (url, val) => {
|
|
560
720
|
httpStatsSizeMetric.set({ dispatcher_stats_url: url }, val)
|
|
561
721
|
}
|
|
722
|
+
|
|
723
|
+
const activeResourcesEventLoopMetric = new client.Gauge({
|
|
724
|
+
name: 'active_resources_event_loop',
|
|
725
|
+
help: 'Number of active resources keeping the event loop alive',
|
|
726
|
+
registers: [registry]
|
|
727
|
+
})
|
|
728
|
+
globalThis.platformatic.onActiveResourcesEventLoop = val => activeResourcesEventLoopMetric.set(val)
|
|
562
729
|
}
|
|
563
730
|
|
|
564
731
|
async #invalidateHttpCache (opts = {}) {
|
|
@@ -112,7 +112,11 @@ export class ChildProcess extends ITC {
|
|
|
112
112
|
|
|
113
113
|
this.listen()
|
|
114
114
|
this.#setupLogger()
|
|
115
|
-
|
|
115
|
+
|
|
116
|
+
if (globalThis.platformatic.exitOnUnhandledErrors) {
|
|
117
|
+
this.#setupHandlers()
|
|
118
|
+
}
|
|
119
|
+
|
|
116
120
|
this.#setupServer()
|
|
117
121
|
this.#setupInterceptors()
|
|
118
122
|
|
|
@@ -282,6 +286,13 @@ export class ChildProcess extends ITC {
|
|
|
282
286
|
globalThis.platformatic.onHttpStatsSize = (url, val) => {
|
|
283
287
|
httpStatsSizeMetric.set({ dispatcher_stats_url: url }, val)
|
|
284
288
|
}
|
|
289
|
+
|
|
290
|
+
const activeResourcesEventLoopMetric = new client.Gauge({
|
|
291
|
+
name: 'active_resources_event_loop',
|
|
292
|
+
help: 'Number of active resources keeping the event loop alive',
|
|
293
|
+
registers: [registry]
|
|
294
|
+
})
|
|
295
|
+
globalThis.platformatic.onActiveResourcesEventLoop = val => activeResourcesEventLoopMetric.set(val)
|
|
285
296
|
}
|
|
286
297
|
|
|
287
298
|
async #getMetrics ({ format } = {}) {
|
|
@@ -397,6 +408,14 @@ export class ChildProcess extends ITC {
|
|
|
397
408
|
|
|
398
409
|
process.on('uncaughtException', handleUnhandled.bind(this, 'uncaught exception'))
|
|
399
410
|
process.on('unhandledRejection', handleUnhandled.bind(this, 'unhandled rejection'))
|
|
411
|
+
|
|
412
|
+
process.on('newListener', event => {
|
|
413
|
+
if (event === 'uncaughtException' || event === 'unhandledRejection') {
|
|
414
|
+
this.#logger.warn(
|
|
415
|
+
`A listener has been added for the "process.${event}" event. This listener will be never triggered as Watt default behavior will kill the process before.\n To disable this behavior, set "exitOnUnhandledErrors" to false in the runtime config.`
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
})
|
|
400
419
|
}
|
|
401
420
|
|
|
402
421
|
#notifyConfig (config) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/basic",
|
|
3
|
-
"version": "3.0.0-
|
|
3
|
+
"version": "3.0.0-rc.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
"split2": "^4.2.0",
|
|
26
26
|
"undici": "^7.0.0",
|
|
27
27
|
"ws": "^8.18.0",
|
|
28
|
-
"@platformatic/
|
|
29
|
-
"@platformatic/
|
|
30
|
-
"@platformatic/metrics": "3.0.0-
|
|
31
|
-
"@platformatic/telemetry": "3.0.0-
|
|
28
|
+
"@platformatic/foundation": "3.0.0-rc.1",
|
|
29
|
+
"@platformatic/itc": "3.0.0-rc.1",
|
|
30
|
+
"@platformatic/metrics": "3.0.0-rc.1",
|
|
31
|
+
"@platformatic/telemetry": "3.0.0-rc.1"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"cleaner-spec-reporter": "^0.5.0",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"test": "node --test --test-reporter=cleaner-spec-reporter --test-concurrency=1 --test-timeout=2000000 test/*.test.js test/**/*.test.js",
|
|
48
48
|
"gen-schema": "node lib/schema.js > schema.json",
|
|
49
49
|
"gen-types": "json2ts > config.d.ts < schema.json",
|
|
50
|
-
"build": "
|
|
50
|
+
"build": "npm run gen-schema && npm run gen-types",
|
|
51
51
|
"lint": "eslint"
|
|
52
52
|
}
|
|
53
53
|
}
|
package/schema.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://schemas.platformatic.dev/@platformatic/basic/3.0.0-
|
|
2
|
+
"$id": "https://schemas.platformatic.dev/@platformatic/basic/3.0.0-rc.1.json",
|
|
3
3
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
4
|
"title": "Platformatic Basic Config",
|
|
5
5
|
"type": "object",
|
|
@@ -175,6 +175,13 @@
|
|
|
175
175
|
},
|
|
176
176
|
"additionalProperties": false
|
|
177
177
|
},
|
|
178
|
+
"dependencies": {
|
|
179
|
+
"type": "array",
|
|
180
|
+
"items": {
|
|
181
|
+
"type": "string"
|
|
182
|
+
},
|
|
183
|
+
"default": []
|
|
184
|
+
},
|
|
178
185
|
"arguments": {
|
|
179
186
|
"type": "array",
|
|
180
187
|
"items": {
|
|
@@ -556,6 +563,10 @@
|
|
|
556
563
|
}
|
|
557
564
|
]
|
|
558
565
|
},
|
|
566
|
+
"exitOnUnhandledErrors": {
|
|
567
|
+
"default": true,
|
|
568
|
+
"type": "boolean"
|
|
569
|
+
},
|
|
559
570
|
"gracefulShutdown": {
|
|
560
571
|
"type": "object",
|
|
561
572
|
"properties": {
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { ensureLoggableError } from '@platformatic/foundation'
|
|
2
|
-
import { generateRequest, sanitize } from '@platformatic/itc/lib/index.js'
|
|
3
|
-
import { once } from 'node:events'
|
|
4
|
-
import { platform } from 'node:os'
|
|
5
|
-
import { workerData } from 'node:worker_threads'
|
|
6
|
-
import build from 'pino-abstract-transport'
|
|
7
|
-
import { WebSocket } from 'ws'
|
|
8
|
-
import { getSocketPath } from './child-manager.js'
|
|
9
|
-
|
|
10
|
-
/* c8 ignore next 5 */
|
|
11
|
-
function logDirectError (message, error) {
|
|
12
|
-
process._rawDebug(`Logger thread for child process of application ${workerData.id} ${message}.`, {
|
|
13
|
-
error: ensureLoggableError(error)
|
|
14
|
-
})
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/* c8 ignore next 4 */
|
|
18
|
-
function handleUnhandled (type, error) {
|
|
19
|
-
logDirectError(`threw an ${type}`, error)
|
|
20
|
-
process.exit(6)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
process.on('uncaughtException', handleUnhandled.bind(null, 'uncaught exception'))
|
|
24
|
-
process.on('unhandledRejection', handleUnhandled.bind(null, 'unhandled rejection'))
|
|
25
|
-
|
|
26
|
-
export default async function () {
|
|
27
|
-
try {
|
|
28
|
-
/* c8 ignore next */
|
|
29
|
-
const protocol = platform() === 'win32' ? 'ws+unix:' : 'ws+unix://'
|
|
30
|
-
const socket = new WebSocket(`${protocol}${getSocketPath(process.env.PLT_MANAGER_ID)}`)
|
|
31
|
-
|
|
32
|
-
await once(socket, 'open')
|
|
33
|
-
|
|
34
|
-
// Do not process responses but empty the socket inbound queue
|
|
35
|
-
socket.on('message', () => {})
|
|
36
|
-
|
|
37
|
-
/* c8 ignore next 3 */
|
|
38
|
-
socket.on('error', error => {
|
|
39
|
-
logDirectError('threw a socket error', error)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
return build(
|
|
43
|
-
async function (source) {
|
|
44
|
-
for await (const obj of source) {
|
|
45
|
-
socket.send(JSON.stringify(sanitize(generateRequest('log', { logs: [obj] }))))
|
|
46
|
-
}
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
close (_, cb) {
|
|
50
|
-
socket.close()
|
|
51
|
-
cb()
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
)
|
|
55
|
-
/* c8 ignore next 3 */
|
|
56
|
-
} catch (error) {
|
|
57
|
-
logDirectError('threw a connection error', error)
|
|
58
|
-
}
|
|
59
|
-
}
|