@platformatic/runtime 3.72.0 → 4.0.0-alpha.1
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 +22 -27
- package/index.js +290 -30
- package/lib/config.js +270 -367
- package/lib/errors.js +27 -13
- package/lib/generator.js +249 -127
- package/lib/logger.js +3 -3
- package/lib/management-api.js +22 -65
- 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 +1081 -572
- 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
|
@@ -14,7 +14,8 @@ import { platform, tmpdir } from 'node:os'
|
|
|
14
14
|
import { dirname, join } from 'node:path'
|
|
15
15
|
import { setTimeout as sleep } from 'node:timers/promises'
|
|
16
16
|
import { createWebSocketStream } from 'ws'
|
|
17
|
-
import {
|
|
17
|
+
import { prepareAddedApplications } from './config.js'
|
|
18
|
+
import { readMemory } from './memory.js'
|
|
18
19
|
|
|
19
20
|
const PLATFORMATIC_TMP_DIR = join(tmpdir(), 'platformatic', 'runtimes')
|
|
20
21
|
|
|
@@ -99,18 +100,6 @@ export async function managementApiPlugin (app, opts) {
|
|
|
99
100
|
return runtime.getRuntimeMetadata()
|
|
100
101
|
})
|
|
101
102
|
|
|
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
103
|
app.get('/env', quiet, async () => {
|
|
115
104
|
return { ...process.env, ...runtime.getRuntimeEnv() }
|
|
116
105
|
})
|
|
@@ -147,6 +136,20 @@ export async function managementApiPlugin (app, opts) {
|
|
|
147
136
|
return runtime.getApplications()
|
|
148
137
|
})
|
|
149
138
|
|
|
139
|
+
app.get('/memory', quiet, async () => {
|
|
140
|
+
return readMemory()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
app.get('/workers/scaler', quiet, async (_request, reply) => {
|
|
144
|
+
const scaler = runtime.getDynamicWorkersScaler()
|
|
145
|
+
if (!scaler) {
|
|
146
|
+
reply.code(404)
|
|
147
|
+
return { statusCode: 404, error: 'Not Found', message: 'Dynamic workers scaling is not enabled.' }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return scaler.getDiagnostics()
|
|
151
|
+
})
|
|
152
|
+
|
|
150
153
|
app.post('/applications', async (request, reply) => {
|
|
151
154
|
let applications = request.body
|
|
152
155
|
|
|
@@ -169,11 +172,9 @@ export async function managementApiPlugin (app, opts) {
|
|
|
169
172
|
}
|
|
170
173
|
}
|
|
171
174
|
|
|
172
|
-
|
|
173
|
-
applications[i] = await prepareApplication(config, applications[i], config.workers)
|
|
174
|
-
}
|
|
175
|
+
const prepared = await prepareAddedApplications(config, applications, runtime.getApplicationsIds())
|
|
175
176
|
|
|
176
|
-
const created = await runtime.addApplications(
|
|
177
|
+
const created = await runtime.addApplications(prepared, request.query.start !== 'false')
|
|
177
178
|
reply.code(201)
|
|
178
179
|
return created
|
|
179
180
|
})
|
|
@@ -303,7 +304,10 @@ export async function managementApiPlugin (app, opts) {
|
|
|
303
304
|
})
|
|
304
305
|
|
|
305
306
|
app.get('/metrics', quiet, async (req, reply) => {
|
|
306
|
-
|
|
307
|
+
// The live read: this needs one boolean, and the public getter builds a frozen snapshot of the
|
|
308
|
+
// whole configuration -- which is the right shape to hand a consumer and the wrong one to pay
|
|
309
|
+
// for on a scrape.
|
|
310
|
+
const config = runtime.getRuntimeConfig(true)
|
|
307
311
|
|
|
308
312
|
if (config.metrics?.enabled === false) {
|
|
309
313
|
reply.code(501)
|
|
@@ -326,53 +330,6 @@ export async function managementApiPlugin (app, opts) {
|
|
|
326
330
|
return metrics
|
|
327
331
|
})
|
|
328
332
|
|
|
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
333
|
app.get('/logs/live', { ...quiet, websocket: true }, async socket => {
|
|
377
334
|
runtime.addLoggerDestination(createWebSocketStream(socket))
|
|
378
335
|
})
|
|
@@ -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
|
+
}
|