@platformatic/runtime 3.72.0 → 4.0.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/config.d.ts +602 -673
- package/index.d.ts +31 -28
- package/index.js +290 -30
- package/lib/config.js +270 -367
- package/lib/errors.js +32 -13
- package/lib/generator.js +249 -127
- package/lib/logger.js +3 -3
- package/lib/management-api.js +44 -72
- package/lib/management-handlers.js +3 -5
- package/lib/memory.js +29 -0
- package/lib/policies.js +27 -0
- package/lib/predictive-scaling.js +910 -0
- package/lib/predictive-worker-scaler.js +378 -0
- package/lib/runtime.js +1179 -606
- package/lib/scheduler.js +6 -41
- package/lib/schema.js +77 -15
- package/lib/upgrade.js +16 -1
- package/lib/versions/v4.0.0.js +31 -0
- package/lib/worker/controller.js +89 -109
- package/lib/worker/interceptors.js +34 -34
- package/lib/worker/itc.js +248 -66
- package/lib/worker/main.js +50 -98
- package/lib/worker/messaging.js +42 -1
- package/lib/worker/round-robin-map.js +10 -3
- package/lib/worker/symbols.js +6 -2
- package/package.json +33 -24
- package/schema.json +252 -433
- package/.gitignore +0 -3
- package/.npmignore +0 -2
- package/lib/scaling-algorithm.js +0 -256
- package/lib/worker-scaler.js +0 -249
package/lib/management-api.js
CHANGED
|
@@ -3,18 +3,22 @@ import fastifyWebsocket from '@fastify/websocket'
|
|
|
3
3
|
import {
|
|
4
4
|
applications as applicationSchema,
|
|
5
5
|
createDirectory,
|
|
6
|
+
createValidator,
|
|
6
7
|
createSharedTemporaryDirectory,
|
|
7
8
|
kMetadata,
|
|
8
9
|
safeRemove,
|
|
9
10
|
validate
|
|
10
11
|
} from '@platformatic/foundation'
|
|
12
|
+
import { schemaComponents } from '@platformatic/foundation/schema'
|
|
11
13
|
import fastify from 'fastify'
|
|
12
14
|
import { chmod } from 'node:fs/promises'
|
|
13
15
|
import { platform, tmpdir } from 'node:os'
|
|
14
16
|
import { dirname, join } from 'node:path'
|
|
15
17
|
import { setTimeout as sleep } from 'node:timers/promises'
|
|
16
18
|
import { createWebSocketStream } from 'ws'
|
|
17
|
-
import {
|
|
19
|
+
import { prepareAddedApplications } from './config.js'
|
|
20
|
+
import { readMemory } from './memory.js'
|
|
21
|
+
import { MetricsDisabledError } from './errors.js'
|
|
18
22
|
|
|
19
23
|
const PLATFORMATIC_TMP_DIR = join(tmpdir(), 'platformatic', 'runtimes')
|
|
20
24
|
|
|
@@ -99,18 +103,6 @@ export async function managementApiPlugin (app, opts) {
|
|
|
99
103
|
return runtime.getRuntimeMetadata()
|
|
100
104
|
})
|
|
101
105
|
|
|
102
|
-
app.get('/config', quiet, async request => {
|
|
103
|
-
const metadata = request.query.metadata === 'true'
|
|
104
|
-
const rawConfig = await runtime.getRuntimeConfig(metadata)
|
|
105
|
-
|
|
106
|
-
if (metadata) {
|
|
107
|
-
const { [kMetadata]: __metadata, ...config } = rawConfig
|
|
108
|
-
return { ...config, __metadata }
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return rawConfig
|
|
112
|
-
})
|
|
113
|
-
|
|
114
106
|
app.get('/env', quiet, async () => {
|
|
115
107
|
return { ...process.env, ...runtime.getRuntimeEnv() }
|
|
116
108
|
})
|
|
@@ -147,6 +139,20 @@ export async function managementApiPlugin (app, opts) {
|
|
|
147
139
|
return runtime.getApplications()
|
|
148
140
|
})
|
|
149
141
|
|
|
142
|
+
app.get('/memory', quiet, async () => {
|
|
143
|
+
return readMemory()
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
app.get('/workers/scaler', quiet, async (_request, reply) => {
|
|
147
|
+
const scaler = runtime.getDynamicWorkersScaler()
|
|
148
|
+
if (!scaler) {
|
|
149
|
+
reply.code(404)
|
|
150
|
+
return { statusCode: 404, error: 'Not Found', message: 'Dynamic workers scaling is not enabled.' }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return scaler.getDiagnostics()
|
|
154
|
+
})
|
|
155
|
+
|
|
150
156
|
app.post('/applications', async (request, reply) => {
|
|
151
157
|
let applications = request.body
|
|
152
158
|
|
|
@@ -169,11 +175,9 @@ export async function managementApiPlugin (app, opts) {
|
|
|
169
175
|
}
|
|
170
176
|
}
|
|
171
177
|
|
|
172
|
-
|
|
173
|
-
applications[i] = await prepareApplication(config, applications[i], config.workers)
|
|
174
|
-
}
|
|
178
|
+
const prepared = await prepareAddedApplications(config, applications, runtime.getApplicationsIds())
|
|
175
179
|
|
|
176
|
-
const created = await runtime.addApplications(
|
|
180
|
+
const created = await runtime.addApplications(prepared, request.query.start !== 'false')
|
|
177
181
|
reply.code(201)
|
|
178
182
|
return created
|
|
179
183
|
})
|
|
@@ -302,16 +306,31 @@ export async function managementApiPlugin (app, opts) {
|
|
|
302
306
|
return reply
|
|
303
307
|
})
|
|
304
308
|
|
|
309
|
+
const metricsSchema = schemaComponents.runtimeProperties.metrics.anyOf[1]
|
|
310
|
+
app.patch('/metrics', {
|
|
311
|
+
schema: {
|
|
312
|
+
body: {
|
|
313
|
+
...metricsSchema,
|
|
314
|
+
minProperties: 1,
|
|
315
|
+
properties: { ...metricsSchema.properties, enabled: { type: 'boolean' } }
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
// A patch must not inject defaults, coerce values, or silently discard unknown options.
|
|
319
|
+
validatorCompiler: ({ schema }) => createValidator(schema, {
|
|
320
|
+
useDefaults: false,
|
|
321
|
+
coerceTypes: false,
|
|
322
|
+
removeAdditional: false
|
|
323
|
+
}, { root: runtime.getRuntimeConfig(true)[kMetadata].root })
|
|
324
|
+
}, async req => runtime.updateMetricsConfig(req.body))
|
|
325
|
+
|
|
305
326
|
app.get('/metrics', quiet, async (req, reply) => {
|
|
306
|
-
|
|
327
|
+
// The live read: this needs one boolean, and the public getter builds a frozen snapshot of the
|
|
328
|
+
// whole configuration -- which is the right shape to hand a consumer and the wrong one to pay
|
|
329
|
+
// for on a scrape.
|
|
330
|
+
const config = runtime.getRuntimeConfig(true)
|
|
307
331
|
|
|
308
|
-
if (config.metrics?.enabled === false) {
|
|
309
|
-
|
|
310
|
-
return {
|
|
311
|
-
statusCode: 501,
|
|
312
|
-
error: 'Not Implemented',
|
|
313
|
-
message: 'Metrics are disabled.'
|
|
314
|
-
}
|
|
332
|
+
if (config.metrics === false || config.metrics?.enabled === false) {
|
|
333
|
+
throw new MetricsDisabledError()
|
|
315
334
|
}
|
|
316
335
|
|
|
317
336
|
const accepts = req.accepts()
|
|
@@ -326,53 +345,6 @@ export async function managementApiPlugin (app, opts) {
|
|
|
326
345
|
return metrics
|
|
327
346
|
})
|
|
328
347
|
|
|
329
|
-
// TODO: Remove in next major version - deprecated endpoint
|
|
330
|
-
app.get('/metrics/live', { ...quiet, websocket: true }, async socket => {
|
|
331
|
-
const config = await runtime.getRuntimeConfig()
|
|
332
|
-
|
|
333
|
-
if (config.metrics?.enabled === false) {
|
|
334
|
-
socket.send(
|
|
335
|
-
JSON.stringify({
|
|
336
|
-
statusCode: 501,
|
|
337
|
-
error: 'Not Implemented',
|
|
338
|
-
message: 'Metrics are disabled.'
|
|
339
|
-
}),
|
|
340
|
-
() => {
|
|
341
|
-
socket.close()
|
|
342
|
-
}
|
|
343
|
-
)
|
|
344
|
-
|
|
345
|
-
return
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const pollAndSendMetrics = async () => {
|
|
349
|
-
try {
|
|
350
|
-
const metrics = await runtime.getFormattedMetrics()
|
|
351
|
-
if (metrics) {
|
|
352
|
-
const serializedMetrics = JSON.stringify(metrics)
|
|
353
|
-
socket.send(serializedMetrics + '\n')
|
|
354
|
-
}
|
|
355
|
-
} catch (error) {
|
|
356
|
-
// If there's an error, stop polling and close the connection
|
|
357
|
-
clearInterval(pollingInterval)
|
|
358
|
-
socket.close()
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Poll every second
|
|
363
|
-
const pollingInterval = setInterval(pollAndSendMetrics, 1000)
|
|
364
|
-
|
|
365
|
-
// Send initial metrics immediately
|
|
366
|
-
await pollAndSendMetrics()
|
|
367
|
-
|
|
368
|
-
const cleanup = () => {
|
|
369
|
-
clearInterval(pollingInterval)
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
socket.on('error', cleanup)
|
|
373
|
-
socket.on('close', cleanup)
|
|
374
|
-
})
|
|
375
|
-
|
|
376
348
|
app.get('/logs/live', { ...quiet, websocket: true }, async socket => {
|
|
377
349
|
runtime.addLoggerDestination(createWebSocketStream(socket))
|
|
378
350
|
})
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
kMetadata,
|
|
4
4
|
validate
|
|
5
5
|
} from '@platformatic/foundation'
|
|
6
|
-
import {
|
|
6
|
+
import { prepareAddedApplications } from './config.js'
|
|
7
7
|
|
|
8
8
|
const allOperations = [
|
|
9
9
|
'getRuntimeStatus',
|
|
@@ -138,11 +138,9 @@ export function createManagementHandlers (runtime, allowedOperations) {
|
|
|
138
138
|
|
|
139
139
|
validate(applicationSchema, applications, {}, true, config[kMetadata].root)
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
applications[i] = await prepareApplication(config, applications[i], config.workers)
|
|
143
|
-
}
|
|
141
|
+
const prepared = await prepareAddedApplications(config, applications, runtime.getApplicationsIds())
|
|
144
142
|
|
|
145
|
-
return runtime.addApplications(
|
|
143
|
+
return runtime.addApplications(prepared, start)
|
|
146
144
|
})
|
|
147
145
|
|
|
148
146
|
register('removeApplications', async ({ ids }) => {
|
package/lib/memory.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getMemoryInfo } from './metrics.js'
|
|
2
|
+
|
|
3
|
+
// Memory readings are shared for ttlMs, and concurrent callers share one read.
|
|
4
|
+
// The scope (host or cgroup) is detected on the first read and then reused.
|
|
5
|
+
export function createMemoryReader ({ read = getMemoryInfo, ttlMs = 1000 } = {}) {
|
|
6
|
+
let scope
|
|
7
|
+
let reading = null
|
|
8
|
+
let readAt = 0
|
|
9
|
+
let pending = null
|
|
10
|
+
|
|
11
|
+
return function readMemory () {
|
|
12
|
+
if (reading && Date.now() - readAt < ttlMs) return Promise.resolve(reading)
|
|
13
|
+
|
|
14
|
+
pending ??= read({ scope })
|
|
15
|
+
.then(info => {
|
|
16
|
+
scope = info.scope
|
|
17
|
+
reading = info
|
|
18
|
+
readAt = Date.now()
|
|
19
|
+
return info
|
|
20
|
+
})
|
|
21
|
+
.finally(() => {
|
|
22
|
+
pending = null
|
|
23
|
+
})
|
|
24
|
+
return pending
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Shared by every caller in the process, so they share one cached reading.
|
|
29
|
+
export const readMemory = createMemoryReader()
|
package/lib/policies.js
CHANGED
|
@@ -21,3 +21,30 @@ export function createChannelCreationHook (config) {
|
|
|
21
21
|
return !forbidden.has(`${first.toLowerCase()}:${second.toLowerCase()}`)
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
export function createTargetPermissionHook (config) {
|
|
26
|
+
const denyList = config.policies?.deny
|
|
27
|
+
|
|
28
|
+
if (typeof denyList === 'undefined') {
|
|
29
|
+
return undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const forbidden = new Set()
|
|
33
|
+
|
|
34
|
+
for (let [first, unalloweds] of Object.entries(denyList)) {
|
|
35
|
+
for (let second of unalloweds) {
|
|
36
|
+
first = first.toLowerCase()
|
|
37
|
+
second = second.toLowerCase()
|
|
38
|
+
|
|
39
|
+
forbidden.add(`${first}:${second}`)
|
|
40
|
+
forbidden.add(`${second}:${first}`)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return function targetPermissionHook (req, target) {
|
|
45
|
+
const source = config.applicationId ?? req?.headers?.['x-plt-application-id']
|
|
46
|
+
const destination = target?.metadata?.applicationId
|
|
47
|
+
|
|
48
|
+
return !source || !destination || !forbidden.has(`${source.toLowerCase()}:${destination.toLowerCase()}`)
|
|
49
|
+
}
|
|
50
|
+
}
|