@platformatic/runtime 3.63.0-alpha.3 → 3.64.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 +4 -0
- package/index.d.ts +179 -8
- package/lib/errors.js +30 -0
- package/lib/management-api.js +16 -3
- package/lib/management-handlers.js +3 -3
- package/lib/prom-server.js +81 -8
- package/lib/runtime.js +664 -57
- package/lib/scaling-algorithm.js +3 -3
- package/lib/worker/controller.js +4 -2
- package/lib/worker/itc.js +37 -18
- package/lib/worker/main.js +14 -4
- package/lib/worker/management.js +2 -2
- package/lib/worker/messaging.js +20 -7
- package/lib/worker-scaler.js +20 -9
- package/package.json +17 -17
- package/schema.json +13 -2
package/config.d.ts
CHANGED
|
@@ -696,6 +696,10 @@ export type PlatformaticRuntimeConfig = {
|
|
|
696
696
|
[k: string]: string;
|
|
697
697
|
};
|
|
698
698
|
envfile?: string;
|
|
699
|
+
/**
|
|
700
|
+
* When set to true, the configuration loading fails if a {PLT_*} placeholder references an environment variable which is not set. When set to "warn", a warning listing the missing variables is logged but the placeholders are still replaced with an empty string. Defaults to false.
|
|
701
|
+
*/
|
|
702
|
+
strictEnv?: boolean | string;
|
|
699
703
|
sourceMaps?: boolean;
|
|
700
704
|
nodeModulesSourceMaps?: string[];
|
|
701
705
|
scheduler?: {
|
package/index.d.ts
CHANGED
|
@@ -2,13 +2,14 @@ import { FastifyError } from '@fastify/error'
|
|
|
2
2
|
import { Configuration, ConfigurationOptions, logFatalError, parseArgs } from '@platformatic/foundation'
|
|
3
3
|
import { BaseGenerator } from '@platformatic/generators'
|
|
4
4
|
import { PlatformaticGlobal } from '@platformatic/globals'
|
|
5
|
+
import * as PromClient from '@platformatic/prom-client'
|
|
5
6
|
import { JSONSchemaType } from 'ajv'
|
|
6
7
|
import * as colorette from 'colorette'
|
|
7
8
|
import { EventEmitter } from 'node:events'
|
|
8
9
|
import { Logger } from 'pino'
|
|
9
10
|
import { PlatformaticRuntimeConfig } from './config.js'
|
|
10
11
|
|
|
11
|
-
export type RuntimeConfiguration =
|
|
12
|
+
export type RuntimeConfiguration = Configuration<PlatformaticRuntimeConfig>
|
|
12
13
|
|
|
13
14
|
export type ApplicationCommandContext = {
|
|
14
15
|
colorette: typeof colorette
|
|
@@ -57,6 +58,19 @@ export namespace errors {
|
|
|
57
58
|
export const InvalidExtensionError: (path: string) => FastifyError
|
|
58
59
|
export const ReservedITCHandlerNameError: (name: string) => FastifyError
|
|
59
60
|
export const DuplicateITCHandlerNameError: (name: string) => FastifyError
|
|
61
|
+
export const MetricFamilyCollisionError: (
|
|
62
|
+
extension: string,
|
|
63
|
+
metricFamily: string,
|
|
64
|
+
otherSource: string
|
|
65
|
+
) => FastifyError
|
|
66
|
+
export const DuplicateExtensionHealthCheckError: (kind: string, name: string, extension: string) => FastifyError
|
|
67
|
+
export const DuplicateExtensionHealthRouteError: (
|
|
68
|
+
extension: string,
|
|
69
|
+
method: string,
|
|
70
|
+
url: string,
|
|
71
|
+
error: string
|
|
72
|
+
) => FastifyError
|
|
73
|
+
export const ExtensionHealthRoutesUnavailableError: () => FastifyError
|
|
60
74
|
export const LastProfileTimeoutError: (id: string) => FastifyError
|
|
61
75
|
export const SchedulerJobNotFoundError: (name: string) => FastifyError
|
|
62
76
|
export const DuplicateSchedulerJobError: (name: string) => FastifyError
|
|
@@ -111,6 +125,28 @@ export interface ApplicationDetails {
|
|
|
111
125
|
url?: string | null
|
|
112
126
|
}
|
|
113
127
|
|
|
128
|
+
export interface ApplicationsTopology {
|
|
129
|
+
entrypoint: string
|
|
130
|
+
production: boolean
|
|
131
|
+
applications: ApplicationDetails[]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface WorkerDetails {
|
|
135
|
+
application: string
|
|
136
|
+
worker: string
|
|
137
|
+
status: string
|
|
138
|
+
thread: number
|
|
139
|
+
raw?: unknown
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface SharedContextUpdateOptions {
|
|
143
|
+
overwrite?: boolean
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface RuntimeSharedContextUpdateOptions extends SharedContextUpdateOptions {
|
|
147
|
+
context?: object
|
|
148
|
+
}
|
|
149
|
+
|
|
114
150
|
export interface SchedulerJobBase {
|
|
115
151
|
name: string
|
|
116
152
|
cron: string
|
|
@@ -172,15 +208,15 @@ export declare class ManagementClient {
|
|
|
172
208
|
getRuntimeConfig (): Promise<Record<string, unknown>>
|
|
173
209
|
getRuntimeEnv (): Promise<Record<string, string>>
|
|
174
210
|
getApplicationsIds (): Promise<string[]>
|
|
175
|
-
getApplications (): Promise<
|
|
176
|
-
getWorkers (): Promise<Record<string,
|
|
211
|
+
getApplications (): Promise<ApplicationsTopology>
|
|
212
|
+
getWorkers (): Promise<Record<string, WorkerDetails>>
|
|
177
213
|
getApplicationDetails (id: string): Promise<ApplicationDetails>
|
|
178
214
|
getApplicationConfig (id: string): Promise<Record<string, unknown>>
|
|
179
215
|
getApplicationEnv (id: string): Promise<Record<string, string>>
|
|
180
216
|
getApplicationOpenapiSchema (id: string): Promise<unknown>
|
|
181
217
|
getApplicationGraphqlSchema (id: string): Promise<unknown>
|
|
182
218
|
getMetrics (format?: string): Promise<{ metrics: unknown }>
|
|
183
|
-
|
|
219
|
+
getSchedulerJobs (): Promise<SchedulerJob[]>
|
|
184
220
|
pauseSchedulerJob (name: string): Promise<SchedulerJob>
|
|
185
221
|
resumeSchedulerJob (name: string): Promise<SchedulerJob>
|
|
186
222
|
runSchedulerJob (name: string): Promise<SchedulerRunResult>
|
|
@@ -205,15 +241,78 @@ export interface RuntimeExtensionITC {
|
|
|
205
241
|
notify (target: string, name: string, payload?: unknown): Promise<void>
|
|
206
242
|
}
|
|
207
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Shared context facade exposed to main-thread runtime extensions.
|
|
246
|
+
*
|
|
247
|
+
* Mirrors the worker-side `sharedContext` API from `@platformatic/globals`:
|
|
248
|
+
* - `get()` returns the current snapshot (a plain object). On the main thread
|
|
249
|
+
* this is always synchronous; the `Promise` variant exists for parity with
|
|
250
|
+
* workers where the first read may be asynchronous.
|
|
251
|
+
* - `update(update, options?)` merges `update` into the current context by
|
|
252
|
+
* default. Pass `{ overwrite: true }` to replace it. Updates are broadcast
|
|
253
|
+
* to all running workers exactly like worker-originated updates.
|
|
254
|
+
*/
|
|
255
|
+
export interface RuntimeExtensionSharedContext {
|
|
256
|
+
get (): object | Promise<object>
|
|
257
|
+
update (update: object, options?: SharedContextUpdateOptions): Promise<void>
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export interface RuntimeExtensionMetrics {
|
|
261
|
+
client: typeof PromClient
|
|
262
|
+
registry: PromClient.Registry
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export type ExtensionHealthCheckResult =
|
|
266
|
+
| boolean
|
|
267
|
+
| {
|
|
268
|
+
status: boolean
|
|
269
|
+
statusCode?: number
|
|
270
|
+
body?: string | object
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export type ExtensionHealthCheck = () => ExtensionHealthCheckResult | Promise<ExtensionHealthCheckResult>
|
|
274
|
+
|
|
275
|
+
export interface RuntimeExtensionHealth {
|
|
276
|
+
/**
|
|
277
|
+
* Registers a readiness check that participates in `/ready`.
|
|
278
|
+
* Readiness-only failures do not fail `/status` (liveness).
|
|
279
|
+
* Returns an unregister function.
|
|
280
|
+
*/
|
|
281
|
+
registerReadinessCheck (name: string, check: ExtensionHealthCheck): () => void
|
|
282
|
+
/**
|
|
283
|
+
* Registers a liveness check that participates in `/status`.
|
|
284
|
+
* Returns an unregister function.
|
|
285
|
+
*/
|
|
286
|
+
registerLivenessCheck (name: string, check: ExtensionHealthCheck): () => void
|
|
287
|
+
/**
|
|
288
|
+
* Registers a Fastify plugin on the health probes server (shared with metrics
|
|
289
|
+
* when they use the same address). Routes are registered before the server
|
|
290
|
+
* starts listening. Returns an unregister function that disables the routes.
|
|
291
|
+
*/
|
|
292
|
+
registerRoutes (plugin: (instance: any, opts: any) => unknown | Promise<unknown>): () => void
|
|
293
|
+
}
|
|
294
|
+
|
|
208
295
|
export interface RuntimeExtensionContext {
|
|
209
296
|
runtime: Runtime
|
|
210
297
|
itc: RuntimeExtensionITC
|
|
211
298
|
logger: Logger
|
|
212
299
|
options: Record<string, unknown>
|
|
213
300
|
root: string
|
|
301
|
+
sharedContext: RuntimeExtensionSharedContext
|
|
302
|
+
/**
|
|
303
|
+
* Per-extension Prometheus client and registry. Metrics registered here are
|
|
304
|
+
* collected once by `Runtime.getMetrics()`, the management metrics API, and
|
|
305
|
+
* the existing `/metrics` endpoint. Runtime does not invent a worker ID or
|
|
306
|
+
* application ID for these main-thread metrics; only configured static
|
|
307
|
+
* `metrics.labels` (excluding the application label name) are applied.
|
|
308
|
+
*/
|
|
309
|
+
metrics: RuntimeExtensionMetrics
|
|
310
|
+
health: RuntimeExtensionHealth
|
|
214
311
|
}
|
|
215
312
|
|
|
216
313
|
export interface RuntimeExtensionInstance {
|
|
314
|
+
start?: () => void | Promise<void>
|
|
315
|
+
stop?: () => void | Promise<void>
|
|
217
316
|
close?: () => void | Promise<void>
|
|
218
317
|
}
|
|
219
318
|
|
|
@@ -232,18 +331,76 @@ export declare class Runtime extends EventEmitter {
|
|
|
232
331
|
getRuntimeStatus (): string
|
|
233
332
|
getRuntimeMetadata (): Promise<RuntimeMetadata>
|
|
234
333
|
getRuntimeEnv (): Record<string, string>
|
|
334
|
+
// `includeMeta: true` returns the full merged Configuration object (which
|
|
335
|
+
// carries the kMetadata symbol key: `{ root, path, env, module }`), not a
|
|
336
|
+
// plain Record; the argument-invariant overload below covers every other
|
|
337
|
+
// call.
|
|
338
|
+
getRuntimeConfig (includeMeta: true): RuntimeConfiguration
|
|
235
339
|
getRuntimeConfig (includeMeta?: boolean): Record<string, unknown>
|
|
236
340
|
getApplicationsIds (): string[]
|
|
341
|
+
/**
|
|
342
|
+
* Returns topology for every configured application.
|
|
343
|
+
* When `allowUnloaded` is `true`, applications without running workers are
|
|
344
|
+
* reported as `{ id, status: 'stopped' }` instead of throwing.
|
|
345
|
+
*/
|
|
346
|
+
getApplications (allowUnloaded?: boolean): Promise<ApplicationsTopology>
|
|
347
|
+
getWorkers (includeRaw?: boolean): Promise<Record<string, WorkerDetails>>
|
|
348
|
+
/**
|
|
349
|
+
* Returns details for a single application.
|
|
350
|
+
* When `allowUnloaded` is `true` and the application has no running workers,
|
|
351
|
+
* returns `{ id, status: 'stopped' }` instead of throwing.
|
|
352
|
+
*/
|
|
237
353
|
getApplicationDetails (id: string, allowUnloaded?: boolean): Promise<ApplicationDetails>
|
|
354
|
+
/**
|
|
355
|
+
* Returns the resolved configuration of an application worker.
|
|
356
|
+
* When `ensureStarted` is `true` (default), throws `PLT_RUNTIME_APPLICATION_NOT_STARTED`
|
|
357
|
+
* if the application is not running. When no worker exists for a known application,
|
|
358
|
+
* throws `PLT_RUNTIME_WORKER_NOT_FOUND`. Unknown applications throw
|
|
359
|
+
* `PLT_RUNTIME_APPLICATION_NOT_FOUND`.
|
|
360
|
+
*/
|
|
361
|
+
getApplicationConfig (id: string, ensureStarted?: boolean): Promise<Record<string, unknown>>
|
|
362
|
+
/**
|
|
363
|
+
* Returns the effective environment of an application worker (`process.env`
|
|
364
|
+
* merged with the capability env).
|
|
365
|
+
* When `ensureStarted` is `true` (default), throws `PLT_RUNTIME_APPLICATION_NOT_STARTED`
|
|
366
|
+
* if the application is not running. When no worker exists for a known application
|
|
367
|
+
* (stopped/unloaded), throws `PLT_RUNTIME_WORKER_NOT_FOUND`. Unknown applications
|
|
368
|
+
* throw `PLT_RUNTIME_APPLICATION_NOT_FOUND`.
|
|
369
|
+
*/
|
|
370
|
+
getApplicationEnv (id: string, ensureStarted?: boolean): Promise<Record<string, string>>
|
|
371
|
+
getApplicationOpenapiSchema (id: string): Promise<unknown>
|
|
372
|
+
getApplicationGraphqlSchema (id: string): Promise<unknown>
|
|
373
|
+
getMetrics (format?: string): Promise<{ metrics: unknown }>
|
|
374
|
+
/**
|
|
375
|
+
* Returns the current shared context. Synchronous on the main thread.
|
|
376
|
+
* Do not mutate it in place — use `updateSharedContext()` so changes are
|
|
377
|
+
* broadcast to workers.
|
|
378
|
+
*/
|
|
379
|
+
getSharedContext (): object
|
|
380
|
+
/**
|
|
381
|
+
* Merges `options.context` into the shared context (or replaces it when
|
|
382
|
+
* `options.overwrite` is `true`) and broadcasts the result to every running
|
|
383
|
+
* worker. Returns the updated snapshot. Broadcast failures are logged and do
|
|
384
|
+
* not reject the promise.
|
|
385
|
+
*/
|
|
386
|
+
updateSharedContext (options?: RuntimeSharedContextUpdateOptions): Promise<object>
|
|
238
387
|
startApplication (id: string, silent?: boolean): Promise<void>
|
|
239
388
|
stopApplication (id: string, silent?: boolean): Promise<void>
|
|
240
389
|
restartApplication (id: string): Promise<void>
|
|
241
390
|
addApplications (applications: unknown[], start?: boolean): Promise<ApplicationDetails[]>
|
|
242
391
|
removeApplications (applications: string[], silent?: boolean): Promise<ApplicationDetails[]>
|
|
392
|
+
startApplicationProfiling (id: string, options: Record<string, unknown> & { allWorkers: true }, ensureStarted?: boolean): Promise<{ workers: number[] }>
|
|
243
393
|
startApplicationProfiling (id: string, options?: Record<string, unknown>, ensureStarted?: boolean): Promise<void>
|
|
394
|
+
stopApplicationProfiling (id: string, options: Record<string, unknown> & { allWorkers: true }, ensureStarted?: boolean): Promise<Array<{ workerIndex: number, profile: Buffer }>>
|
|
395
|
+
stopApplicationProfiling (id: string, options: Record<string, unknown> & { includeSampleCount: true }, ensureStarted?: boolean): Promise<{ profile: Buffer, sampleCount: number }>
|
|
244
396
|
stopApplicationProfiling (id: string, options?: Record<string, unknown>, ensureStarted?: boolean): Promise<Buffer>
|
|
245
|
-
getApplicationLastProfile (id: string, options?: Record<string, unknown>, ensureStarted?: boolean): Promise<{ profile: Buffer, timestamp: number | null, preserved: boolean }>
|
|
246
|
-
|
|
397
|
+
getApplicationLastProfile (id: string, options?: Record<string, unknown>, ensureStarted?: boolean): Promise<{ profile: Buffer, timestamp: number | null, sampleCount: number | null, preserved: boolean }>
|
|
398
|
+
// Patches one application's resolved configuration through the same
|
|
399
|
+
// per-application JSON-Patch mechanism the runtime already applies during
|
|
400
|
+
// create()/start().
|
|
401
|
+
setApplicationConfigPatch (applicationId: string, patch: Array<Record<string, unknown>>): void
|
|
402
|
+
removeApplicationConfigPatch (applicationId: string): void
|
|
403
|
+
getSchedulerJobs (): SchedulerJob[]
|
|
247
404
|
pauseSchedulerJob (name: string): Promise<SchedulerJob>
|
|
248
405
|
resumeSchedulerJob (name: string): Promise<SchedulerJob>
|
|
249
406
|
runSchedulerJob (name: string): Promise<SchedulerRunResult>
|
|
@@ -264,13 +421,27 @@ export declare function loadConfiguration (
|
|
|
264
421
|
context?: ConfigurationOptions
|
|
265
422
|
): Promise<RuntimeConfiguration>
|
|
266
423
|
|
|
424
|
+
// create() additionally accepts `setupSignals`, a Runtime-only option (it is
|
|
425
|
+
// never read outside this package) that controls whether SIGUSR2/close-with-grace
|
|
426
|
+
// handlers are installed.
|
|
427
|
+
export interface RuntimeCreateContext extends ConfigurationOptions<PlatformaticRuntimeConfig> {
|
|
428
|
+
setupSignals?: boolean
|
|
429
|
+
}
|
|
430
|
+
|
|
267
431
|
export function create (
|
|
268
432
|
root: string,
|
|
269
433
|
source?: string | PlatformaticRuntimeConfig,
|
|
270
|
-
context?:
|
|
434
|
+
context?: RuntimeCreateContext
|
|
271
435
|
): Promise<Runtime>
|
|
272
436
|
|
|
273
|
-
|
|
437
|
+
// The runtime's actual per-application preparation (worker defaults, paths,
|
|
438
|
+
// telemetry hooks, capability resolution) also takes the resolved worker
|
|
439
|
+
// count as a third argument, and is itself async.
|
|
440
|
+
export declare function prepareApplication (
|
|
441
|
+
config: RuntimeConfiguration,
|
|
442
|
+
application: object,
|
|
443
|
+
workers?: PlatformaticRuntimeConfig['workers']
|
|
444
|
+
): Promise<object>
|
|
274
445
|
|
|
275
446
|
export declare function transform (
|
|
276
447
|
config: RuntimeConfiguration,
|
package/lib/errors.js
CHANGED
|
@@ -166,6 +166,16 @@ export const FailedToLoadExtensionError = createError(
|
|
|
166
166
|
'Failed to load the extension "%s": %s'
|
|
167
167
|
)
|
|
168
168
|
|
|
169
|
+
export const FailedToStartExtensionError = createError(
|
|
170
|
+
`${ERROR_PREFIX}_FAILED_TO_START_EXTENSION`,
|
|
171
|
+
'Failed to start the extension "%s": %s'
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
export const FailedToStopExtensionError = createError(
|
|
175
|
+
`${ERROR_PREFIX}_FAILED_TO_STOP_EXTENSION`,
|
|
176
|
+
'Failed to stop the extension "%s": %s'
|
|
177
|
+
)
|
|
178
|
+
|
|
169
179
|
export const InvalidExtensionError = createError(
|
|
170
180
|
`${ERROR_PREFIX}_INVALID_EXTENSION`,
|
|
171
181
|
'The extension "%s" must export a setup function as its default export'
|
|
@@ -181,6 +191,26 @@ export const DuplicateITCHandlerNameError = createError(
|
|
|
181
191
|
'The ITC command "%s" has already been registered'
|
|
182
192
|
)
|
|
183
193
|
|
|
194
|
+
export const MetricFamilyCollisionError = createError(
|
|
195
|
+
`${ERROR_PREFIX}_METRIC_FAMILY_COLLISION`,
|
|
196
|
+
'Extension "%s" registered metric family "%s" which collides with %s'
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
export const DuplicateExtensionHealthCheckError = createError(
|
|
200
|
+
`${ERROR_PREFIX}_DUPLICATE_EXTENSION_HEALTH_CHECK`,
|
|
201
|
+
'The extension health %s check "%s" has already been registered by "%s"'
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
export const DuplicateExtensionHealthRouteError = createError(
|
|
205
|
+
`${ERROR_PREFIX}_DUPLICATE_EXTENSION_HEALTH_ROUTE`,
|
|
206
|
+
'The extension "%s" failed to register health route %s %s: %s'
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
export const ExtensionHealthRoutesUnavailableError = createError(
|
|
210
|
+
`${ERROR_PREFIX}_EXTENSION_HEALTH_ROUTES_UNAVAILABLE`,
|
|
211
|
+
'Extensions registered health routes but no health probes server is available'
|
|
212
|
+
)
|
|
213
|
+
|
|
184
214
|
export const FailedToSendHealthSignalsError = createError(
|
|
185
215
|
`${ERROR_PREFIX}_FAILED_TO_SEND_HEALTH_SIGNALS`,
|
|
186
216
|
'Cannot send health signals from application "%s": %s'
|
package/lib/management-api.js
CHANGED
|
@@ -80,7 +80,7 @@ export async function managementApiPlugin (app, opts) {
|
|
|
80
80
|
})
|
|
81
81
|
|
|
82
82
|
app.get('/scheduler', async () => {
|
|
83
|
-
const jobs = await runtime.
|
|
83
|
+
const jobs = await runtime.getSchedulerJobs()
|
|
84
84
|
return { jobs }
|
|
85
85
|
})
|
|
86
86
|
|
|
@@ -220,8 +220,8 @@ export async function managementApiPlugin (app, opts) {
|
|
|
220
220
|
app.log.debug('start profiling', { id })
|
|
221
221
|
|
|
222
222
|
const options = request.body || {}
|
|
223
|
-
await runtime.startApplicationProfiling(id, options)
|
|
224
|
-
reply.code(200).send({})
|
|
223
|
+
const result = await runtime.startApplicationProfiling(id, options)
|
|
224
|
+
reply.code(200).send(result ?? {})
|
|
225
225
|
})
|
|
226
226
|
|
|
227
227
|
app.post('/applications/:id/pprof/stop', async (request, reply) => {
|
|
@@ -230,6 +230,19 @@ export async function managementApiPlugin (app, opts) {
|
|
|
230
230
|
|
|
231
231
|
const options = request.body || {}
|
|
232
232
|
const profileData = await runtime.stopApplicationProfiling(id, options)
|
|
233
|
+
|
|
234
|
+
// With allWorkers there is one profile per worker: return them as JSON
|
|
235
|
+
// with base64 encoded payloads since a single binary body cannot carry
|
|
236
|
+
// multiple profiles.
|
|
237
|
+
if (options.allWorkers && Array.isArray(profileData)) {
|
|
238
|
+
const profiles = profileData.map(({ workerIndex, profile }) => ({
|
|
239
|
+
workerIndex,
|
|
240
|
+
profile: Buffer.from(profile).toString('base64')
|
|
241
|
+
}))
|
|
242
|
+
|
|
243
|
+
return { profiles }
|
|
244
|
+
}
|
|
245
|
+
|
|
233
246
|
reply.type('application/octet-stream').code(200).send(profileData)
|
|
234
247
|
})
|
|
235
248
|
|
|
@@ -19,7 +19,7 @@ const allOperations = [
|
|
|
19
19
|
'getApplicationOpenapiSchema',
|
|
20
20
|
'getApplicationGraphqlSchema',
|
|
21
21
|
'getMetrics',
|
|
22
|
-
'
|
|
22
|
+
'getSchedulerJobs',
|
|
23
23
|
'pauseSchedulerJob',
|
|
24
24
|
'resumeSchedulerJob',
|
|
25
25
|
'runSchedulerJob',
|
|
@@ -100,8 +100,8 @@ export function createManagementHandlers (runtime, allowedOperations) {
|
|
|
100
100
|
return runtime.getMetrics(format)
|
|
101
101
|
})
|
|
102
102
|
|
|
103
|
-
register('
|
|
104
|
-
return runtime.
|
|
103
|
+
register('getSchedulerJobs', async () => {
|
|
104
|
+
return runtime.getSchedulerJobs()
|
|
105
105
|
})
|
|
106
106
|
|
|
107
107
|
// Write operations
|
package/lib/prom-server.js
CHANGED
|
@@ -4,6 +4,7 @@ import { loadModule, sanitizeHTTPSOptions } from '@platformatic/foundation'
|
|
|
4
4
|
import fastify from 'fastify'
|
|
5
5
|
import { createRequire } from 'node:module'
|
|
6
6
|
import { resolve } from 'node:path'
|
|
7
|
+
import { DuplicateExtensionHealthRouteError } from './errors.js'
|
|
7
8
|
|
|
8
9
|
const DEFAULT_HOSTNAME = '0.0.0.0'
|
|
9
10
|
const DEFAULT_PORT = 9090
|
|
@@ -19,7 +20,7 @@ const DEFAULT_LIVENESS_SUCCESS_BODY = 'OK'
|
|
|
19
20
|
const DEFAULT_LIVENESS_FAIL_STATUS_CODE = 500
|
|
20
21
|
const DEFAULT_LIVENESS_FAIL_BODY = 'ERR'
|
|
21
22
|
|
|
22
|
-
async function
|
|
23
|
+
async function checkWorkerReadiness (runtime) {
|
|
23
24
|
const workers = await runtime.getWorkers()
|
|
24
25
|
const applications = await runtime.getApplicationsIds()
|
|
25
26
|
|
|
@@ -52,18 +53,39 @@ async function checkReadiness (runtime) {
|
|
|
52
53
|
return { response, status }
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
async function checkReadiness (runtime) {
|
|
57
|
+
const workerResult = await checkWorkerReadiness(runtime)
|
|
58
|
+
if (!workerResult.status) {
|
|
59
|
+
return workerResult
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Extension readiness checks participate in /ready only.
|
|
63
|
+
const extensionResult = await runtime.runExtensionReadinessChecks()
|
|
64
|
+
if (!extensionResult.status) {
|
|
65
|
+
return {
|
|
66
|
+
status: false,
|
|
67
|
+
response: extensionResult.response ?? workerResult.response
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
status: true,
|
|
73
|
+
response: extensionResult.response ?? workerResult.response
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
55
77
|
async function checkLiveness (runtime) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
78
|
+
// Worker readiness still gates liveness (existing semantics), but extension
|
|
79
|
+
// readiness-only failures must not fail /status and cause restart loops.
|
|
80
|
+
const workerReadiness = await checkWorkerReadiness(runtime)
|
|
81
|
+
if (!workerReadiness.status) {
|
|
82
|
+
return { status: false, readiness: workerReadiness.response }
|
|
59
83
|
}
|
|
60
|
-
// TODO test, doc
|
|
61
|
-
// in case of readiness check failure, if custom readiness response is set, we return the readiness check response on health check endpoint
|
|
62
84
|
|
|
63
85
|
const checks = await runtime.getCustomHealthChecks()
|
|
64
86
|
|
|
65
87
|
let response
|
|
66
|
-
const
|
|
88
|
+
const workerStatus = Object.values(checks).every(check => {
|
|
67
89
|
if (typeof check === 'boolean') {
|
|
68
90
|
return check
|
|
69
91
|
} else if (typeof check === 'object') {
|
|
@@ -73,7 +95,22 @@ async function checkLiveness (runtime) {
|
|
|
73
95
|
return false
|
|
74
96
|
})
|
|
75
97
|
|
|
76
|
-
|
|
98
|
+
if (!workerStatus) {
|
|
99
|
+
return { response, status: false }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const extensionResult = await runtime.runExtensionLivenessChecks()
|
|
103
|
+
if (!extensionResult.status) {
|
|
104
|
+
return {
|
|
105
|
+
status: false,
|
|
106
|
+
response: extensionResult.response ?? response
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
status: true,
|
|
112
|
+
response: extensionResult.response ?? response
|
|
113
|
+
}
|
|
77
114
|
}
|
|
78
115
|
|
|
79
116
|
function isHealthProbesServerEnabled (healthProbes) {
|
|
@@ -122,6 +159,37 @@ function resolveHealthProbesOptions (metricsOpts, healthProbes) {
|
|
|
122
159
|
}
|
|
123
160
|
}
|
|
124
161
|
|
|
162
|
+
async function registerExtensionHealthRoutes (promServer, runtime) {
|
|
163
|
+
const routes = runtime.getExtensionHealthRoutes?.() ?? []
|
|
164
|
+
|
|
165
|
+
for (const entry of routes) {
|
|
166
|
+
if (!entry.active) {
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
await promServer.register(async function extensionHealthRoutes (app) {
|
|
172
|
+
app.addHook('onRequest', async (_req, reply) => {
|
|
173
|
+
if (!entry.active) {
|
|
174
|
+
return reply.code(404).send({ message: 'Not Found' })
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
await app.register(entry.plugin)
|
|
179
|
+
})
|
|
180
|
+
entry.applied = true
|
|
181
|
+
} catch (err) {
|
|
182
|
+
const routeMatch = typeof err.message === 'string'
|
|
183
|
+
? err.message.match(/Method '([^']+)' already declared for route '([^']+)'/i)
|
|
184
|
+
: null
|
|
185
|
+
const method = routeMatch?.[1] ?? 'UNKNOWN'
|
|
186
|
+
const url = routeMatch?.[2] ?? 'unknown'
|
|
187
|
+
|
|
188
|
+
throw new DuplicateExtensionHealthRouteError(entry.extensionPath, method, url, err.message, { cause: err })
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
125
193
|
async function startServer (runtime, opts, metricsEnabled, healthProbesEnabled) {
|
|
126
194
|
opts = typeof opts === 'object' && opts !== null ? opts : {}
|
|
127
195
|
|
|
@@ -281,6 +349,11 @@ async function startServer (runtime, opts, metricsEnabled, healthProbesEnabled)
|
|
|
281
349
|
await promServer.register(plugin)
|
|
282
350
|
}
|
|
283
351
|
|
|
352
|
+
// Extension routes must be registered before the server starts listening.
|
|
353
|
+
if (healthProbesEnabled) {
|
|
354
|
+
await registerExtensionHealthRoutes(promServer, runtime)
|
|
355
|
+
}
|
|
356
|
+
|
|
284
357
|
await promServer.listen({ port, host })
|
|
285
358
|
return promServer
|
|
286
359
|
}
|