@platformatic/basic 2.7.1-alpha.2 → 2.8.0-alpha.2
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 +50 -23
- 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,29 +1,30 @@
|
|
|
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'
|
|
13
13
|
import { cleanBasePath } from './utils.js'
|
|
14
14
|
import { ChildManager } from './worker/child-manager.js'
|
|
15
15
|
|
|
16
|
-
const kITC = Symbol.for('plt.runtime.itc')
|
|
17
|
-
|
|
18
16
|
export class BaseStackable {
|
|
19
17
|
childManager
|
|
20
18
|
#subprocess
|
|
21
19
|
#subprocessStarted
|
|
22
20
|
|
|
23
21
|
constructor (type, version, options, root, configManager) {
|
|
22
|
+
options.context.worker ??= { count: 1, index: 0 }
|
|
23
|
+
|
|
24
24
|
this.type = type
|
|
25
25
|
this.version = version
|
|
26
|
-
this.
|
|
26
|
+
this.serviceId = options.context.serviceId
|
|
27
|
+
this.workerId = options.context.worker.count > 1 ? options.context.worker.index : undefined
|
|
27
28
|
this.telemetryConfig = options.context.telemetryConfig
|
|
28
29
|
this.options = options
|
|
29
30
|
this.root = root
|
|
@@ -37,24 +38,33 @@ export class BaseStackable {
|
|
|
37
38
|
this.startHttpTimer = null
|
|
38
39
|
this.endHttpTimer = null
|
|
39
40
|
this.clientWs = null
|
|
40
|
-
this.runtimeConfig = workerData?.config ??
|
|
41
|
+
this.runtimeConfig = deepmerge(options.context.runtimeConfig ?? {}, workerData?.config ?? {})
|
|
41
42
|
|
|
42
43
|
// Setup the logger
|
|
43
44
|
const pinoOptions = {
|
|
44
|
-
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
|
|
45
50
|
}
|
|
46
51
|
|
|
47
|
-
if (
|
|
48
|
-
pinoOptions.
|
|
52
|
+
if (typeof options.context.worker?.index !== 'undefined') {
|
|
53
|
+
pinoOptions.base = { pid: process.pid, hostname: hostname(), worker: this.workerId }
|
|
49
54
|
}
|
|
55
|
+
|
|
50
56
|
this.logger = pino(pinoOptions)
|
|
51
57
|
|
|
52
58
|
// Setup globals
|
|
53
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(),
|
|
54
65
|
setOpenapiSchema: this.setOpenapiSchema.bind(this),
|
|
55
66
|
setGraphqlSchema: this.setGraphqlSchema.bind(this),
|
|
56
|
-
setBasePath: this.setBasePath.bind(this)
|
|
57
|
-
invalidateHttpCache: this.#invalidateHttpCache.bind(this)
|
|
67
|
+
setBasePath: this.setBasePath.bind(this)
|
|
58
68
|
})
|
|
59
69
|
}
|
|
60
70
|
|
|
@@ -241,12 +251,31 @@ export class BaseStackable {
|
|
|
241
251
|
}
|
|
242
252
|
|
|
243
253
|
async stopCommand () {
|
|
254
|
+
const exitTimeout = this.runtimeConfig.gracefulShutdown.runtime
|
|
255
|
+
|
|
244
256
|
this.#subprocessStarted = false
|
|
245
257
|
const exitPromise = once(this.subprocess, 'exit')
|
|
246
258
|
|
|
247
|
-
|
|
248
|
-
this.
|
|
259
|
+
// Attempt graceful close on the process
|
|
260
|
+
this.childManager.notify(this.clientWs, 'close')
|
|
261
|
+
|
|
262
|
+
// If the process hasn't exited in X seconds, kill it in the polite way
|
|
263
|
+
/* c8 ignore next 10 */
|
|
264
|
+
const res = await executeWithTimeout(exitPromise, exitTimeout)
|
|
265
|
+
if (res === 'timeout') {
|
|
266
|
+
this.subprocess.kill(this.subprocessTerminationSignal ?? 'SIGINT')
|
|
267
|
+
|
|
268
|
+
// If the process hasn't exited in X seconds, kill it the hard way
|
|
269
|
+
const res = await executeWithTimeout(exitPromise, exitTimeout)
|
|
270
|
+
if (res === 'timeout') {
|
|
271
|
+
this.subprocess.kill('SIGKILL')
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
249
275
|
await exitPromise
|
|
276
|
+
|
|
277
|
+
// Close the manager
|
|
278
|
+
this.childManager.close()
|
|
250
279
|
}
|
|
251
280
|
|
|
252
281
|
getChildManager () {
|
|
@@ -273,14 +302,16 @@ export class BaseStackable {
|
|
|
273
302
|
|
|
274
303
|
if (this.childManager && this.clientWs) {
|
|
275
304
|
await this.childManager.send(this.clientWs, 'collectMetrics', {
|
|
276
|
-
serviceId: this.
|
|
305
|
+
serviceId: this.serviceId,
|
|
306
|
+
workerId: this.workerId,
|
|
277
307
|
metricsConfig
|
|
278
308
|
})
|
|
279
309
|
return
|
|
280
310
|
}
|
|
281
311
|
|
|
282
312
|
const { registry, startHttpTimer, endHttpTimer } = await collectMetrics(
|
|
283
|
-
this.
|
|
313
|
+
this.serviceId,
|
|
314
|
+
this.workerId,
|
|
284
315
|
metricsConfig
|
|
285
316
|
)
|
|
286
317
|
|
|
@@ -297,13 +328,7 @@ export class BaseStackable {
|
|
|
297
328
|
|
|
298
329
|
if (!this.metricsRegistry) return null
|
|
299
330
|
|
|
300
|
-
return format === 'json'
|
|
301
|
-
? await this.metricsRegistry.getMetricsAsJSON()
|
|
302
|
-
: await this.metricsRegistry.metrics()
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
async #invalidateHttpCache (opts = {}) {
|
|
306
|
-
await globalThis[kITC].send('invalidateHttpCache', opts)
|
|
331
|
+
return format === 'json' ? await this.metricsRegistry.getMetricsAsJSON() : await this.metricsRegistry.metrics()
|
|
307
332
|
}
|
|
308
333
|
|
|
309
334
|
getMeta () {
|
|
@@ -319,6 +344,8 @@ export class BaseStackable {
|
|
|
319
344
|
|
|
320
345
|
return {
|
|
321
346
|
id: this.id,
|
|
347
|
+
serviceId: this.serviceId,
|
|
348
|
+
workerId: this.workerId,
|
|
322
349
|
// Always use URL to avoid serialization problem in Windows
|
|
323
350
|
root: pathToFileURL(this.root).toString(),
|
|
324
351
|
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.2",
|
|
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/
|
|
27
|
-
"@platformatic/
|
|
28
|
-
"@platformatic/
|
|
29
|
-
"@platformatic/
|
|
30
|
-
"@platformatic/utils": "2.
|
|
26
|
+
"@platformatic/config": "2.8.0-alpha.2",
|
|
27
|
+
"@platformatic/itc": "2.8.0-alpha.2",
|
|
28
|
+
"@platformatic/telemetry": "2.8.0-alpha.2",
|
|
29
|
+
"@platformatic/metrics": "2.8.0-alpha.2",
|
|
30
|
+
"@platformatic/utils": "2.8.0-alpha.2"
|
|
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.2.json",
|
|
3
3
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
4
|
"title": "Platformatic Stackable",
|
|
5
5
|
"type": "object",
|