@platformatic/runtime 3.0.0-alpha.1 → 3.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/index.d.ts +24 -3
- package/index.js +57 -6
- package/lib/config.js +1 -1
- package/lib/generator.js +12 -33
- package/lib/logger.js +6 -29
- package/lib/management-api.js +5 -55
- package/lib/prom-server.js +7 -3
- package/lib/runtime.js +67 -182
- package/lib/schema.js +1 -1
- package/lib/shared-http-cache.js +1 -1
- package/lib/upgrade.js +1 -1
- package/lib/utils.js +0 -6
- package/lib/worker/app.js +27 -11
- package/lib/worker/itc.js +21 -5
- package/lib/worker/main.js +15 -2
- package/lib/worker/messaging.js +1 -1
- package/lib/worker/shared-context.js +26 -0
- package/package.json +20 -21
- package/schema.json +1 -1
package/index.d.ts
CHANGED
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
import { FastifyError } from '@fastify/error'
|
|
2
|
+
import { Configuration, ConfigurationOptions, logFatalError, parseArgs } from '@platformatic/foundation'
|
|
2
3
|
import { BaseGenerator } from '@platformatic/generators'
|
|
3
|
-
import { Configuration, ConfigurationOptions } from '@platformatic/utils'
|
|
4
4
|
import { JSONSchemaType } from 'ajv'
|
|
5
|
+
import * as colorette from 'colorette'
|
|
6
|
+
import { Logger } from 'pino'
|
|
5
7
|
import { PlatformaticRuntimeConfig } from './config'
|
|
6
8
|
|
|
7
9
|
export type RuntimeConfiguration = Promise<Configuration<PlatformaticRuntimeConfig>>
|
|
8
10
|
|
|
11
|
+
export type ServiceCommandContext = {
|
|
12
|
+
colorette: typeof colorette
|
|
13
|
+
parseArgs: typeof parseArgs
|
|
14
|
+
logFatalError: typeof logFatalError
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ServiceCommand = (
|
|
18
|
+
logger: Logger,
|
|
19
|
+
configuration: Configuration<unknown>,
|
|
20
|
+
args: string[],
|
|
21
|
+
context: ServiceCommandContext
|
|
22
|
+
) => Promise<void>
|
|
23
|
+
|
|
24
|
+
export interface ServicesCommands {
|
|
25
|
+
services: Record<string, Configuration<unknown>>
|
|
26
|
+
commands: Record<string, ServiceCommand>
|
|
27
|
+
help: Record<string, string | (() => string)>
|
|
28
|
+
}
|
|
29
|
+
|
|
9
30
|
export module errors {
|
|
10
31
|
export const RuntimeExitedError: () => FastifyError
|
|
11
32
|
export const UnknownRuntimeAPICommandError: (command: string) => FastifyError
|
|
@@ -45,8 +66,6 @@ export class Generator extends BaseGenerator.BaseGenerator {}
|
|
|
45
66
|
|
|
46
67
|
export class WrappedGenerator extends BaseGenerator.BaseGenerator {}
|
|
47
68
|
|
|
48
|
-
export declare function getRuntimeLogsDir (runtimeDir: string, runtimePID: number): string
|
|
49
|
-
|
|
50
69
|
export declare const schema: JSONSchemaType<PlatformaticRuntimeConfig>
|
|
51
70
|
|
|
52
71
|
export declare class Runtime {}
|
|
@@ -71,3 +90,5 @@ export function create (
|
|
|
71
90
|
): Promise<Runtime>
|
|
72
91
|
|
|
73
92
|
export declare function transform (config: RuntimeConfiguration): Promise<RuntimeConfiguration> | RuntimeConfiguration
|
|
93
|
+
|
|
94
|
+
export declare function loadServicesCommands (): Promise<ServicesCommands>
|
package/index.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const inspector = require('node:inspector')
|
|
4
|
-
const { kMetadata } = require('@platformatic/utils')
|
|
5
|
-
const { resolve, validationOptions } = require('@platformatic/basic')
|
|
6
4
|
const {
|
|
5
|
+
kMetadata,
|
|
6
|
+
loadConfigurationModule,
|
|
7
|
+
abstractLogger,
|
|
8
|
+
findRuntimeConfigurationFile,
|
|
7
9
|
loadConfiguration: utilsLoadConfiguration,
|
|
8
10
|
extractModuleFromSchemaUrl,
|
|
9
11
|
ensureLoggableError
|
|
10
|
-
} = require('@platformatic/
|
|
12
|
+
} = require('@platformatic/foundation')
|
|
13
|
+
const { resolve, validationOptions } = require('@platformatic/basic')
|
|
11
14
|
const { NodeInspectorFlagsNotSupportedError } = require('./lib/errors')
|
|
12
15
|
const { wrapInRuntimeConfig, transform } = require('./lib/config')
|
|
13
16
|
const { RuntimeGenerator, WrappedGenerator } = require('./lib/generator')
|
|
@@ -15,7 +18,6 @@ const { Runtime } = require('./lib/runtime')
|
|
|
15
18
|
const symbols = require('./lib/worker/symbols')
|
|
16
19
|
const { schema } = require('./lib/schema')
|
|
17
20
|
const { upgrade } = require('./lib/upgrade')
|
|
18
|
-
const { getRuntimeLogsDir } = require('./lib/utils')
|
|
19
21
|
|
|
20
22
|
async function restartRuntime (runtime) {
|
|
21
23
|
runtime.logger.info('Received SIGUSR2, restarting all services ...')
|
|
@@ -56,6 +58,53 @@ async function loadConfiguration (configOrRoot, sourceOrConfig, context) {
|
|
|
56
58
|
})
|
|
57
59
|
}
|
|
58
60
|
|
|
61
|
+
async function loadServicesCommands () {
|
|
62
|
+
const services = {}
|
|
63
|
+
const commands = {}
|
|
64
|
+
const help = {}
|
|
65
|
+
|
|
66
|
+
let config
|
|
67
|
+
try {
|
|
68
|
+
const file = await findRuntimeConfigurationFile(abstractLogger, process.cwd(), null, false, false)
|
|
69
|
+
|
|
70
|
+
/* c8 ignore next 3 - Hard to test */
|
|
71
|
+
if (!file) {
|
|
72
|
+
throw new Error('No runtime configuration file found.')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
config = await loadConfiguration(file)
|
|
76
|
+
|
|
77
|
+
/* c8 ignore next 3 - Hard to test */
|
|
78
|
+
if (!config) {
|
|
79
|
+
throw new Error('No runtime configuration file found.')
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
return { services, commands, help }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const service of config.services) {
|
|
86
|
+
try {
|
|
87
|
+
const serviceConfig = await utilsLoadConfiguration(service.config)
|
|
88
|
+
const pkg = await loadConfigurationModule(service.path, serviceConfig)
|
|
89
|
+
|
|
90
|
+
if (pkg.createCommands) {
|
|
91
|
+
const definition = await pkg.createCommands(service.id)
|
|
92
|
+
for (const command of Object.keys(definition.commands)) {
|
|
93
|
+
services[command] = service
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
Object.assign(commands, definition.commands)
|
|
97
|
+
Object.assign(help, definition.help)
|
|
98
|
+
}
|
|
99
|
+
/* c8 ignore next 3 - Hard to test */
|
|
100
|
+
} catch {
|
|
101
|
+
// No-op, ignore the service
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { services, commands, help }
|
|
106
|
+
}
|
|
107
|
+
|
|
59
108
|
async function create (configOrRoot, sourceOrConfig, context) {
|
|
60
109
|
const config = await loadConfiguration(configOrRoot, sourceOrConfig, context)
|
|
61
110
|
|
|
@@ -75,10 +124,12 @@ async function create (configOrRoot, sourceOrConfig, context) {
|
|
|
75
124
|
await runtime.start()
|
|
76
125
|
break
|
|
77
126
|
} catch (err) {
|
|
78
|
-
if (err.code !== 'EADDRINUSE' || context?.skipPortInUseHandling) {
|
|
127
|
+
if ((err.code !== 'EADDRINUSE' && err.code !== 'EACCES') || context?.skipPortInUseHandling) {
|
|
79
128
|
throw err
|
|
80
129
|
}
|
|
81
130
|
|
|
131
|
+
await runtime.close()
|
|
132
|
+
|
|
82
133
|
// Get the actual port from the error message if original port was 0
|
|
83
134
|
if (!port) {
|
|
84
135
|
const mo = err.message.match(/ address already in use (.+)/)
|
|
@@ -101,7 +152,6 @@ const platformaticVersion = require('./package.json').version
|
|
|
101
152
|
module.exports.errors = require('./lib/errors')
|
|
102
153
|
module.exports.Generator = RuntimeGenerator
|
|
103
154
|
module.exports.WrappedGenerator = WrappedGenerator
|
|
104
|
-
module.exports.getRuntimeLogsDir = getRuntimeLogsDir
|
|
105
155
|
module.exports.schema = schema
|
|
106
156
|
module.exports.symbols = symbols
|
|
107
157
|
module.exports.Runtime = Runtime
|
|
@@ -110,3 +160,4 @@ module.exports.version = platformaticVersion
|
|
|
110
160
|
module.exports.loadConfiguration = loadConfiguration
|
|
111
161
|
module.exports.create = create
|
|
112
162
|
module.exports.transform = transform
|
|
163
|
+
module.exports.loadServicesCommands = loadServicesCommands
|
package/lib/config.js
CHANGED
package/lib/generator.js
CHANGED
|
@@ -18,8 +18,9 @@ const {
|
|
|
18
18
|
findConfigurationFile,
|
|
19
19
|
loadConfiguration,
|
|
20
20
|
loadConfigurationFile,
|
|
21
|
-
kMetadata
|
|
22
|
-
|
|
21
|
+
kMetadata,
|
|
22
|
+
defaultPackageManager
|
|
23
|
+
} = require('@platformatic/foundation')
|
|
23
24
|
const { createRequire } = require('node:module')
|
|
24
25
|
|
|
25
26
|
const wrappableProperties = {
|
|
@@ -34,7 +35,7 @@ const wrappableProperties = {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
const engines = {
|
|
37
|
-
node: '>=22.
|
|
38
|
+
node: '>=22.18.0'
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
const ERROR_PREFIX = 'PLT_RUNTIME_GEN'
|
|
@@ -65,6 +66,7 @@ class RuntimeGenerator extends BaseGenerator {
|
|
|
65
66
|
this.services = []
|
|
66
67
|
this.existingServices = []
|
|
67
68
|
this.entryPoint = null
|
|
69
|
+
this.packageManager = opts.packageManager ?? defaultPackageManager
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
async addService (service, name) {
|
|
@@ -84,9 +86,7 @@ class RuntimeGenerator extends BaseGenerator {
|
|
|
84
86
|
service
|
|
85
87
|
})
|
|
86
88
|
|
|
87
|
-
|
|
88
|
-
service.setRuntime(this)
|
|
89
|
-
}
|
|
89
|
+
service.setRuntime(this)
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
setEntryPoint (entryPoint) {
|
|
@@ -100,7 +100,6 @@ class RuntimeGenerator extends BaseGenerator {
|
|
|
100
100
|
async generatePackageJson () {
|
|
101
101
|
const template = {
|
|
102
102
|
name: `${this.runtimeName}`,
|
|
103
|
-
workspaces: [this.servicesFolder + '/*'],
|
|
104
103
|
scripts: {
|
|
105
104
|
dev: this.config.devCommand,
|
|
106
105
|
build: this.config.buildCommand,
|
|
@@ -118,13 +117,11 @@ class RuntimeGenerator extends BaseGenerator {
|
|
|
118
117
|
},
|
|
119
118
|
engines
|
|
120
119
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
template.scripts.clean = 'rm -fr ./dist'
|
|
125
|
-
template.scripts.build = 'platformatic compile'
|
|
126
|
-
template.devDependencies.typescript = typescriptVersion
|
|
120
|
+
|
|
121
|
+
if (this.packageManager === 'npm' || this.packageManager === 'yarn') {
|
|
122
|
+
template.workspaces = [this.servicesFolder + '/*']
|
|
127
123
|
}
|
|
124
|
+
|
|
128
125
|
return template
|
|
129
126
|
}
|
|
130
127
|
|
|
@@ -191,7 +188,6 @@ class RuntimeGenerator extends BaseGenerator {
|
|
|
191
188
|
// set default config
|
|
192
189
|
service.setConfig()
|
|
193
190
|
}
|
|
194
|
-
service.config.typescript = this.config.typescript
|
|
195
191
|
})
|
|
196
192
|
}
|
|
197
193
|
|
|
@@ -226,7 +222,7 @@ class RuntimeGenerator extends BaseGenerator {
|
|
|
226
222
|
this.addFile({
|
|
227
223
|
path: '',
|
|
228
224
|
file: '.env.sample',
|
|
229
|
-
contents: envObjectToString(this.config.
|
|
225
|
+
contents: envObjectToString(this.config.defaultEnv)
|
|
230
226
|
})
|
|
231
227
|
|
|
232
228
|
return {
|
|
@@ -256,18 +252,6 @@ class RuntimeGenerator extends BaseGenerator {
|
|
|
256
252
|
async prepareQuestions () {
|
|
257
253
|
await this.populateFromExistingConfig()
|
|
258
254
|
|
|
259
|
-
// typescript
|
|
260
|
-
this.questions.push({
|
|
261
|
-
type: 'list',
|
|
262
|
-
name: 'typescript',
|
|
263
|
-
message: 'Do you want to use TypeScript?',
|
|
264
|
-
default: false,
|
|
265
|
-
choices: [
|
|
266
|
-
{ name: 'yes', value: true },
|
|
267
|
-
{ name: 'no', value: false }
|
|
268
|
-
]
|
|
269
|
-
})
|
|
270
|
-
|
|
271
255
|
if (this.existingConfig) {
|
|
272
256
|
return
|
|
273
257
|
}
|
|
@@ -311,11 +295,6 @@ class RuntimeGenerator extends BaseGenerator {
|
|
|
311
295
|
async prepareServiceFiles () {
|
|
312
296
|
let servicesEnv = {}
|
|
313
297
|
for (const svc of this.services) {
|
|
314
|
-
// Propagate TypeScript
|
|
315
|
-
svc.service.setConfig({
|
|
316
|
-
...svc.service.config,
|
|
317
|
-
typescript: this.config.typescript
|
|
318
|
-
})
|
|
319
298
|
const svcEnv = await svc.service.prepare()
|
|
320
299
|
servicesEnv = {
|
|
321
300
|
...servicesEnv,
|
|
@@ -590,7 +569,7 @@ class WrappedGenerator extends BaseGenerator {
|
|
|
590
569
|
this.addFile({
|
|
591
570
|
path: '',
|
|
592
571
|
file: '.env.sample',
|
|
593
|
-
contents: (await this.#readExistingFile('.env.sample', '', '\n')) + envObjectToString(this.config.
|
|
572
|
+
contents: (await this.#readExistingFile('.env.sample', '', '\n')) + envObjectToString(this.config.defaultEnv)
|
|
594
573
|
})
|
|
595
574
|
}
|
|
596
575
|
|
package/lib/logger.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { once } = require('node:events')
|
|
4
|
-
const { join } = require('node:path')
|
|
5
3
|
const { isatty } = require('node:tty')
|
|
6
4
|
const pino = require('pino')
|
|
7
5
|
const pretty = require('pino-pretty')
|
|
8
|
-
const { abstractLogger, buildPinoFormatters, buildPinoTimestamp } = require('@platformatic/
|
|
6
|
+
const { abstractLogger, buildPinoFormatters, buildPinoTimestamp } = require('@platformatic/foundation')
|
|
9
7
|
|
|
10
8
|
const customPrettifiers = {
|
|
11
9
|
name (name, _, obj) {
|
|
@@ -19,26 +17,24 @@ const customPrettifiers = {
|
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
// Create the runtime logger
|
|
22
|
-
async function createLogger (config
|
|
20
|
+
async function createLogger (config) {
|
|
23
21
|
const loggerConfig = { ...config.logger, transport: undefined }
|
|
24
22
|
if (config.logger.base === null) {
|
|
25
23
|
loggerConfig.base = undefined
|
|
26
24
|
}
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
let cliStream = process.env.PLT_RUNTIME_LOGGER_STDOUT
|
|
30
|
-
? pino.destination(process.env.PLT_RUNTIME_LOGGER_STDOUT)
|
|
31
|
-
: isatty(1)
|
|
32
|
-
? pretty({ customPrettifiers })
|
|
33
|
-
: pino.destination(1)
|
|
26
|
+
let cliStream
|
|
34
27
|
|
|
35
28
|
if (config.logger.transport) {
|
|
36
29
|
cliStream = pino.transport(config.logger.transport)
|
|
30
|
+
} else {
|
|
31
|
+
cliStream = isatty(1) ? pretty({ customPrettifiers }) : pino.destination(1)
|
|
37
32
|
}
|
|
38
33
|
|
|
39
34
|
if (loggerConfig.formatters) {
|
|
40
35
|
loggerConfig.formatters = buildPinoFormatters(loggerConfig.formatters)
|
|
41
36
|
}
|
|
37
|
+
|
|
42
38
|
if (loggerConfig.timestamp) {
|
|
43
39
|
loggerConfig.timestamp = buildPinoTimestamp(loggerConfig.timestamp)
|
|
44
40
|
}
|
|
@@ -57,25 +53,6 @@ async function createLogger (config, runtimeLogsDir) {
|
|
|
57
53
|
logsLimitCount = 1
|
|
58
54
|
}
|
|
59
55
|
|
|
60
|
-
const pinoRoll = pino.transport({
|
|
61
|
-
target: 'pino-roll',
|
|
62
|
-
options: {
|
|
63
|
-
file: join(runtimeLogsDir, 'logs'),
|
|
64
|
-
mode: 0o600,
|
|
65
|
-
size: logsFileMb + 'm',
|
|
66
|
-
mkdir: true,
|
|
67
|
-
fsync: true,
|
|
68
|
-
limit: {
|
|
69
|
-
count: logsLimitCount
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
multiStream.add({ level: loggerConfig.level, stream: pinoRoll })
|
|
75
|
-
|
|
76
|
-
// Make sure there is a file before continuing otherwise the management API log endpoint might bail out
|
|
77
|
-
await once(pinoRoll, 'ready')
|
|
78
|
-
|
|
79
56
|
return [pino(loggerConfig, multiStream), multiStream]
|
|
80
57
|
}
|
|
81
58
|
|
package/lib/management-api.js
CHANGED
|
@@ -2,14 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
const { platform, tmpdir } = require('node:os')
|
|
4
4
|
const { join } = require('node:path')
|
|
5
|
-
const { createDirectory, safeRemove } = require('@platformatic/
|
|
5
|
+
const { createDirectory, safeRemove } = require('@platformatic/foundation')
|
|
6
6
|
|
|
7
7
|
const fastify = require('fastify')
|
|
8
8
|
const ws = require('ws')
|
|
9
9
|
|
|
10
|
-
const errors = require('./errors')
|
|
11
|
-
const { getRuntimeLogsDir } = require('./utils')
|
|
12
|
-
|
|
13
10
|
const PLATFORMATIC_TMP_DIR = join(tmpdir(), 'platformatic', 'runtimes')
|
|
14
11
|
|
|
15
12
|
async function managementApiPlugin (app, opts) {
|
|
@@ -152,54 +149,7 @@ async function managementApiPlugin (app, opts) {
|
|
|
152
149
|
})
|
|
153
150
|
|
|
154
151
|
app.get('/logs/live', { websocket: true }, async (socket, req) => {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (startLogId) {
|
|
158
|
-
const logIds = await runtime.getLogIds()
|
|
159
|
-
if (!logIds.includes(startLogId)) {
|
|
160
|
-
throw new errors.LogFileNotFound(startLogId)
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const stream = ws.createWebSocketStream(socket)
|
|
165
|
-
runtime.pipeLogsStream(stream, req.log, startLogId)
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
app.get('/logs/indexes', async req => {
|
|
169
|
-
const returnAllIds = req.query.all === 'true'
|
|
170
|
-
|
|
171
|
-
if (returnAllIds) {
|
|
172
|
-
const runtimesLogsIds = await runtime.getAllLogIds()
|
|
173
|
-
return runtimesLogsIds
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const runtimeLogsIds = await runtime.getLogIds()
|
|
177
|
-
return { indexes: runtimeLogsIds }
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
app.get('/logs/all', async (req, reply) => {
|
|
181
|
-
const runtimePID = parseInt(req.query.pid) || process.pid
|
|
182
|
-
|
|
183
|
-
const logsIds = await runtime.getLogIds(runtimePID)
|
|
184
|
-
const startLogId = logsIds.at(0)
|
|
185
|
-
const endLogId = logsIds.at(-1)
|
|
186
|
-
|
|
187
|
-
reply.hijack()
|
|
188
|
-
|
|
189
|
-
runtime.pipeLogsStream(reply.raw, req.log, startLogId, endLogId, runtimePID)
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
app.get('/logs/:id', async req => {
|
|
193
|
-
const logId = parseInt(req.params.id)
|
|
194
|
-
const runtimePID = parseInt(req.query.pid) || process.pid
|
|
195
|
-
|
|
196
|
-
const logIds = await runtime.getLogIds(runtimePID)
|
|
197
|
-
if (!logIds || !logIds.includes(logId)) {
|
|
198
|
-
throw new errors.LogFileNotFound(logId)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const logFileStream = await runtime.getLogFileStream(logId, runtimePID)
|
|
202
|
-
return logFileStream
|
|
152
|
+
runtime.addLoggerDestination(ws.createWebSocketStream(socket))
|
|
203
153
|
})
|
|
204
154
|
}
|
|
205
155
|
|
|
@@ -212,9 +162,6 @@ async function startManagementApi (runtime, root) {
|
|
|
212
162
|
await createDirectory(runtimePIDDir, true)
|
|
213
163
|
}
|
|
214
164
|
|
|
215
|
-
const runtimeLogsDir = getRuntimeLogsDir(root, process.pid)
|
|
216
|
-
await createDirectory(runtimeLogsDir, true)
|
|
217
|
-
|
|
218
165
|
let socketPath = null
|
|
219
166
|
if (platform() === 'win32') {
|
|
220
167
|
socketPath = '\\\\.\\pipe\\platformatic-' + runtimePID.toString()
|
|
@@ -232,6 +179,9 @@ async function startManagementApi (runtime, root) {
|
|
|
232
179
|
}
|
|
233
180
|
})
|
|
234
181
|
|
|
182
|
+
// When the runtime closes, close the management API as well
|
|
183
|
+
runtime.on('closed', managementApi.close.bind(managementApi))
|
|
184
|
+
|
|
235
185
|
await managementApi.listen({ path: socketPath })
|
|
236
186
|
return managementApi
|
|
237
187
|
/* c8 ignore next 4 */
|
package/lib/prom-server.js
CHANGED
|
@@ -77,6 +77,7 @@ async function startPrometheusServer (runtime, opts) {
|
|
|
77
77
|
const auth = opts.auth ?? null
|
|
78
78
|
|
|
79
79
|
const promServer = fastify({ name: 'Prometheus server' })
|
|
80
|
+
promServer.register(require('@fastify/accepts'))
|
|
80
81
|
|
|
81
82
|
let onRequestHook
|
|
82
83
|
if (auth) {
|
|
@@ -122,9 +123,12 @@ async function startPrometheusServer (runtime, opts) {
|
|
|
122
123
|
logLevel: 'warn',
|
|
123
124
|
onRequest: onRequestHook,
|
|
124
125
|
handler: async (req, reply) => {
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
126
|
+
const accepts = req.accepts()
|
|
127
|
+
const reqType = !accepts.type('text/plain') && accepts.type('application/json') ? 'json' : 'text'
|
|
128
|
+
if (reqType === 'text') {
|
|
129
|
+
reply.type('text/plain')
|
|
130
|
+
}
|
|
131
|
+
return (await runtime.getMetrics(reqType)).metrics
|
|
128
132
|
},
|
|
129
133
|
})
|
|
130
134
|
|
package/lib/runtime.js
CHANGED
|
@@ -4,21 +4,21 @@ const { ITC } = require('@platformatic/itc')
|
|
|
4
4
|
const {
|
|
5
5
|
features,
|
|
6
6
|
ensureLoggableError,
|
|
7
|
+
ensureError,
|
|
7
8
|
executeWithTimeout,
|
|
8
9
|
deepmerge,
|
|
9
10
|
parseMemorySize,
|
|
10
11
|
kTimeout,
|
|
11
12
|
kMetadata
|
|
12
|
-
} = require('@platformatic/
|
|
13
|
+
} = require('@platformatic/foundation')
|
|
13
14
|
const { once, EventEmitter } = require('node:events')
|
|
14
|
-
const {
|
|
15
|
-
const {
|
|
15
|
+
const { existsSync } = require('node:fs')
|
|
16
|
+
const { readFile } = require('node:fs/promises')
|
|
16
17
|
const { STATUS_CODES } = require('node:http')
|
|
17
18
|
const { join } = require('node:path')
|
|
18
19
|
const { pathToFileURL } = require('node:url')
|
|
19
20
|
const { setTimeout: sleep, setImmediate: immediate } = require('node:timers/promises')
|
|
20
21
|
const { Worker } = require('node:worker_threads')
|
|
21
|
-
const ts = require('tail-file-stream')
|
|
22
22
|
const { Agent, interceptors: undiciInterceptors, request } = require('undici')
|
|
23
23
|
const { createThreadInterceptor } = require('undici-thread-interceptor')
|
|
24
24
|
const SonicBoom = require('sonic-boom')
|
|
@@ -29,7 +29,7 @@ const { startManagementApi } = require('./management-api')
|
|
|
29
29
|
const { startPrometheusServer } = require('./prom-server')
|
|
30
30
|
const { startScheduler } = require('./scheduler')
|
|
31
31
|
const { createSharedStore } = require('./shared-http-cache')
|
|
32
|
-
const { getRuntimeTmpDir
|
|
32
|
+
const { getRuntimeTmpDir } = require('./utils')
|
|
33
33
|
const { sendViaITC, waitEventFromITC } = require('./worker/itc')
|
|
34
34
|
const { RoundRobinMap } = require('./worker/round-robin-map.js')
|
|
35
35
|
const {
|
|
@@ -71,7 +71,6 @@ class Runtime extends EventEmitter {
|
|
|
71
71
|
#context
|
|
72
72
|
#isProduction
|
|
73
73
|
#runtimeTmpDir
|
|
74
|
-
#runtimeLogsDir
|
|
75
74
|
#servicesIds
|
|
76
75
|
#entrypointId
|
|
77
76
|
#url
|
|
@@ -92,6 +91,7 @@ class Runtime extends EventEmitter {
|
|
|
92
91
|
servicesConfigsPatches
|
|
93
92
|
#scheduler
|
|
94
93
|
#stdio
|
|
94
|
+
#sharedContext
|
|
95
95
|
|
|
96
96
|
constructor (config, context) {
|
|
97
97
|
super()
|
|
@@ -103,7 +103,6 @@ class Runtime extends EventEmitter {
|
|
|
103
103
|
this.#context = context ?? {}
|
|
104
104
|
this.#isProduction = this.#context.isProduction ?? this.#context.production ?? false
|
|
105
105
|
this.#runtimeTmpDir = getRuntimeTmpDir(this.#root)
|
|
106
|
-
this.#runtimeLogsDir = getRuntimeLogsDir(this.#root, process.pid)
|
|
107
106
|
this.#workers = new RoundRobinMap()
|
|
108
107
|
this.#servicesIds = []
|
|
109
108
|
this.#url = undefined
|
|
@@ -133,8 +132,11 @@ class Runtime extends EventEmitter {
|
|
|
133
132
|
getHttpCacheValue: this.#getHttpCacheValue.bind(this),
|
|
134
133
|
setHttpCacheValue: this.#setHttpCacheValue.bind(this),
|
|
135
134
|
deleteHttpCacheValue: this.#deleteHttpCacheValue.bind(this),
|
|
136
|
-
invalidateHttpCache: this.invalidateHttpCache.bind(this)
|
|
135
|
+
invalidateHttpCache: this.invalidateHttpCache.bind(this),
|
|
136
|
+
updateSharedContext: this.updateSharedContext.bind(this),
|
|
137
|
+
getSharedContext: this.getSharedContext.bind(this)
|
|
137
138
|
}
|
|
139
|
+
this.#sharedContext = {}
|
|
138
140
|
}
|
|
139
141
|
|
|
140
142
|
async init () {
|
|
@@ -157,7 +159,7 @@ class Runtime extends EventEmitter {
|
|
|
157
159
|
}
|
|
158
160
|
|
|
159
161
|
// Create the logger
|
|
160
|
-
const [logger, destination] = await createLogger(config
|
|
162
|
+
const [logger, destination] = await createLogger(config)
|
|
161
163
|
this.logger = logger
|
|
162
164
|
this.#loggerDestination = destination
|
|
163
165
|
|
|
@@ -379,12 +381,8 @@ class Runtime extends EventEmitter {
|
|
|
379
381
|
|
|
380
382
|
await this.stop(silent)
|
|
381
383
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
setImmediate(() => {
|
|
385
|
-
this.#managementApi.close()
|
|
386
|
-
})
|
|
387
|
-
}
|
|
384
|
+
// The management API autocloses by itself via event in management-api.js.
|
|
385
|
+
// This is needed to let management API stop endpoint to reply.
|
|
388
386
|
|
|
389
387
|
if (this.#prometheusServer) {
|
|
390
388
|
await this.#prometheusServer.close()
|
|
@@ -549,8 +547,7 @@ class Runtime extends EventEmitter {
|
|
|
549
547
|
metrics = await this.getFormattedMetrics()
|
|
550
548
|
} catch (error) {
|
|
551
549
|
if (!(error instanceof errors.RuntimeExitedError)) {
|
|
552
|
-
|
|
553
|
-
console.error('Error collecting metrics', error)
|
|
550
|
+
this.logger.error({ err: ensureLoggableError(error) }, 'Error collecting metrics')
|
|
554
551
|
}
|
|
555
552
|
return
|
|
556
553
|
}
|
|
@@ -563,87 +560,18 @@ class Runtime extends EventEmitter {
|
|
|
563
560
|
}, COLLECT_METRICS_TIMEOUT).unref()
|
|
564
561
|
}
|
|
565
562
|
|
|
566
|
-
async
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
const runtimeLogFiles = await this.#getRuntimeLogFiles(runtimePID)
|
|
571
|
-
|
|
572
|
-
if (runtimeLogFiles.length === 0) {
|
|
573
|
-
writableStream.end()
|
|
574
|
-
return
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
let latestFileId = parseInt(runtimeLogFiles.at(-1).slice('logs.'.length))
|
|
578
|
-
|
|
579
|
-
let fileStream = null
|
|
580
|
-
let fileId = startLogId ?? latestFileId
|
|
581
|
-
let isClosed = false
|
|
563
|
+
async addLoggerDestination (writableStream) {
|
|
564
|
+
// Add the stream - We output everything we get
|
|
565
|
+
this.#loggerDestination.add({ stream: writableStream, level: 1 })
|
|
582
566
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const watcher = watch(runtimeLogsDir, async (event, filename) => {
|
|
586
|
-
if (event === 'rename' && filename.startsWith('logs')) {
|
|
587
|
-
const logFileId = parseInt(filename.slice('logs.'.length))
|
|
588
|
-
if (logFileId > latestFileId) {
|
|
589
|
-
latestFileId = logFileId
|
|
590
|
-
fileStream.unwatch()
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
}).unref()
|
|
594
|
-
|
|
595
|
-
const streamLogFile = () => {
|
|
596
|
-
if (fileId > endLogId) {
|
|
597
|
-
writableStream.end()
|
|
598
|
-
return
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
const fileName = 'logs.' + fileId
|
|
602
|
-
const filePath = join(runtimeLogsDir, fileName)
|
|
603
|
-
|
|
604
|
-
const prevFileStream = fileStream
|
|
605
|
-
|
|
606
|
-
fileStream = ts.createReadStream(filePath)
|
|
607
|
-
fileStream.pipe(writableStream, { end: false, persistent: false })
|
|
608
|
-
|
|
609
|
-
if (prevFileStream) {
|
|
610
|
-
prevFileStream.unpipe(writableStream)
|
|
611
|
-
prevFileStream.destroy()
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
fileStream.on('close', () => {
|
|
615
|
-
if (latestFileId > fileId && !isClosed) {
|
|
616
|
-
streamLogFile(++fileId)
|
|
617
|
-
}
|
|
618
|
-
})
|
|
619
|
-
|
|
620
|
-
fileStream.on('error', err => {
|
|
621
|
-
isClosed = true
|
|
622
|
-
logger.error(err, 'Error streaming log file')
|
|
623
|
-
fileStream.destroy()
|
|
624
|
-
watcher.close()
|
|
625
|
-
writableStream.end()
|
|
626
|
-
})
|
|
627
|
-
|
|
628
|
-
fileStream.on('eof', () => {
|
|
629
|
-
if (fileId >= endLogId) {
|
|
630
|
-
writableStream.end()
|
|
631
|
-
return
|
|
632
|
-
}
|
|
633
|
-
if (latestFileId > fileId) {
|
|
634
|
-
fileStream.unwatch()
|
|
635
|
-
}
|
|
636
|
-
})
|
|
637
|
-
|
|
638
|
-
return fileStream
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
streamLogFile(fileId)
|
|
567
|
+
// Immediately get the counter of the lastId so we can use it to later remove it
|
|
568
|
+
const id = this.#loggerDestination.lastId
|
|
642
569
|
|
|
643
570
|
const onClose = () => {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
571
|
+
writableStream.removeListener('close', onClose)
|
|
572
|
+
writableStream.removeListener('error', onClose)
|
|
573
|
+
this.removeListener('closed', onClose)
|
|
574
|
+
this.#loggerDestination.remove(id)
|
|
647
575
|
}
|
|
648
576
|
|
|
649
577
|
writableStream.on('close', onClose)
|
|
@@ -1004,44 +932,6 @@ class Runtime extends EventEmitter {
|
|
|
1004
932
|
this.servicesConfigsPatches.delete(id)
|
|
1005
933
|
}
|
|
1006
934
|
|
|
1007
|
-
async getLogIds (runtimePID) {
|
|
1008
|
-
runtimePID = runtimePID ?? process.pid
|
|
1009
|
-
|
|
1010
|
-
const runtimeLogFiles = await this.#getRuntimeLogFiles(runtimePID)
|
|
1011
|
-
const runtimeLogIds = []
|
|
1012
|
-
|
|
1013
|
-
for (const logFile of runtimeLogFiles) {
|
|
1014
|
-
const logId = parseInt(logFile.slice('logs.'.length))
|
|
1015
|
-
runtimeLogIds.push(logId)
|
|
1016
|
-
}
|
|
1017
|
-
return runtimeLogIds
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
async getAllLogIds () {
|
|
1021
|
-
const runtimesLogFiles = await this.#getAllLogsFiles()
|
|
1022
|
-
const runtimesLogsIds = []
|
|
1023
|
-
|
|
1024
|
-
for (const runtime of runtimesLogFiles) {
|
|
1025
|
-
const runtimeLogIds = []
|
|
1026
|
-
for (const logFile of runtime.runtimeLogFiles) {
|
|
1027
|
-
const logId = parseInt(logFile.slice('logs.'.length))
|
|
1028
|
-
runtimeLogIds.push(logId)
|
|
1029
|
-
}
|
|
1030
|
-
runtimesLogsIds.push({
|
|
1031
|
-
pid: runtime.runtimePID,
|
|
1032
|
-
indexes: runtimeLogIds
|
|
1033
|
-
})
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
return runtimesLogsIds
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
async getLogFileStream (logFileId, runtimePID) {
|
|
1040
|
-
const runtimeLogsDir = this.#getRuntimeLogsDir(runtimePID)
|
|
1041
|
-
const filePath = join(runtimeLogsDir, `logs.${logFileId}`)
|
|
1042
|
-
return createReadStream(filePath)
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
935
|
#getHttpCacheValue ({ request }) {
|
|
1046
936
|
if (!this.#sharedHttpCache) {
|
|
1047
937
|
return
|
|
@@ -1111,6 +1001,33 @@ class Runtime extends EventEmitter {
|
|
|
1111
1001
|
return super.emit(event, payload)
|
|
1112
1002
|
}
|
|
1113
1003
|
|
|
1004
|
+
async updateSharedContext (options = {}) {
|
|
1005
|
+
const { context, overwrite = false } = options
|
|
1006
|
+
|
|
1007
|
+
const sharedContext = overwrite ? {} : this.#sharedContext
|
|
1008
|
+
Object.assign(sharedContext, context)
|
|
1009
|
+
|
|
1010
|
+
this.#sharedContext = sharedContext
|
|
1011
|
+
|
|
1012
|
+
const promises = []
|
|
1013
|
+
for (const worker of this.#workers.values()) {
|
|
1014
|
+
promises.push(sendViaITC(worker, 'setSharedContext', sharedContext))
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const results = await Promise.allSettled(promises)
|
|
1018
|
+
for (const result of results) {
|
|
1019
|
+
if (result.status === 'rejected') {
|
|
1020
|
+
this.logger.error({ err: result.reason }, 'Cannot update shared context')
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
return sharedContext
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
getSharedContext () {
|
|
1028
|
+
return this.#sharedContext
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1114
1031
|
async #setDispatcher (undiciConfig) {
|
|
1115
1032
|
const config = this.#config
|
|
1116
1033
|
|
|
@@ -1226,8 +1143,7 @@ class Runtime extends EventEmitter {
|
|
|
1226
1143
|
count: workersCount
|
|
1227
1144
|
},
|
|
1228
1145
|
inspectorOptions,
|
|
1229
|
-
dirname: this.#root
|
|
1230
|
-
runtimeLogsDir: this.#runtimeLogsDir
|
|
1146
|
+
dirname: this.#root
|
|
1231
1147
|
},
|
|
1232
1148
|
argv: serviceConfig.arguments,
|
|
1233
1149
|
execArgv,
|
|
@@ -1549,16 +1465,24 @@ class Runtime extends EventEmitter {
|
|
|
1549
1465
|
gracePeriod > 0 ? gracePeriod : 1
|
|
1550
1466
|
)
|
|
1551
1467
|
}
|
|
1552
|
-
} catch (
|
|
1468
|
+
} catch (err) {
|
|
1469
|
+
const error = ensureError(err)
|
|
1470
|
+
|
|
1553
1471
|
// TODO: handle port allocation error here
|
|
1554
|
-
if (error.code === 'EADDRINUSE') throw error
|
|
1472
|
+
if (error.code === 'EADDRINUSE' || error.code === 'EACCES') throw error
|
|
1555
1473
|
|
|
1556
1474
|
this.#cleanupWorker(worker)
|
|
1557
1475
|
|
|
1558
1476
|
if (worker[kWorkerStatus] !== 'exited') {
|
|
1559
1477
|
// This prevent the exit handler to restart service
|
|
1560
1478
|
worker[kWorkerStatus] = 'exited'
|
|
1561
|
-
|
|
1479
|
+
|
|
1480
|
+
// Wait for the worker to exit gracefully, otherwise we terminate it
|
|
1481
|
+
const waitTimeout = await executeWithTimeout(once(worker, 'exit'), config.gracefulShutdown.service)
|
|
1482
|
+
|
|
1483
|
+
if (waitTimeout === kTimeout) {
|
|
1484
|
+
await worker.terminate()
|
|
1485
|
+
}
|
|
1562
1486
|
}
|
|
1563
1487
|
|
|
1564
1488
|
this.emit('service:worker:start:error', { ...eventPayload, error })
|
|
@@ -1836,7 +1760,11 @@ class Runtime extends EventEmitter {
|
|
|
1836
1760
|
})
|
|
1837
1761
|
}
|
|
1838
1762
|
|
|
1839
|
-
|
|
1763
|
+
try {
|
|
1764
|
+
this.#workersBroadcastChannel.postMessage(workers)
|
|
1765
|
+
} catch (err) {
|
|
1766
|
+
this.logger?.error({ err }, 'Error when broadcasting workers')
|
|
1767
|
+
}
|
|
1840
1768
|
}
|
|
1841
1769
|
|
|
1842
1770
|
async #getWorkerMessagingChannel ({ service, worker }, context) {
|
|
@@ -1867,49 +1795,6 @@ class Runtime extends EventEmitter {
|
|
|
1867
1795
|
return packageJson
|
|
1868
1796
|
}
|
|
1869
1797
|
|
|
1870
|
-
#getRuntimeLogsDir (runtimePID) {
|
|
1871
|
-
return join(this.#runtimeTmpDir, runtimePID.toString(), 'logs')
|
|
1872
|
-
}
|
|
1873
|
-
|
|
1874
|
-
async #getRuntimeLogFiles (runtimePID) {
|
|
1875
|
-
const runtimeLogsDir = this.#getRuntimeLogsDir(runtimePID)
|
|
1876
|
-
const runtimeLogsFiles = await readdir(runtimeLogsDir)
|
|
1877
|
-
return runtimeLogsFiles
|
|
1878
|
-
.filter(file => file.startsWith('logs'))
|
|
1879
|
-
.sort((log1, log2) => {
|
|
1880
|
-
const index1 = parseInt(log1.slice('logs.'.length))
|
|
1881
|
-
const index2 = parseInt(log2.slice('logs.'.length))
|
|
1882
|
-
return index1 - index2
|
|
1883
|
-
})
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1886
|
-
async #getAllLogsFiles () {
|
|
1887
|
-
try {
|
|
1888
|
-
await access(this.#runtimeTmpDir)
|
|
1889
|
-
} catch (err) {
|
|
1890
|
-
this.logger.error({ err: ensureLoggableError(err) }, 'Cannot access temporary folder.')
|
|
1891
|
-
return []
|
|
1892
|
-
}
|
|
1893
|
-
|
|
1894
|
-
const runtimePIDs = await readdir(this.#runtimeTmpDir)
|
|
1895
|
-
const runtimesLogFiles = []
|
|
1896
|
-
|
|
1897
|
-
for (const runtimePID of runtimePIDs) {
|
|
1898
|
-
const runtimeLogsDir = this.#getRuntimeLogsDir(runtimePID)
|
|
1899
|
-
const runtimeLogsDirStat = await stat(runtimeLogsDir)
|
|
1900
|
-
const runtimeLogFiles = await this.#getRuntimeLogFiles(runtimePID)
|
|
1901
|
-
const lastModified = runtimeLogsDirStat.mtime
|
|
1902
|
-
|
|
1903
|
-
runtimesLogFiles.push({
|
|
1904
|
-
runtimePID: parseInt(runtimePID),
|
|
1905
|
-
runtimeLogFiles,
|
|
1906
|
-
lastModified
|
|
1907
|
-
})
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
return runtimesLogFiles.sort((runtime1, runtime2) => runtime1.lastModified - runtime2.lastModified)
|
|
1911
|
-
}
|
|
1912
|
-
|
|
1913
1798
|
#handleWorkerStandardStreams (worker, serviceId, workerId) {
|
|
1914
1799
|
const binding = { name: serviceId }
|
|
1915
1800
|
|
package/lib/schema.js
CHANGED
package/lib/shared-http-cache.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { join } = require('node:path')
|
|
4
|
-
const { loadModule } = require('@platformatic/
|
|
4
|
+
const { loadModule } = require('@platformatic/foundation')
|
|
5
5
|
const MemoryCacheStore = require('@platformatic/undici-cache-memory')
|
|
6
6
|
const { createRequire } = require('node:module')
|
|
7
7
|
|
package/lib/upgrade.js
CHANGED
package/lib/utils.js
CHANGED
|
@@ -20,14 +20,8 @@ function getRuntimeTmpDir (runtimeDir) {
|
|
|
20
20
|
return join(platformaticTmpDir, runtimeDirHash)
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
function getRuntimeLogsDir (runtimeDir, runtimePID) {
|
|
24
|
-
const runtimeTmpDir = getRuntimeTmpDir(runtimeDir)
|
|
25
|
-
return join(runtimeTmpDir, runtimePID.toString(), 'logs')
|
|
26
|
-
}
|
|
27
|
-
|
|
28
23
|
module.exports = {
|
|
29
24
|
getArrayDifference,
|
|
30
|
-
getRuntimeLogsDir,
|
|
31
25
|
getRuntimeTmpDir,
|
|
32
26
|
getServiceUrl
|
|
33
27
|
}
|
package/lib/worker/app.js
CHANGED
|
@@ -11,11 +11,11 @@ const {
|
|
|
11
11
|
FileWatcher,
|
|
12
12
|
listRecognizedConfigurationFiles,
|
|
13
13
|
loadConfigurationModule,
|
|
14
|
-
loadConfiguration
|
|
15
|
-
|
|
14
|
+
loadConfiguration,
|
|
15
|
+
ensureLoggableError
|
|
16
|
+
} = require('@platformatic/foundation')
|
|
16
17
|
const { getGlobalDispatcher, setGlobalDispatcher } = require('undici')
|
|
17
18
|
const debounce = require('debounce')
|
|
18
|
-
|
|
19
19
|
const errors = require('../errors')
|
|
20
20
|
const { getServiceUrl } = require('../utils')
|
|
21
21
|
|
|
@@ -129,10 +129,14 @@ class PlatformaticApp extends EventEmitter {
|
|
|
129
129
|
this.#updateDispatcher()
|
|
130
130
|
} catch (err) {
|
|
131
131
|
if (err.validationErrors) {
|
|
132
|
-
|
|
132
|
+
globalThis.platformatic.logger.error(
|
|
133
|
+
{ err: ensureLoggableError(err.validationErrors) },
|
|
134
|
+
'The application threw a validation error.'
|
|
135
|
+
)
|
|
136
|
+
|
|
133
137
|
process.exit(1)
|
|
134
138
|
} else {
|
|
135
|
-
this.#
|
|
139
|
+
this.#logAndThrow(err)
|
|
136
140
|
}
|
|
137
141
|
}
|
|
138
142
|
}
|
|
@@ -147,7 +151,7 @@ class PlatformaticApp extends EventEmitter {
|
|
|
147
151
|
try {
|
|
148
152
|
await this.stackable.init?.()
|
|
149
153
|
} catch (err) {
|
|
150
|
-
this.#
|
|
154
|
+
this.#logAndThrow(err)
|
|
151
155
|
}
|
|
152
156
|
|
|
153
157
|
if (this.#watch) {
|
|
@@ -181,8 +185,8 @@ class PlatformaticApp extends EventEmitter {
|
|
|
181
185
|
this.emit('start')
|
|
182
186
|
}
|
|
183
187
|
|
|
184
|
-
async stop () {
|
|
185
|
-
if (!this.#started || this.#starting) {
|
|
188
|
+
async stop (force = false) {
|
|
189
|
+
if (!force && (!this.#started || this.#starting)) {
|
|
186
190
|
throw new errors.ApplicationNotStartedError()
|
|
187
191
|
}
|
|
188
192
|
|
|
@@ -205,6 +209,18 @@ class PlatformaticApp extends EventEmitter {
|
|
|
205
209
|
}
|
|
206
210
|
|
|
207
211
|
async getMetrics ({ format }) {
|
|
212
|
+
const dispatcher = getGlobalDispatcher()
|
|
213
|
+
if (globalThis.platformatic?.onHttpStatsFree && dispatcher?.stats) {
|
|
214
|
+
for (const url in dispatcher.stats) {
|
|
215
|
+
const { free, connected, pending, queued, running, size } = dispatcher.stats[url]
|
|
216
|
+
globalThis.platformatic.onHttpStatsFree(url, free || 0)
|
|
217
|
+
globalThis.platformatic.onHttpStatsConnected(url, connected || 0)
|
|
218
|
+
globalThis.platformatic.onHttpStatsPending(url, pending || 0)
|
|
219
|
+
globalThis.platformatic.onHttpStatsQueued(url, queued || 0)
|
|
220
|
+
globalThis.platformatic.onHttpStatsRunning(url, running || 0)
|
|
221
|
+
globalThis.platformatic.onHttpStatsSize(url, size || 0)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
208
224
|
return this.stackable.getMetrics({ format })
|
|
209
225
|
}
|
|
210
226
|
|
|
@@ -251,9 +267,9 @@ class PlatformaticApp extends EventEmitter {
|
|
|
251
267
|
}
|
|
252
268
|
}
|
|
253
269
|
|
|
254
|
-
#
|
|
255
|
-
|
|
256
|
-
|
|
270
|
+
#logAndThrow (err) {
|
|
271
|
+
globalThis.platformatic.logger.error({ err: ensureLoggableError(err) }, 'The application threw an error.')
|
|
272
|
+
throw err
|
|
257
273
|
}
|
|
258
274
|
|
|
259
275
|
#updateDispatcher () {
|
package/lib/worker/itc.js
CHANGED
|
@@ -4,6 +4,7 @@ const { once } = require('node:events')
|
|
|
4
4
|
const { parentPort, workerData } = require('node:worker_threads')
|
|
5
5
|
|
|
6
6
|
const { ITC } = require('@platformatic/itc')
|
|
7
|
+
const { ensureLoggableError } = require('@platformatic/foundation')
|
|
7
8
|
const { Unpromise } = require('@watchable/unpromise')
|
|
8
9
|
|
|
9
10
|
const errors = require('../errors')
|
|
@@ -56,7 +57,13 @@ async function waitEventFromITC (worker, event) {
|
|
|
56
57
|
return safeHandleInITC(worker, () => once(worker[kITC], event))
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
function
|
|
60
|
+
async function closeITC (dispatcher, itc, messaging) {
|
|
61
|
+
await dispatcher.interceptor.close()
|
|
62
|
+
itc.close()
|
|
63
|
+
messaging.close()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function setupITC (app, service, dispatcher, sharedContext) {
|
|
60
67
|
const messaging = new MessagingITC(app.appConfig.id, workerData.config)
|
|
61
68
|
|
|
62
69
|
Object.assign(globalThis.platformatic ?? {}, {
|
|
@@ -79,7 +86,14 @@ function setupITC (app, service, dispatcher) {
|
|
|
79
86
|
// This gives a chance to a stackable to perform custom logic
|
|
80
87
|
globalThis.platformatic.events.emit('start')
|
|
81
88
|
|
|
82
|
-
|
|
89
|
+
try {
|
|
90
|
+
await app.start()
|
|
91
|
+
} catch (e) {
|
|
92
|
+
await app.stop(true)
|
|
93
|
+
await closeITC(dispatcher, itc, messaging)
|
|
94
|
+
|
|
95
|
+
throw ensureLoggableError(e)
|
|
96
|
+
}
|
|
83
97
|
}
|
|
84
98
|
|
|
85
99
|
if (service.entrypoint) {
|
|
@@ -104,9 +118,7 @@ function setupITC (app, service, dispatcher) {
|
|
|
104
118
|
await app.stop()
|
|
105
119
|
}
|
|
106
120
|
|
|
107
|
-
await dispatcher
|
|
108
|
-
itc.close()
|
|
109
|
-
messaging.close()
|
|
121
|
+
await closeITC(dispatcher, itc, messaging)
|
|
110
122
|
},
|
|
111
123
|
|
|
112
124
|
async build () {
|
|
@@ -210,6 +222,10 @@ function setupITC (app, service, dispatcher) {
|
|
|
210
222
|
}
|
|
211
223
|
},
|
|
212
224
|
|
|
225
|
+
setSharedContext (context) {
|
|
226
|
+
sharedContext._set(context)
|
|
227
|
+
},
|
|
228
|
+
|
|
213
229
|
saveMessagingChannel (channel) {
|
|
214
230
|
messaging.addSource(channel)
|
|
215
231
|
}
|
package/lib/worker/main.js
CHANGED
|
@@ -16,12 +16,13 @@ const {
|
|
|
16
16
|
getPrivateSymbol,
|
|
17
17
|
buildPinoFormatters,
|
|
18
18
|
buildPinoTimestamp
|
|
19
|
-
} = require('@platformatic/
|
|
19
|
+
} = require('@platformatic/foundation')
|
|
20
20
|
const dotenv = require('dotenv')
|
|
21
21
|
const pino = require('pino')
|
|
22
22
|
const { fetch } = require('undici')
|
|
23
23
|
|
|
24
24
|
const { PlatformaticApp } = require('./app')
|
|
25
|
+
const { SharedContext } = require('./shared-context')
|
|
25
26
|
const { setupITC } = require('./itc')
|
|
26
27
|
const { setDispatcher } = require('./interceptors')
|
|
27
28
|
const { kId, kITC, kStderrMarker } = require('./symbols')
|
|
@@ -64,6 +65,11 @@ function patchLogging () {
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
function createLogger () {
|
|
68
|
+
// Do not propagate runtime transports to the worker
|
|
69
|
+
if (workerData.config.logger) {
|
|
70
|
+
delete workerData.config.logger.transport
|
|
71
|
+
}
|
|
72
|
+
|
|
67
73
|
const pinoOptions = {
|
|
68
74
|
level: 'trace',
|
|
69
75
|
name: workerData.serviceConfig.id,
|
|
@@ -187,8 +193,15 @@ async function main () {
|
|
|
187
193
|
}
|
|
188
194
|
}
|
|
189
195
|
|
|
196
|
+
const sharedContext = new SharedContext()
|
|
197
|
+
// Limit the amount of methods a user can call
|
|
198
|
+
globalThis.platformatic.sharedContext = {
|
|
199
|
+
get: () => sharedContext.get(),
|
|
200
|
+
update: (...args) => sharedContext.update(...args)
|
|
201
|
+
}
|
|
202
|
+
|
|
190
203
|
// Setup interaction with parent port
|
|
191
|
-
const itc = setupITC(app, service, threadDispatcher)
|
|
204
|
+
const itc = setupITC(app, service, threadDispatcher, sharedContext)
|
|
192
205
|
globalThis[kITC] = itc
|
|
193
206
|
|
|
194
207
|
// Get the dependencies
|
package/lib/worker/messaging.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { executeWithTimeout, kTimeout } = require('@platformatic/
|
|
3
|
+
const { executeWithTimeout, kTimeout } = require('@platformatic/foundation')
|
|
4
4
|
const { ITC, generateResponse, sanitize } = require('@platformatic/itc')
|
|
5
5
|
const errors = require('../errors')
|
|
6
6
|
const { RoundRobinMap } = require('./round-robin-map')
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { kITC } = require('./symbols')
|
|
4
|
+
|
|
5
|
+
class SharedContext {
|
|
6
|
+
constructor () {
|
|
7
|
+
this.sharedContext = null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
update (context, options = {}) {
|
|
11
|
+
return globalThis[kITC].send('updateSharedContext', { ...options, context })
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
get () {
|
|
15
|
+
if (this.sharedContext === null) {
|
|
16
|
+
this.sharedContext = globalThis[kITC].send('getSharedContext')
|
|
17
|
+
}
|
|
18
|
+
return this.sharedContext
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
_set (context) {
|
|
22
|
+
this.sharedContext = context
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { SharedContext }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/runtime",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"author": "Platformatic Inc. <oss@platformatic.dev> (https://platformatic.dev)",
|
|
@@ -33,58 +33,57 @@
|
|
|
33
33
|
"typescript": "^5.5.4",
|
|
34
34
|
"undici-oidc-interceptor": "^0.5.0",
|
|
35
35
|
"why-is-node-running": "^2.2.2",
|
|
36
|
-
"@platformatic/composer": "3.0.0-alpha.
|
|
37
|
-
"@platformatic/db": "3.0.0-alpha.
|
|
38
|
-
"@platformatic/node": "3.0.0-alpha.
|
|
39
|
-
"@platformatic/service": "3.0.0-alpha.
|
|
40
|
-
"@platformatic/sql-
|
|
41
|
-
"@platformatic/sql-
|
|
36
|
+
"@platformatic/composer": "3.0.0-alpha.2",
|
|
37
|
+
"@platformatic/db": "3.0.0-alpha.2",
|
|
38
|
+
"@platformatic/node": "3.0.0-alpha.2",
|
|
39
|
+
"@platformatic/service": "3.0.0-alpha.2",
|
|
40
|
+
"@platformatic/sql-mapper": "3.0.0-alpha.2",
|
|
41
|
+
"@platformatic/sql-graphql": "3.0.0-alpha.2"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@fastify/accepts": "^5.0.0",
|
|
45
45
|
"@fastify/error": "^4.0.0",
|
|
46
46
|
"@fastify/websocket": "^11.0.0",
|
|
47
47
|
"@hapi/topo": "^6.0.2",
|
|
48
|
-
"@opentelemetry/api": "^1.
|
|
48
|
+
"@opentelemetry/api": "^1.9.0",
|
|
49
49
|
"@platformatic/undici-cache-memory": "^0.8.1",
|
|
50
50
|
"@watchable/unpromise": "^1.0.2",
|
|
51
51
|
"change-case-all": "^2.1.0",
|
|
52
52
|
"close-with-grace": "^2.0.0",
|
|
53
|
-
"
|
|
53
|
+
"colorette": "^2.0.20",
|
|
54
54
|
"cron": "^4.1.0",
|
|
55
55
|
"debounce": "^2.0.0",
|
|
56
56
|
"dotenv": "^16.4.5",
|
|
57
57
|
"dotenv-tool": "^0.1.1",
|
|
58
|
-
"es-main": "^1.3.0",
|
|
59
58
|
"fastest-levenshtein": "^1.0.16",
|
|
60
59
|
"fastify": "^5.0.0",
|
|
61
60
|
"graphql": "^16.8.1",
|
|
62
61
|
"help-me": "^5.0.0",
|
|
63
62
|
"minimist": "^1.2.8",
|
|
64
|
-
"pino": "^9.
|
|
63
|
+
"pino": "^9.9.0",
|
|
65
64
|
"pino-pretty": "^13.0.0",
|
|
66
|
-
"pino-roll": "^2.0.0",
|
|
67
65
|
"prom-client": "^15.1.2",
|
|
68
66
|
"semgrator": "^0.3.0",
|
|
69
67
|
"sonic-boom": "^4.2.0",
|
|
70
|
-
"tail-file-stream": "^0.2.0",
|
|
71
68
|
"undici": "^7.0.0",
|
|
72
69
|
"undici-thread-interceptor": "^0.14.0",
|
|
73
70
|
"ws": "^8.16.0",
|
|
74
|
-
"@platformatic/
|
|
75
|
-
"@platformatic/
|
|
76
|
-
"@platformatic/
|
|
77
|
-
"@platformatic/itc": "3.0.0-alpha.
|
|
78
|
-
"@platformatic/
|
|
79
|
-
"@platformatic/
|
|
80
|
-
|
|
71
|
+
"@platformatic/basic": "3.0.0-alpha.2",
|
|
72
|
+
"@platformatic/generators": "3.0.0-alpha.2",
|
|
73
|
+
"@platformatic/foundation": "3.0.0-alpha.2",
|
|
74
|
+
"@platformatic/itc": "3.0.0-alpha.2",
|
|
75
|
+
"@platformatic/metrics": "3.0.0-alpha.2",
|
|
76
|
+
"@platformatic/telemetry": "3.0.0-alpha.2"
|
|
77
|
+
},
|
|
78
|
+
"engines": {
|
|
79
|
+
"node": ">=22.18.0"
|
|
81
80
|
},
|
|
82
81
|
"scripts": {
|
|
83
82
|
"test": "pnpm run lint && borp --concurrency=1 --timeout=1200000",
|
|
84
83
|
"test:main": "borp --concurrency=1 --timeout=1200000 test/*.test.js test/*.test.mjs test/versions/*.test.js test/versions/*.test.mjs",
|
|
85
84
|
"test:api": "borp --concurrency=1 --timeout=1200000 test/api/*.test.js test/api/*.test.mjs test/management-api/*.test.js test/management-api/*.test.mjs",
|
|
86
85
|
"test:cli": "borp --concurrency=1 --timeout=1200000 test/cli/*.test.js test/cli/*.test.mjs test/cli/**/*.test.js test/cli/**/*.test.mjs",
|
|
87
|
-
"test:start": "borp --concurrency=1 --timeout=1200000 test/start/*.test.js test/start/*.test.mjs",
|
|
86
|
+
"test:start": "borp --concurrency=1 --reporter=tap --timeout=1200000 test/start/*.test.js test/start/*.test.mjs",
|
|
88
87
|
"test:multiple-workers": "borp --concurrency=1 --timeout=1200000 test/multiple-workers/*.test.js test/multiple-workers/*.test.mjs",
|
|
89
88
|
"coverage": "pnpm run lint && borp -X fixtures -X test -C --concurrency=1 --timeout=1200000 ",
|
|
90
89
|
"gen-schema": "node lib/schema.js > schema.json",
|
package/schema.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.0.0-alpha.
|
|
2
|
+
"$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.0.0-alpha.2.json",
|
|
3
3
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
4
|
"title": "Platformatic Runtime Config",
|
|
5
5
|
"type": "object",
|