@platformatic/basic 2.7.0 → 2.8.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +3 -2
- package/lib/base.js +46 -14
- package/lib/worker/child-manager.js +7 -7
- package/lib/worker/child-process.js +51 -36
- package/lib/worker/child-transport.js +1 -1
- package/package.json +6 -6
- package/schema.json +1 -1
package/index.js
CHANGED
|
@@ -110,7 +110,8 @@ async function buildStackable (opts) {
|
|
|
110
110
|
const serviceRoot = relative(process.cwd(), opts.context.directory)
|
|
111
111
|
if (
|
|
112
112
|
!hadConfig &&
|
|
113
|
-
!existsSync(resolve(serviceRoot, 'platformatic.json') || existsSync(resolve(serviceRoot, 'watt.json')))
|
|
113
|
+
!existsSync(resolve(serviceRoot, 'platformatic.json') || existsSync(resolve(serviceRoot, 'watt.json'))) &&
|
|
114
|
+
opts.context.worker?.count > 1
|
|
114
115
|
) {
|
|
115
116
|
const logger = pino({
|
|
116
117
|
level: opts.context.serverConfig?.logger?.level ?? 'warn',
|
|
@@ -119,7 +120,7 @@ async function buildStackable (opts) {
|
|
|
119
120
|
|
|
120
121
|
logger.warn(
|
|
121
122
|
[
|
|
122
|
-
`Platformatic has auto-detected that service ${opts.context.serviceId} ${autodetectDescription}.\n`,
|
|
123
|
+
`Platformatic has auto-detected that service "${opts.context.serviceId}" ${autodetectDescription}.\n`,
|
|
123
124
|
`We suggest you create a platformatic.json or watt.json file in the folder ${serviceRoot} with the "$schema" `,
|
|
124
125
|
`property set to "https://schemas.platformatic.dev/${toImport}/${packageJson.version}.json".`
|
|
125
126
|
].join('')
|
package/lib/base.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { deepmerge } from '@platformatic/utils'
|
|
2
1
|
import { collectMetrics } from '@platformatic/metrics'
|
|
2
|
+
import { deepmerge, executeWithTimeout } from '@platformatic/utils'
|
|
3
3
|
import { parseCommandString } from 'execa'
|
|
4
4
|
import { spawn } from 'node:child_process'
|
|
5
5
|
import { once } from 'node:events'
|
|
6
|
-
import { workerData } from 'node:worker_threads'
|
|
7
6
|
import { existsSync } from 'node:fs'
|
|
8
|
-
import { platform } from 'node:os'
|
|
7
|
+
import { hostname, platform } from 'node:os'
|
|
9
8
|
import { pathToFileURL } from 'node:url'
|
|
9
|
+
import { workerData } from 'node:worker_threads'
|
|
10
10
|
import pino from 'pino'
|
|
11
11
|
import split2 from 'split2'
|
|
12
12
|
import { NonZeroExitCode } from './errors.js'
|
|
@@ -19,9 +19,12 @@ export class BaseStackable {
|
|
|
19
19
|
#subprocessStarted
|
|
20
20
|
|
|
21
21
|
constructor (type, version, options, root, configManager) {
|
|
22
|
+
options.context.worker ??= { count: 1, index: 0 }
|
|
23
|
+
|
|
22
24
|
this.type = type
|
|
23
25
|
this.version = version
|
|
24
|
-
this.
|
|
26
|
+
this.serviceId = options.context.serviceId
|
|
27
|
+
this.workerId = options.context.worker.count > 1 ? options.context.worker.index : undefined
|
|
25
28
|
this.telemetryConfig = options.context.telemetryConfig
|
|
26
29
|
this.options = options
|
|
27
30
|
this.root = root
|
|
@@ -39,16 +42,26 @@ export class BaseStackable {
|
|
|
39
42
|
|
|
40
43
|
// Setup the logger
|
|
41
44
|
const pinoOptions = {
|
|
42
|
-
level: this.serverConfig?.logger?.level ?? 'trace'
|
|
45
|
+
level: this.configManager.current?.logger?.level ?? this.serverConfig?.logger?.level ?? 'trace'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (this.serviceId) {
|
|
49
|
+
pinoOptions.name = this.serviceId
|
|
43
50
|
}
|
|
44
51
|
|
|
45
|
-
if (
|
|
46
|
-
pinoOptions.
|
|
52
|
+
if (typeof options.context.worker?.index !== 'undefined') {
|
|
53
|
+
pinoOptions.base = { pid: process.pid, hostname: hostname(), worker: this.workerId }
|
|
47
54
|
}
|
|
55
|
+
|
|
48
56
|
this.logger = pino(pinoOptions)
|
|
49
57
|
|
|
50
58
|
// Setup globals
|
|
51
59
|
this.registerGlobals({
|
|
60
|
+
serviceId: this.serviceId,
|
|
61
|
+
workerId: this.workerId,
|
|
62
|
+
logLevel: this.logger.level,
|
|
63
|
+
// Always use URL to avoid serialization problem in Windows
|
|
64
|
+
root: pathToFileURL(this.root).toString(),
|
|
52
65
|
setOpenapiSchema: this.setOpenapiSchema.bind(this),
|
|
53
66
|
setGraphqlSchema: this.setGraphqlSchema.bind(this),
|
|
54
67
|
setBasePath: this.setBasePath.bind(this)
|
|
@@ -241,9 +254,26 @@ export class BaseStackable {
|
|
|
241
254
|
this.#subprocessStarted = false
|
|
242
255
|
const exitPromise = once(this.subprocess, 'exit')
|
|
243
256
|
|
|
244
|
-
|
|
245
|
-
this.
|
|
257
|
+
// Attempt graceful close on the process
|
|
258
|
+
this.childManager.notify(this.clientWs, 'close')
|
|
259
|
+
|
|
260
|
+
// If the process hasn't exited in 10 seconds, kill it in the polite way
|
|
261
|
+
/* c8 ignore next 10 */
|
|
262
|
+
const res = await executeWithTimeout(exitPromise, 10000)
|
|
263
|
+
if (res === 'timeout') {
|
|
264
|
+
this.subprocess.kill(this.subprocessTerminationSignal ?? 'SIGINT')
|
|
265
|
+
|
|
266
|
+
// If the process hasn't exited in 10 seconds, kill it the hard way
|
|
267
|
+
const res = await executeWithTimeout(exitPromise, 10000)
|
|
268
|
+
if (res === 'timeout') {
|
|
269
|
+
this.subprocess.kill('SIGKILL')
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
246
273
|
await exitPromise
|
|
274
|
+
|
|
275
|
+
// Close the manager
|
|
276
|
+
this.childManager.close()
|
|
247
277
|
}
|
|
248
278
|
|
|
249
279
|
getChildManager () {
|
|
@@ -270,14 +300,16 @@ export class BaseStackable {
|
|
|
270
300
|
|
|
271
301
|
if (this.childManager && this.clientWs) {
|
|
272
302
|
await this.childManager.send(this.clientWs, 'collectMetrics', {
|
|
273
|
-
serviceId: this.
|
|
303
|
+
serviceId: this.serviceId,
|
|
304
|
+
workerId: this.workerId,
|
|
274
305
|
metricsConfig
|
|
275
306
|
})
|
|
276
307
|
return
|
|
277
308
|
}
|
|
278
309
|
|
|
279
310
|
const { registry, startHttpTimer, endHttpTimer } = await collectMetrics(
|
|
280
|
-
this.
|
|
311
|
+
this.serviceId,
|
|
312
|
+
this.workerId,
|
|
281
313
|
metricsConfig
|
|
282
314
|
)
|
|
283
315
|
|
|
@@ -294,9 +326,7 @@ export class BaseStackable {
|
|
|
294
326
|
|
|
295
327
|
if (!this.metricsRegistry) return null
|
|
296
328
|
|
|
297
|
-
return format === 'json'
|
|
298
|
-
? await this.metricsRegistry.getMetricsAsJSON()
|
|
299
|
-
: await this.metricsRegistry.metrics()
|
|
329
|
+
return format === 'json' ? await this.metricsRegistry.getMetricsAsJSON() : await this.metricsRegistry.metrics()
|
|
300
330
|
}
|
|
301
331
|
|
|
302
332
|
getMeta () {
|
|
@@ -312,6 +342,8 @@ export class BaseStackable {
|
|
|
312
342
|
|
|
313
343
|
return {
|
|
314
344
|
id: this.id,
|
|
345
|
+
serviceId: this.serviceId,
|
|
346
|
+
workerId: this.workerId,
|
|
315
347
|
// Always use URL to avoid serialization problem in Windows
|
|
316
348
|
root: pathToFileURL(this.root).toString(),
|
|
317
349
|
basePath,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ITC
|
|
1
|
+
import { ITC } from '@platformatic/itc'
|
|
2
2
|
import { createDirectory, ensureLoggableError } from '@platformatic/utils'
|
|
3
3
|
import { once } from 'node:events'
|
|
4
4
|
import { rm, writeFile } from 'node:fs/promises'
|
|
@@ -128,16 +128,11 @@ export class ChildManager extends ITC {
|
|
|
128
128
|
})
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
async close (
|
|
131
|
+
async close () {
|
|
132
132
|
if (this.#dataPath) {
|
|
133
133
|
await rm(this.#dataPath, { force: true })
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
for (const client of this.#clients) {
|
|
137
|
-
this.#currentClient = client
|
|
138
|
-
this._send(generateNotification('close', signal))
|
|
139
|
-
}
|
|
140
|
-
|
|
141
136
|
this.#server?.close()
|
|
142
137
|
super.close()
|
|
143
138
|
}
|
|
@@ -195,6 +190,11 @@ export class ChildManager extends ITC {
|
|
|
195
190
|
return super.send(name, message)
|
|
196
191
|
}
|
|
197
192
|
|
|
193
|
+
notify (client, name, message) {
|
|
194
|
+
this.#currentClient = client
|
|
195
|
+
return super.notify(name, message)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
198
|
_send (message, stringify = true) {
|
|
199
199
|
if (!this.#currentClient) {
|
|
200
200
|
this.#currentClient = this.#requests.get(message.reqId)
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { ITC } from '@platformatic/itc'
|
|
2
|
+
import { collectMetrics } from '@platformatic/metrics'
|
|
2
3
|
import { setupNodeHTTPTelemetry } from '@platformatic/telemetry'
|
|
3
4
|
import { createPinoWritable, ensureLoggableError } from '@platformatic/utils'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { once } from 'node:events'
|
|
5
|
+
import diagnosticChannel, { tracingChannel } from 'node:diagnostics_channel'
|
|
6
|
+
import { EventEmitter, once } from 'node:events'
|
|
7
7
|
import { readFile } from 'node:fs/promises'
|
|
8
|
+
import { ServerResponse } from 'node:http'
|
|
8
9
|
import { register } from 'node:module'
|
|
9
|
-
import { platform, tmpdir } from 'node:os'
|
|
10
|
+
import { hostname, platform, tmpdir } from 'node:os'
|
|
10
11
|
import { basename, resolve } from 'node:path'
|
|
11
12
|
import { fileURLToPath } from 'node:url'
|
|
12
13
|
import { isMainThread } from 'node:worker_threads'
|
|
@@ -15,9 +16,9 @@ import { getGlobalDispatcher, setGlobalDispatcher } from 'undici'
|
|
|
15
16
|
import { WebSocket } from 'ws'
|
|
16
17
|
import { exitCodes } from '../errors.js'
|
|
17
18
|
import { importFile } from '../utils.js'
|
|
18
|
-
import { getSocketPath
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
import { getSocketPath } from './child-manager.js'
|
|
20
|
+
|
|
21
|
+
const windowsNpmExecutables = ['npm-prefix.js', 'npm-cli.js']
|
|
21
22
|
|
|
22
23
|
function createInterceptor (itc) {
|
|
23
24
|
return function (dispatch) {
|
|
@@ -97,7 +98,7 @@ export class ChildProcess extends ITC {
|
|
|
97
98
|
},
|
|
98
99
|
getMetrics: (...args) => {
|
|
99
100
|
return this.#getMetrics(...args)
|
|
100
|
-
}
|
|
101
|
+
}
|
|
101
102
|
}
|
|
102
103
|
})
|
|
103
104
|
|
|
@@ -113,8 +114,11 @@ export class ChildProcess extends ITC {
|
|
|
113
114
|
this.#setupServer()
|
|
114
115
|
this.#setupInterceptors()
|
|
115
116
|
|
|
116
|
-
this.on('close',
|
|
117
|
-
|
|
117
|
+
this.on('close', () => {
|
|
118
|
+
if (!globalThis.platformatic.events.emit('close')) {
|
|
119
|
+
// No user event, just exit without errors
|
|
120
|
+
process.exit(0)
|
|
121
|
+
}
|
|
118
122
|
})
|
|
119
123
|
}
|
|
120
124
|
|
|
@@ -167,17 +171,16 @@ export class ChildProcess extends ITC {
|
|
|
167
171
|
this.#socket.close()
|
|
168
172
|
}
|
|
169
173
|
|
|
170
|
-
async #collectMetrics ({ serviceId, metricsConfig }) {
|
|
171
|
-
const { registry } = await collectMetrics(serviceId, metricsConfig)
|
|
174
|
+
async #collectMetrics ({ serviceId, workerId, metricsConfig }) {
|
|
175
|
+
const { registry } = await collectMetrics(serviceId, workerId, metricsConfig)
|
|
172
176
|
this.#metricsRegistry = registry
|
|
173
177
|
}
|
|
174
178
|
|
|
175
179
|
async #getMetrics ({ format } = {}) {
|
|
176
180
|
if (!this.#metricsRegistry) return null
|
|
177
181
|
|
|
178
|
-
const res =
|
|
179
|
-
? await this.#metricsRegistry.getMetricsAsJSON()
|
|
180
|
-
: await this.#metricsRegistry.metrics()
|
|
182
|
+
const res =
|
|
183
|
+
format === 'json' ? await this.#metricsRegistry.getMetricsAsJSON() : await this.#metricsRegistry.metrics()
|
|
181
184
|
|
|
182
185
|
return res
|
|
183
186
|
}
|
|
@@ -185,20 +188,30 @@ export class ChildProcess extends ITC {
|
|
|
185
188
|
#setupLogger () {
|
|
186
189
|
// Since this is executed by user code, make sure we only override this in the main thread
|
|
187
190
|
// The rest will be intercepted by the BaseStackable.
|
|
191
|
+
const pinoOptions = {
|
|
192
|
+
level: 'info',
|
|
193
|
+
name: globalThis.platformatic.serviceId
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (typeof globalThis.platformatic.workerId !== 'undefined') {
|
|
197
|
+
pinoOptions.base = {
|
|
198
|
+
pid: process.pid,
|
|
199
|
+
hostname: hostname(),
|
|
200
|
+
worker: parseInt(globalThis.platformatic.workerId)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
188
204
|
if (isMainThread) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
options: { id: globalThis.platformatic.id }
|
|
195
|
-
}
|
|
196
|
-
})
|
|
205
|
+
pinoOptions.transport = {
|
|
206
|
+
target: new URL('./child-transport.js', import.meta.url).toString()
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this.#logger = pino(pinoOptions)
|
|
197
210
|
|
|
198
211
|
Reflect.defineProperty(process, 'stdout', { value: createPinoWritable(this.#logger, 'info') })
|
|
199
212
|
Reflect.defineProperty(process, 'stderr', { value: createPinoWritable(this.#logger, 'error', true) })
|
|
200
213
|
} else {
|
|
201
|
-
this.#logger = pino(
|
|
214
|
+
this.#logger = pino(pinoOptions)
|
|
202
215
|
}
|
|
203
216
|
}
|
|
204
217
|
|
|
@@ -262,11 +275,13 @@ export class ChildProcess extends ITC {
|
|
|
262
275
|
}
|
|
263
276
|
|
|
264
277
|
#setupHandlers () {
|
|
278
|
+
const errorLabel =
|
|
279
|
+
typeof globalThis.platformatic.workerId !== 'undefined'
|
|
280
|
+
? `worker ${globalThis.platformatic.workerId} of the service "${globalThis.platformatic.serviceId}"`
|
|
281
|
+
: `service "${globalThis.platformatic.serviceId}"`
|
|
282
|
+
|
|
265
283
|
function handleUnhandled (type, err) {
|
|
266
|
-
this.#logger.error(
|
|
267
|
-
{ err: ensureLoggableError(err) },
|
|
268
|
-
`Child process for service ${globalThis.platformatic.id} threw an ${type}.`
|
|
269
|
-
)
|
|
284
|
+
this.#logger.error({ err: ensureLoggableError(err) }, `Child process for the ${errorLabel} threw an ${type}.`)
|
|
270
285
|
|
|
271
286
|
// Give some time to the logger and ITC notifications to land before shutting down
|
|
272
287
|
setTimeout(() => process.exit(exitCodes.PROCESS_UNHANDLED_ERROR), 100)
|
|
@@ -304,10 +319,7 @@ function stripBasePath (basePath) {
|
|
|
304
319
|
|
|
305
320
|
if (headers) {
|
|
306
321
|
for (const key in headers) {
|
|
307
|
-
if (
|
|
308
|
-
key.toLowerCase() === 'location' &&
|
|
309
|
-
!headers[key].startsWith(basePath)
|
|
310
|
-
) {
|
|
322
|
+
if (key.toLowerCase() === 'location' && !headers[key].startsWith(basePath)) {
|
|
311
323
|
headers[key] = basePath + headers[key]
|
|
312
324
|
}
|
|
313
325
|
}
|
|
@@ -328,10 +340,16 @@ function stripBasePath (basePath) {
|
|
|
328
340
|
}
|
|
329
341
|
|
|
330
342
|
async function main () {
|
|
343
|
+
const executable = basename(process.argv[1] ?? '')
|
|
344
|
+
if (!isMainThread || windowsNpmExecutables.includes(executable)) {
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
|
|
331
348
|
const dataPath = resolve(tmpdir(), 'platformatic', 'runtimes', `${process.env.PLT_MANAGER_ID}.json`)
|
|
332
349
|
const { data, loader, scripts } = JSON.parse(await readFile(dataPath))
|
|
333
350
|
|
|
334
351
|
globalThis.platformatic = data
|
|
352
|
+
globalThis.platformatic.events = new EventEmitter()
|
|
335
353
|
|
|
336
354
|
if (data.root && isMainThread) {
|
|
337
355
|
process.chdir(fileURLToPath(data.root))
|
|
@@ -348,7 +366,4 @@ async function main () {
|
|
|
348
366
|
globalThis[Symbol.for('plt.children.itc')] = new ChildProcess()
|
|
349
367
|
}
|
|
350
368
|
|
|
351
|
-
|
|
352
|
-
if (!isWindows || basename(process.argv.at(-1)) !== 'npm-prefix.js') {
|
|
353
|
-
await main()
|
|
354
|
-
}
|
|
369
|
+
await main()
|
|
@@ -23,7 +23,7 @@ function handleUnhandled (type, error) {
|
|
|
23
23
|
process.on('uncaughtException', handleUnhandled.bind(null, 'uncaught exception'))
|
|
24
24
|
process.on('unhandledRejection', handleUnhandled.bind(null, 'unhandled rejection'))
|
|
25
25
|
|
|
26
|
-
export default async function (
|
|
26
|
+
export default async function () {
|
|
27
27
|
try {
|
|
28
28
|
/* c8 ignore next */
|
|
29
29
|
const protocol = platform() === 'win32' ? 'ws+unix:' : 'ws+unix://'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/basic",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0-alpha.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
"split2": "^4.2.0",
|
|
24
24
|
"undici": "^6.19.5",
|
|
25
25
|
"ws": "^8.18.0",
|
|
26
|
-
"@platformatic/config": "2.
|
|
27
|
-
"@platformatic/itc": "2.
|
|
28
|
-
"@platformatic/
|
|
29
|
-
"@platformatic/
|
|
30
|
-
"@platformatic/utils": "2.
|
|
26
|
+
"@platformatic/config": "2.8.0-alpha.1",
|
|
27
|
+
"@platformatic/itc": "2.8.0-alpha.1",
|
|
28
|
+
"@platformatic/metrics": "2.8.0-alpha.1",
|
|
29
|
+
"@platformatic/telemetry": "2.8.0-alpha.1",
|
|
30
|
+
"@platformatic/utils": "2.8.0-alpha.1"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"borp": "^0.18.0",
|
package/schema.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://schemas.platformatic.dev/@platformatic/basic/2.
|
|
2
|
+
"$id": "https://schemas.platformatic.dev/@platformatic/basic/2.8.0-alpha.1.json",
|
|
3
3
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
4
|
"title": "Platformatic Stackable",
|
|
5
5
|
"type": "object",
|