@platformatic/runtime 3.54.0 → 3.56.0
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 +8 -0
- package/index.d.ts +32 -2
- package/index.js +1 -0
- package/lib/config.js +3 -11
- package/lib/errors.js +4 -0
- package/lib/generator.js +39 -25
- package/lib/logger.js +22 -20
- package/lib/loopback-messaging.js +78 -0
- package/lib/management-api.js +3 -3
- package/lib/runtime.js +54 -9
- package/lib/utils.js +12 -3
- package/lib/worker/controller.js +45 -13
- package/lib/worker/health-signals.js +4 -2
- package/lib/worker/http-cache.js +5 -5
- package/lib/worker/interceptors.js +79 -16
- package/lib/worker/itc.js +16 -6
- package/lib/worker/main.js +57 -15
- package/lib/worker/management.js +3 -2
- package/lib/worker/messaging.js +23 -15
- package/lib/worker/shared-context.js +5 -3
- package/lib/worker/symbols.js +1 -2
- package/package.json +17 -16
- package/schema.json +125 -1
package/lib/worker/controller.js
CHANGED
|
@@ -5,8 +5,19 @@ import {
|
|
|
5
5
|
kHandledError,
|
|
6
6
|
listRecognizedConfigurationFiles,
|
|
7
7
|
loadConfiguration,
|
|
8
|
-
loadConfigurationModule
|
|
8
|
+
loadConfigurationModule,
|
|
9
|
+
mirrorGlobalDispatcherForBuiltinFetch
|
|
9
10
|
} from '@platformatic/foundation'
|
|
11
|
+
import {
|
|
12
|
+
getLogger,
|
|
13
|
+
getOnActiveResourcesEventLoop,
|
|
14
|
+
getOnHttpStatsConnected,
|
|
15
|
+
getOnHttpStatsFree,
|
|
16
|
+
getOnHttpStatsPending,
|
|
17
|
+
getOnHttpStatsQueued,
|
|
18
|
+
getOnHttpStatsRunning,
|
|
19
|
+
getOnHttpStatsSize
|
|
20
|
+
} from '@platformatic/globals'
|
|
10
21
|
import debounce from 'debounce'
|
|
11
22
|
import { EventEmitter } from 'node:events'
|
|
12
23
|
import { existsSync } from 'node:fs'
|
|
@@ -16,6 +27,7 @@ import { workerData } from 'node:worker_threads'
|
|
|
16
27
|
import { getGlobalDispatcher, setGlobalDispatcher } from 'undici'
|
|
17
28
|
import { ApplicationAlreadyStartedError, RuntimeNotStartedError } from '../errors.js'
|
|
18
29
|
import { getApplicationUrl } from '../utils.js'
|
|
30
|
+
import { markAsPlatformaticDispatcher, refreshGlobalDispatcher } from './interceptors.js'
|
|
19
31
|
|
|
20
32
|
function fetchApplicationUrl (application, key) {
|
|
21
33
|
if (!key.endsWith('_URL') || !application.id) {
|
|
@@ -28,7 +40,8 @@ function fetchApplicationUrl (application, key) {
|
|
|
28
40
|
function handleUnhandled (app, type, err) {
|
|
29
41
|
const label = `worker ${workerData.worker.index} of the application "${workerData.applicationConfig.id}"`
|
|
30
42
|
|
|
31
|
-
|
|
43
|
+
const logger = getLogger()
|
|
44
|
+
logger.error({ err: ensureLoggableError(err) }, `The ${label} threw an ${type}.`)
|
|
32
45
|
|
|
33
46
|
executeWithTimeout(app?.stop(), 1000)
|
|
34
47
|
.catch()
|
|
@@ -141,7 +154,8 @@ export class Controller extends EventEmitter {
|
|
|
141
154
|
}
|
|
142
155
|
} catch (err) {
|
|
143
156
|
if (err.validationErrors) {
|
|
144
|
-
|
|
157
|
+
const logger = getLogger()
|
|
158
|
+
logger.error(
|
|
145
159
|
{ err: ensureLoggableError(err) },
|
|
146
160
|
'The application threw a validation error.'
|
|
147
161
|
)
|
|
@@ -192,6 +206,9 @@ export class Controller extends EventEmitter {
|
|
|
192
206
|
|
|
193
207
|
try {
|
|
194
208
|
await this.capability.start({ listen })
|
|
209
|
+
if (refreshGlobalDispatcher()) {
|
|
210
|
+
this.#updateDispatcher()
|
|
211
|
+
}
|
|
195
212
|
this.#listening = listen
|
|
196
213
|
/* c8 ignore next 5 */
|
|
197
214
|
} catch (err) {
|
|
@@ -243,18 +260,29 @@ export class Controller extends EventEmitter {
|
|
|
243
260
|
|
|
244
261
|
async getMetrics ({ format }) {
|
|
245
262
|
const dispatcher = getGlobalDispatcher()
|
|
246
|
-
|
|
263
|
+
const onHttpStatsFree = getOnHttpStatsFree({ throwOnMissing: false })
|
|
264
|
+
|
|
265
|
+
if (onHttpStatsFree && dispatcher?.stats) {
|
|
266
|
+
const onHttpStatsConnected = getOnHttpStatsConnected()
|
|
267
|
+
const onHttpStatsPending = getOnHttpStatsPending()
|
|
268
|
+
const onHttpStatsQueued = getOnHttpStatsQueued()
|
|
269
|
+
const onHttpStatsRunning = getOnHttpStatsRunning()
|
|
270
|
+
const onHttpStatsSize = getOnHttpStatsSize()
|
|
271
|
+
|
|
247
272
|
for (const url in dispatcher.stats) {
|
|
248
273
|
const { free, connected, pending, queued, running, size } = dispatcher.stats[url]
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
274
|
+
onHttpStatsFree(url, free || 0)
|
|
275
|
+
onHttpStatsConnected(url, connected || 0)
|
|
276
|
+
onHttpStatsPending(url, pending || 0)
|
|
277
|
+
onHttpStatsQueued(url, queued || 0)
|
|
278
|
+
onHttpStatsRunning(url, running || 0)
|
|
279
|
+
onHttpStatsSize(url, size || 0)
|
|
255
280
|
}
|
|
256
281
|
}
|
|
257
|
-
|
|
282
|
+
const onActiveResourcesEventLoop = getOnActiveResourcesEventLoop({ throwOnMissing: false })
|
|
283
|
+
if (onActiveResourcesEventLoop) {
|
|
284
|
+
onActiveResourcesEventLoop(getActiveResourcesInfo().length)
|
|
285
|
+
}
|
|
258
286
|
return this.capability.getMetrics({ format })
|
|
259
287
|
}
|
|
260
288
|
|
|
@@ -299,7 +327,8 @@ export class Controller extends EventEmitter {
|
|
|
299
327
|
}
|
|
300
328
|
|
|
301
329
|
#logAndThrow (err) {
|
|
302
|
-
|
|
330
|
+
const logger = getLogger()
|
|
331
|
+
logger.error(
|
|
303
332
|
{ err: ensureLoggableError(err) },
|
|
304
333
|
err[kHandledError] ? err.message : 'The application threw an error.'
|
|
305
334
|
)
|
|
@@ -325,7 +354,9 @@ export class Controller extends EventEmitter {
|
|
|
325
354
|
|
|
326
355
|
const dispatcher = getGlobalDispatcher().compose(interceptor)
|
|
327
356
|
|
|
357
|
+
markAsPlatformaticDispatcher(dispatcher)
|
|
328
358
|
setGlobalDispatcher(dispatcher)
|
|
359
|
+
mirrorGlobalDispatcherForBuiltinFetch(dispatcher)
|
|
329
360
|
}
|
|
330
361
|
|
|
331
362
|
#setupHandlers () {
|
|
@@ -334,7 +365,8 @@ export class Controller extends EventEmitter {
|
|
|
334
365
|
|
|
335
366
|
process.on('newListener', event => {
|
|
336
367
|
if (event === 'uncaughtException' || event === 'unhandledRejection') {
|
|
337
|
-
|
|
368
|
+
const logger = getLogger()
|
|
369
|
+
logger.warn(
|
|
338
370
|
`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.`
|
|
339
371
|
)
|
|
340
372
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getITC, updateGlobals } from '@platformatic/globals'
|
|
1
2
|
import {
|
|
2
3
|
HealthSignalMustBeObjectError,
|
|
3
4
|
HealthSignalTypeMustBeStringError
|
|
@@ -60,7 +61,8 @@ export function initHealthSignalsApi (options = {}) {
|
|
|
60
61
|
isSending = false
|
|
61
62
|
try {
|
|
62
63
|
const signals = queue.getAll()
|
|
63
|
-
|
|
64
|
+
const itc = getITC()
|
|
65
|
+
await itc.send('sendHealthSignals', {
|
|
64
66
|
workerId,
|
|
65
67
|
signals
|
|
66
68
|
})
|
|
@@ -76,5 +78,5 @@ export function initHealthSignalsApi (options = {}) {
|
|
|
76
78
|
return promise
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
|
|
81
|
+
updateGlobals({ sendHealthSignal })
|
|
80
82
|
}
|
package/lib/worker/http-cache.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { getClientSpansAls, getITC } from '@platformatic/globals'
|
|
1
2
|
import { randomUUID } from 'node:crypto'
|
|
2
3
|
import { Readable, Writable } from 'node:stream'
|
|
3
4
|
import { interceptors } from 'undici'
|
|
4
|
-
import { kITC } from './symbols.js'
|
|
5
5
|
|
|
6
6
|
const kCacheIdHeader = Symbol('cacheIdHeader')
|
|
7
7
|
const CACHE_ID_HEADER = 'x-plt-http-cache-id'
|
|
@@ -28,7 +28,7 @@ export class RemoteCacheStore {
|
|
|
28
28
|
this.#logger.error(err, 'Error in onRequest http cache hook')
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
const itc =
|
|
31
|
+
const itc = getITC()
|
|
32
32
|
if (!itc) return
|
|
33
33
|
|
|
34
34
|
const sanitizedRequest = this.#sanitizeRequest(request)
|
|
@@ -68,7 +68,7 @@ export class RemoteCacheStore {
|
|
|
68
68
|
value.headers = { ...value.headers, [CACHE_ID_HEADER]: cacheEntryId }
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
const itc =
|
|
71
|
+
const itc = getITC()
|
|
72
72
|
if (!itc) throw new Error('Cannot write to cache without an ITC instance')
|
|
73
73
|
|
|
74
74
|
const acc = []
|
|
@@ -96,7 +96,7 @@ export class RemoteCacheStore {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
delete (request) {
|
|
99
|
-
const itc =
|
|
99
|
+
const itc = getITC()
|
|
100
100
|
if (!itc) throw new Error('Cannot delete from cache without an ITC instance')
|
|
101
101
|
|
|
102
102
|
request = this.#sanitizeRequest(request)
|
|
@@ -120,7 +120,7 @@ export function httpCacheInterceptor (interceptorOpts) {
|
|
|
120
120
|
|
|
121
121
|
// AsyncLocalStorage that contains a client http request span
|
|
122
122
|
// Exists only when the nodejs capability telemetry is enabled
|
|
123
|
-
const clientSpansAls =
|
|
123
|
+
const clientSpansAls = getClientSpansAls({ throwOnMissing: false })
|
|
124
124
|
|
|
125
125
|
return originalDispatch => {
|
|
126
126
|
const dispatch = (opts, handler) => {
|
|
@@ -1,13 +1,46 @@
|
|
|
1
|
+
import { getGlobalDispatcherFromKnownUndiciSymbols, mirrorGlobalDispatcherForBuiltinFetch } from '@platformatic/foundation'
|
|
2
|
+
import {
|
|
3
|
+
getInterceptors,
|
|
4
|
+
getLogger,
|
|
5
|
+
getOnHttpCacheHit,
|
|
6
|
+
getOnHttpCacheMiss,
|
|
7
|
+
getOnHttpCacheRequest
|
|
8
|
+
} from '@platformatic/globals'
|
|
1
9
|
import { createTelemetryThreadInterceptorHooks } from '@platformatic/telemetry'
|
|
2
10
|
import { createRequire } from 'node:module'
|
|
3
11
|
import { join } from 'node:path'
|
|
4
12
|
import { pathToFileURL } from 'node:url'
|
|
5
13
|
import { parentPort, workerData } from 'node:worker_threads'
|
|
6
|
-
import { Agent, Client, Pool
|
|
14
|
+
import { Agent, Client, Pool } from 'undici'
|
|
7
15
|
import { wire } from 'undici-thread-interceptor'
|
|
8
16
|
import { createChannelCreationHook } from '../policies.js'
|
|
9
17
|
import { RemoteCacheStore, httpCacheInterceptor } from './http-cache.js'
|
|
10
|
-
|
|
18
|
+
|
|
19
|
+
const kPlatformaticGlobalDispatcher = Symbol.for('platformatic.undici.globalDispatcher')
|
|
20
|
+
|
|
21
|
+
let composeRuntimeDispatcher
|
|
22
|
+
|
|
23
|
+
export function markAsPlatformaticDispatcher (dispatcher) {
|
|
24
|
+
Object.defineProperty(dispatcher, kPlatformaticGlobalDispatcher, {
|
|
25
|
+
value: true,
|
|
26
|
+
enumerable: false,
|
|
27
|
+
configurable: true
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function refreshGlobalDispatcher () {
|
|
32
|
+
if (!composeRuntimeDispatcher) {
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const dispatcher = getGlobalDispatcherFromKnownUndiciSymbols()
|
|
37
|
+
if (!dispatcher || dispatcher[kPlatformaticGlobalDispatcher]) {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
composeRuntimeDispatcher(dispatcher)
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
11
44
|
|
|
12
45
|
export async function setDispatcher (runtimeConfig) {
|
|
13
46
|
const threadDispatcher = createThreadInterceptor(runtimeConfig)
|
|
@@ -27,15 +60,39 @@ export async function setDispatcher (runtimeConfig) {
|
|
|
27
60
|
|
|
28
61
|
const dispatcherOpts = await getDispatcherOpts(runtimeConfig.undici)
|
|
29
62
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
63
|
+
function installDispatcher (baseDispatcher) {
|
|
64
|
+
const dispatcher = baseDispatcher.compose(
|
|
65
|
+
[threadInterceptor, ...userInterceptors, cacheInterceptor].filter(Boolean)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
markAsPlatformaticDispatcher(dispatcher)
|
|
69
|
+
mirrorGlobalDispatcherForBuiltinFetch(dispatcher, createLegacyDispatcher(dispatcher))
|
|
70
|
+
return dispatcher
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
composeRuntimeDispatcher = installDispatcher
|
|
74
|
+
installDispatcher(new Agent(dispatcherOpts))
|
|
33
75
|
|
|
34
76
|
return { threadDispatcher }
|
|
35
77
|
}
|
|
36
78
|
|
|
79
|
+
function createLegacyDispatcher (dispatcher) {
|
|
80
|
+
const legacyDispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]
|
|
81
|
+
const currentDispatcher = globalThis[Symbol.for('undici.globalDispatcher.2')]
|
|
82
|
+
|
|
83
|
+
if (legacyDispatcher && legacyDispatcher !== currentDispatcher && legacyDispatcher.constructor?.name === 'Dispatcher1Wrapper') {
|
|
84
|
+
try {
|
|
85
|
+
return new legacyDispatcher.constructor(dispatcher)
|
|
86
|
+
} catch {
|
|
87
|
+
return dispatcher
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return dispatcher
|
|
92
|
+
}
|
|
93
|
+
|
|
37
94
|
export async function updateUndiciInterceptors (undiciConfig) {
|
|
38
|
-
const updatableInterceptors =
|
|
95
|
+
const updatableInterceptors = getInterceptors()
|
|
39
96
|
if (!updatableInterceptors) return
|
|
40
97
|
|
|
41
98
|
if (Array.isArray(undiciConfig?.interceptors)) {
|
|
@@ -95,11 +152,7 @@ async function loadInterceptors (_require, interceptorsConfigs, key) {
|
|
|
95
152
|
}
|
|
96
153
|
|
|
97
154
|
async function loadInterceptor (_require, interceptorConfig, key) {
|
|
98
|
-
|
|
99
|
-
if (!updatableInterceptors) {
|
|
100
|
-
updatableInterceptors = {}
|
|
101
|
-
globalThis[kInterceptors] = updatableInterceptors
|
|
102
|
-
}
|
|
155
|
+
const updatableInterceptors = getInterceptors()
|
|
103
156
|
|
|
104
157
|
const { module, options } = interceptorConfig
|
|
105
158
|
|
|
@@ -196,24 +249,34 @@ function parseOrigins (origins) {
|
|
|
196
249
|
|
|
197
250
|
function createHttpCacheInterceptor (runtimeConfig) {
|
|
198
251
|
const httpCache = runtimeConfig.httpCache
|
|
252
|
+
|
|
199
253
|
const cacheInterceptor = httpCacheInterceptor({
|
|
200
254
|
store: new RemoteCacheStore({
|
|
201
255
|
onRequest: opts => {
|
|
202
|
-
|
|
256
|
+
const onHttpCacheRequest = getOnHttpCacheRequest({ throwOnMissing: false })
|
|
257
|
+
if (onHttpCacheRequest) {
|
|
258
|
+
onHttpCacheRequest(opts)
|
|
259
|
+
}
|
|
203
260
|
},
|
|
204
261
|
onCacheHit: opts => {
|
|
205
|
-
|
|
262
|
+
const onHttpCacheHit = getOnHttpCacheHit({ throwOnMissing: false })
|
|
263
|
+
if (onHttpCacheHit) {
|
|
264
|
+
onHttpCacheHit(opts)
|
|
265
|
+
}
|
|
206
266
|
},
|
|
207
267
|
onCacheMiss: opts => {
|
|
208
|
-
|
|
268
|
+
const onHttpCacheMiss = getOnHttpCacheMiss({ throwOnMissing: false })
|
|
269
|
+
if (onHttpCacheMiss) {
|
|
270
|
+
onHttpCacheMiss(opts)
|
|
271
|
+
}
|
|
209
272
|
},
|
|
210
|
-
logger:
|
|
273
|
+
logger: getLogger()
|
|
211
274
|
}),
|
|
212
275
|
methods: httpCache.methods ?? ['GET', 'HEAD'],
|
|
213
276
|
origins: parseOrigins(httpCache.origins),
|
|
214
277
|
cacheByDefault: httpCache.cacheByDefault,
|
|
215
278
|
type: httpCache.type,
|
|
216
|
-
logger:
|
|
279
|
+
logger: getLogger()
|
|
217
280
|
})
|
|
218
281
|
return cacheInterceptor
|
|
219
282
|
}
|
package/lib/worker/itc.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ensureLoggableError, executeInParallel, executeWithTimeout, kTimeout } from '@platformatic/foundation'
|
|
2
|
+
import { getEvents, getLogger, getMessaging, updateGlobals } from '@platformatic/globals'
|
|
2
3
|
import { initializeITCTelemetry, ITC } from '@platformatic/itc'
|
|
3
4
|
import { Unpromise } from '@watchable/unpromise'
|
|
4
5
|
import { once } from 'node:events'
|
|
@@ -150,10 +151,10 @@ export async function waitEventFromITC (worker, event) {
|
|
|
150
151
|
export async function setupITC (controller, application, dispatcher, sharedContext) {
|
|
151
152
|
await initializeITCTelemetry()
|
|
152
153
|
|
|
153
|
-
const logger =
|
|
154
|
+
const logger = getLogger()
|
|
154
155
|
const messaging = new MessagingITC(controller.applicationConfig.id, workerData.config, logger)
|
|
155
156
|
|
|
156
|
-
|
|
157
|
+
updateGlobals({
|
|
157
158
|
messaging: {
|
|
158
159
|
handle: messaging.handle.bind(messaging),
|
|
159
160
|
send: messaging.send.bind(messaging),
|
|
@@ -172,7 +173,8 @@ export async function setupITC (controller, application, dispatcher, sharedConte
|
|
|
172
173
|
await once(controller, 'start')
|
|
173
174
|
} else {
|
|
174
175
|
// This gives a chance to a capability to perform custom logic
|
|
175
|
-
|
|
176
|
+
const events = getEvents()
|
|
177
|
+
events.emit('start')
|
|
176
178
|
|
|
177
179
|
try {
|
|
178
180
|
await controller.start()
|
|
@@ -205,7 +207,8 @@ export async function setupITC (controller, application, dispatcher, sharedConte
|
|
|
205
207
|
|
|
206
208
|
if (force || status.startsWith('start')) {
|
|
207
209
|
// This gives a chance to a capability to perform custom logic
|
|
208
|
-
|
|
210
|
+
const events = getEvents()
|
|
211
|
+
events.emit('stop')
|
|
209
212
|
|
|
210
213
|
await controller.stop(force, dependents)
|
|
211
214
|
}
|
|
@@ -419,9 +422,16 @@ export async function setupITC (controller, application, dispatcher, sharedConte
|
|
|
419
422
|
// For service-based capabilities, expose the Fastify app
|
|
420
423
|
replServer.context.app = controller.capability?.getApplication?.()
|
|
421
424
|
replServer.context.capability = controller.capability
|
|
422
|
-
replServer.context.platformatic =
|
|
425
|
+
replServer.context.platformatic = {
|
|
426
|
+
capability: controller.capability,
|
|
427
|
+
config: controller.config,
|
|
428
|
+
events: getEvents(),
|
|
429
|
+
itc,
|
|
430
|
+
logger: getLogger(),
|
|
431
|
+
messaging: getMessaging()
|
|
432
|
+
}
|
|
423
433
|
replServer.context.config = controller.applicationConfig
|
|
424
|
-
replServer.context.logger =
|
|
434
|
+
replServer.context.logger = getLogger()
|
|
425
435
|
|
|
426
436
|
replServer.on('exit', () => {
|
|
427
437
|
port.postMessage({ type: 'exit' })
|
package/lib/worker/main.js
CHANGED
|
@@ -3,15 +3,19 @@ import {
|
|
|
3
3
|
buildPinoTimestamp,
|
|
4
4
|
disablePinoDirectWrite,
|
|
5
5
|
ensureLoggableError,
|
|
6
|
-
getPrivateSymbol
|
|
6
|
+
getPrivateSymbol,
|
|
7
|
+
parseMemorySize
|
|
7
8
|
} from '@platformatic/foundation'
|
|
9
|
+
import { getITC, getLogger, updateGlobals } from '@platformatic/globals'
|
|
8
10
|
import { addPinoInstrumentation } from '@platformatic/telemetry'
|
|
11
|
+
import { Buffer } from 'node:buffer'
|
|
9
12
|
import { subscribe } from 'node:diagnostics_channel'
|
|
10
13
|
import { EventEmitter } from 'node:events'
|
|
11
14
|
import { ServerResponse } from 'node:http'
|
|
12
15
|
import inspector from 'node:inspector'
|
|
13
16
|
import { hostname } from 'node:os'
|
|
14
17
|
import { join, resolve } from 'node:path'
|
|
18
|
+
import { setDefaultHighWaterMark } from 'node:stream'
|
|
15
19
|
import { pathToFileURL } from 'node:url'
|
|
16
20
|
import { threadId, workerData } from 'node:worker_threads'
|
|
17
21
|
import pino from 'pino'
|
|
@@ -22,11 +26,12 @@ import { initHealthSignalsApi } from './health-signals.js'
|
|
|
22
26
|
import { setDispatcher } from './interceptors.js'
|
|
23
27
|
import { setupITC } from './itc.js'
|
|
24
28
|
import { SharedContext } from './shared-context.js'
|
|
25
|
-
import {
|
|
29
|
+
import { kStderrMarker } from './symbols.js'
|
|
26
30
|
|
|
27
31
|
function handlePreInitializationUnhandled (type, err) {
|
|
28
32
|
const label = `worker ${workerData.worker.index} of the application "${workerData.applicationConfig.id}"`
|
|
29
|
-
|
|
33
|
+
const logger = getLogger()
|
|
34
|
+
logger.error(
|
|
30
35
|
{ err: ensureLoggableError(err) },
|
|
31
36
|
`The ${label} threw an ${type} before initialization.`
|
|
32
37
|
)
|
|
@@ -35,7 +40,8 @@ function handlePreInitializationUnhandled (type, err) {
|
|
|
35
40
|
|
|
36
41
|
class ForwardingEventEmitter extends EventEmitter {
|
|
37
42
|
emitAndNotify (event, ...args) {
|
|
38
|
-
|
|
43
|
+
const itc = getITC()
|
|
44
|
+
itc.notify('event', { event, payload: args })
|
|
39
45
|
return this.emit(event, ...args)
|
|
40
46
|
}
|
|
41
47
|
}
|
|
@@ -113,6 +119,36 @@ async function performPreloading (...sources) {
|
|
|
113
119
|
}
|
|
114
120
|
}
|
|
115
121
|
|
|
122
|
+
function resolveHealthSize (runtimeConfig, applicationConfig, field, logger) {
|
|
123
|
+
const raw = applicationConfig.health?.[field] ?? runtimeConfig.health?.[field]
|
|
124
|
+
if (raw === undefined) {
|
|
125
|
+
return undefined
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const size = typeof raw === 'string' ? parseMemorySize(raw) : raw
|
|
129
|
+
|
|
130
|
+
if (!Number.isInteger(size) || size < 0) {
|
|
131
|
+
logger.warn({ [field]: raw }, `Invalid health.${field}, ignoring`)
|
|
132
|
+
return undefined
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return size
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function setupBufferPool (runtimeConfig, applicationConfig, logger) {
|
|
139
|
+
const size = resolveHealthSize(runtimeConfig, applicationConfig, 'bufferPoolSize', logger)
|
|
140
|
+
if (size !== undefined) {
|
|
141
|
+
Buffer.poolSize = size
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function setupDefaultHighWaterMark (runtimeConfig, applicationConfig, logger) {
|
|
146
|
+
const size = resolveHealthSize(runtimeConfig, applicationConfig, 'defaultHighWaterMark', logger)
|
|
147
|
+
if (size !== undefined) {
|
|
148
|
+
setDefaultHighWaterMark(false, size)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
116
152
|
// Enable compile cache if configured (Node.js 22.1.0+)
|
|
117
153
|
async function setupCompileCache (runtimeConfig, applicationConfig, logger) {
|
|
118
154
|
// Normalize boolean shorthand: true -> { enabled: true }
|
|
@@ -168,8 +204,9 @@ async function main () {
|
|
|
168
204
|
const cleanup = setupHandlers()
|
|
169
205
|
|
|
170
206
|
installUndiciGlobals(globalThis)
|
|
171
|
-
|
|
172
|
-
|
|
207
|
+
updateGlobals({
|
|
208
|
+
runtimeId: threadId,
|
|
209
|
+
interceptors: {},
|
|
173
210
|
logger: createLogger(),
|
|
174
211
|
events: new ForwardingEventEmitter()
|
|
175
212
|
})
|
|
@@ -177,8 +214,11 @@ async function main () {
|
|
|
177
214
|
const runtimeConfig = workerData.config
|
|
178
215
|
const applicationConfig = workerData.applicationConfig
|
|
179
216
|
|
|
217
|
+
setupBufferPool(runtimeConfig, applicationConfig, getLogger())
|
|
218
|
+
setupDefaultHighWaterMark(runtimeConfig, applicationConfig, getLogger())
|
|
219
|
+
|
|
180
220
|
// Enable compile cache early before loading user modules
|
|
181
|
-
await setupCompileCache(runtimeConfig, applicationConfig,
|
|
221
|
+
await setupCompileCache(runtimeConfig, applicationConfig, getLogger())
|
|
182
222
|
|
|
183
223
|
await performPreloading(runtimeConfig, applicationConfig)
|
|
184
224
|
|
|
@@ -190,7 +230,8 @@ async function main () {
|
|
|
190
230
|
envfile = resolve(workerData.applicationConfig.path, '.env')
|
|
191
231
|
}
|
|
192
232
|
|
|
193
|
-
|
|
233
|
+
const logger = getLogger()
|
|
234
|
+
logger.debug({ envfile }, 'Loading envfile...')
|
|
194
235
|
|
|
195
236
|
try {
|
|
196
237
|
process.loadEnvFile(envfile)
|
|
@@ -265,15 +306,16 @@ async function main () {
|
|
|
265
306
|
|
|
266
307
|
const sharedContext = new SharedContext()
|
|
267
308
|
// Limit the amount of methods a user can call
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
309
|
+
updateGlobals({
|
|
310
|
+
sharedContext: {
|
|
311
|
+
get: () => sharedContext.get(),
|
|
312
|
+
update: (...args) => sharedContext.update(...args)
|
|
313
|
+
}
|
|
314
|
+
})
|
|
272
315
|
|
|
273
316
|
// Setup interaction with parent port
|
|
274
317
|
const itc = await setupITC(controller, applicationConfig, threadDispatcher, sharedContext)
|
|
275
|
-
|
|
276
|
-
globalThis.platformatic.itc = itc
|
|
318
|
+
updateGlobals({ itc })
|
|
277
319
|
|
|
278
320
|
// Setup management client for privileged applications
|
|
279
321
|
if (applicationConfig.management) {
|
|
@@ -286,7 +328,7 @@ async function main () {
|
|
|
286
328
|
const ops = typeof applicationConfig.management === 'object'
|
|
287
329
|
? applicationConfig.management.operations
|
|
288
330
|
: undefined
|
|
289
|
-
|
|
331
|
+
updateGlobals({ management: new ManagementClient(ops) })
|
|
290
332
|
}
|
|
291
333
|
}
|
|
292
334
|
|
package/lib/worker/management.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getITC } from '@platformatic/globals'
|
|
2
2
|
|
|
3
3
|
export class ManagementClient {
|
|
4
4
|
#allowedOperations
|
|
@@ -14,7 +14,8 @@ export class ManagementClient {
|
|
|
14
14
|
throw new Error(`Operation "${operation}" is not allowed`)
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
const itc = getITC()
|
|
18
|
+
return itc.send('management:' + operation, data)
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
getRuntimeStatus () {
|
package/lib/worker/messaging.js
CHANGED
|
@@ -10,9 +10,10 @@ import {
|
|
|
10
10
|
startOutgoingMessagingSpanSync,
|
|
11
11
|
traceIncomingMessagingHandler
|
|
12
12
|
} from '@platformatic/itc'
|
|
13
|
+
import { getITC } from '@platformatic/globals'
|
|
13
14
|
import { MessagingError } from '../errors.js'
|
|
14
15
|
import { RoundRobinMap } from './round-robin-map.js'
|
|
15
|
-
import {
|
|
16
|
+
import { kWorkersBroadcast } from './symbols.js'
|
|
16
17
|
|
|
17
18
|
const kPendingResponses = Symbol('plt.messaging.pendingResponses')
|
|
18
19
|
|
|
@@ -76,7 +77,7 @@ export class MessagingITC extends ITC {
|
|
|
76
77
|
|
|
77
78
|
try {
|
|
78
79
|
// Get the next worker for the application
|
|
79
|
-
const worker = this
|
|
80
|
+
const worker = this._getNextWorker(application)
|
|
80
81
|
|
|
81
82
|
if (!worker) {
|
|
82
83
|
throw new MessagingError(application, 'No workers available')
|
|
@@ -85,8 +86,9 @@ export class MessagingITC extends ITC {
|
|
|
85
86
|
if (!worker.channel) {
|
|
86
87
|
// Use twice the value here as a fallback measure. The target handler in the main thread is forwarding
|
|
87
88
|
// the request to the worker, using executeWithTimeout with the user set timeout value.
|
|
89
|
+
const itc = getITC()
|
|
88
90
|
const channel = await executeWithTimeout(
|
|
89
|
-
|
|
91
|
+
itc.send('getWorkerMessagingChannel', {
|
|
90
92
|
id: this.#id,
|
|
91
93
|
application: worker.application,
|
|
92
94
|
worker: worker.worker
|
|
@@ -100,10 +102,7 @@ export class MessagingITC extends ITC {
|
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
worker.channel = channel
|
|
103
|
-
this
|
|
104
|
-
|
|
105
|
-
channel[kPendingResponses] = new Map()
|
|
106
|
-
channel.on('close', this.#handlePendingResponse.bind(this, channel))
|
|
105
|
+
this._setupChannel(channel)
|
|
107
106
|
}
|
|
108
107
|
|
|
109
108
|
const context = { ...options }
|
|
@@ -164,13 +163,11 @@ export class MessagingITC extends ITC {
|
|
|
164
163
|
|
|
165
164
|
async addSource (channel) {
|
|
166
165
|
this.#sources.add(channel)
|
|
167
|
-
this
|
|
166
|
+
this._setupChannel(channel)
|
|
167
|
+
}
|
|
168
168
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
channel.on('close', () => {
|
|
172
|
-
this.#sources.delete(channel)
|
|
173
|
-
})
|
|
169
|
+
_getNextWorker (application) {
|
|
170
|
+
return this.#workers.next(application)
|
|
174
171
|
}
|
|
175
172
|
|
|
176
173
|
_send (request, context) {
|
|
@@ -181,7 +178,7 @@ export class MessagingITC extends ITC {
|
|
|
181
178
|
channel[kPendingResponses].set(request.reqId, { application, request })
|
|
182
179
|
}
|
|
183
180
|
|
|
184
|
-
channel.postMessage(sanitize(request, transferList),
|
|
181
|
+
channel.postMessage(sanitize(request, transferList), transferList)
|
|
185
182
|
}
|
|
186
183
|
|
|
187
184
|
_createClosePromise () {
|
|
@@ -209,11 +206,22 @@ export class MessagingITC extends ITC {
|
|
|
209
206
|
this.#sources.clear()
|
|
210
207
|
}
|
|
211
208
|
|
|
212
|
-
|
|
209
|
+
_setupChannel (channel) {
|
|
210
|
+
if (channel[kPendingResponses]) {
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
channel[kPendingResponses] = new Map()
|
|
215
|
+
|
|
213
216
|
// Setup the message for processing
|
|
214
217
|
channel.on('message', event => {
|
|
215
218
|
this.#listener(event, { channel, message: event })
|
|
216
219
|
})
|
|
220
|
+
|
|
221
|
+
channel.on('close', () => {
|
|
222
|
+
this.#sources.delete(channel)
|
|
223
|
+
this.#handlePendingResponse(channel)
|
|
224
|
+
})
|
|
217
225
|
}
|
|
218
226
|
|
|
219
227
|
#updateWorkers (event) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getITC } from '@platformatic/globals'
|
|
2
2
|
|
|
3
3
|
export class SharedContext {
|
|
4
4
|
constructor () {
|
|
@@ -6,12 +6,14 @@ export class SharedContext {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
update (context, options = {}) {
|
|
9
|
-
|
|
9
|
+
const itc = getITC()
|
|
10
|
+
return itc.send('updateSharedContext', { ...options, context })
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
get () {
|
|
13
14
|
if (this.sharedContext === null) {
|
|
14
|
-
|
|
15
|
+
const itc = getITC()
|
|
16
|
+
this.sharedContext = itc.send('getSharedContext')
|
|
15
17
|
}
|
|
16
18
|
return this.sharedContext
|
|
17
19
|
}
|