@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 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.id = options.context.serviceId
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 ?? null
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 (this.id) {
48
- pinoOptions.name = this.id
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
- this.childManager.close(this.subprocessTerminationSignal ?? 'SIGINT')
248
- this.subprocess.kill(this.subprocessTerminationSignal ?? 'SIGINT')
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.id,
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.id,
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, generateNotification } from '@platformatic/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 (signal) {
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 { collectMetrics } from '@platformatic/metrics'
5
- import { tracingChannel } from 'node:diagnostics_channel'
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, isWindows } from './child-manager.js'
19
- import diagnosticChannel from 'node:diagnostics_channel'
20
- import { ServerResponse } from 'node:http'
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', signal => {
117
- process.kill(process.pid, signal)
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 = format === 'json'
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
- this.#logger = pino({
190
- level: 'info',
191
- name: globalThis.platformatic.id,
192
- transport: {
193
- target: new URL('./child-transport.js', import.meta.url).toString(),
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({ level: 'info', name: globalThis.platformatic.id })
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
- /* c8 ignore next 3 */
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 (opts) {
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.7.1-alpha.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/telemetry": "2.7.1-alpha.2",
27
- "@platformatic/config": "2.7.1-alpha.2",
28
- "@platformatic/metrics": "2.7.1-alpha.2",
29
- "@platformatic/itc": "2.7.1-alpha.2",
30
- "@platformatic/utils": "2.7.1-alpha.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.7.1-alpha.2.json",
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",