@platformatic/runtime 1.36.0 → 1.36.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.
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "https://platformatic.dev/schemas/v1.20.0/runtime",
3
+ "entrypoint": "a",
4
+ "autoload": {
5
+ "path": "./services"
6
+ },
7
+ "server": {
8
+ "hostname": "127.0.0.1",
9
+ "port": "{PORT}",
10
+ "logger": {
11
+ "level": "info"
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "https://platformatic.dev/schemas/v1.3.0/service",
3
+ "server": {
4
+ "logger": {
5
+ "level": "warn"
6
+ }
7
+ },
8
+ "plugins": {
9
+ "paths": [{
10
+ "path": "plugin.js",
11
+ "encapsulate": false
12
+ }]
13
+ }
14
+ }
@@ -0,0 +1,9 @@
1
+ 'use strict'
2
+
3
+ module.exports = async function (fastify, options) {
4
+ fastify.post('/crash', async (_, reply) => {
5
+ setImmediate(() => {
6
+ throw new Error('Crash!')
7
+ })
8
+ })
9
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://platformatic.dev/schemas/v1.20.0/runtime",
2
+ "$schema": "https://platformatic.dev/schemas/v1.36.0/runtime",
3
3
  "entrypoint": "a",
4
4
  "autoload": {
5
5
  "path": "./services"
package/lib/api-client.js CHANGED
@@ -88,8 +88,8 @@ class RuntimeApiClient extends EventEmitter {
88
88
  }
89
89
 
90
90
  async start () {
91
- this.started = true
92
91
  const address = await this.#sendCommand('plt:start-services')
92
+ this.started = true
93
93
  this.emit('start', address)
94
94
  return address
95
95
  }
package/lib/errors.js CHANGED
@@ -25,7 +25,6 @@ module.exports = {
25
25
  FailedToUnlinkManagementApiSocket: createError(`${ERROR_PREFIX}_FAILED_TO_UNLINK_MANAGEMENT_API_SOCKET`, 'Failed to unlink management API socket "%s"'),
26
26
  LogFileNotFound: createError(`${ERROR_PREFIX}_LOG_FILE_NOT_FOUND`, 'Log file with index %s not found', 404),
27
27
  CannotFindGeneratorForTemplateError: createError(`${ERROR_PREFIX}_CANNOT_FIND_GENERATOR_FOR_TEMPLATE`, 'Cannot find a generator for template "%s"'),
28
- WorkerExitCodeError: createError(`${ERROR_PREFIX}_WORKER_EXIT_CODE`, 'The worker exited with non-zero exit code "%s"'),
29
28
  WorkerIsRequired: createError(`${ERROR_PREFIX}_REQUIRED_WORKER`, 'The worker parameter is required'),
30
29
 
31
30
  // TODO: should remove next one as it's not used anymore
@@ -90,7 +90,7 @@ class RuntimeGenerator extends BaseGenerator {
90
90
  this.addServicesDependencies()
91
91
 
92
92
  this.addEnvVars({
93
- PLT_SERVER_HOSTNAME: '0.0.0.0',
93
+ PLT_SERVER_HOSTNAME: '127.0.0.1',
94
94
  PORT: this.config.port || 3042,
95
95
  PLT_SERVER_LOGGER_LEVEL: this.config.logLevel || 'info',
96
96
  PLT_MANAGEMENT_API: true
@@ -11,8 +11,7 @@ const errors = require('./errors')
11
11
 
12
12
  const PLATFORMATIC_TMP_DIR = join(tmpdir(), 'platformatic', 'runtimes')
13
13
 
14
- async function createManagementApi (runtimeApiClient) {
15
- const app = fastify()
14
+ async function managementApiPlugin (app, opts) {
16
15
  app.log.warn(
17
16
  'Runtime Management API is in the experimental stage. ' +
18
17
  'The feature is not subject to semantic versioning rules. ' +
@@ -20,174 +19,170 @@ async function createManagementApi (runtimeApiClient) {
20
19
  'Use of the feature is not recommended in production environments.'
21
20
  )
22
21
 
23
- app.register(require('@fastify/websocket'))
24
-
25
- app.register(async (app) => {
26
- app.get('/metadata', async () => {
27
- return runtimeApiClient.getRuntimeMetadata()
28
- })
29
-
30
- app.get('/config', async () => {
31
- return runtimeApiClient.getRuntimeConfig()
32
- })
33
-
34
- app.get('/env', async () => {
35
- return process.env
36
- })
37
-
38
- app.post('/stop', async () => {
39
- app.log.debug('stop services')
40
- await runtimeApiClient.close()
41
- })
42
-
43
- app.post('/reload', async () => {
44
- app.log.debug('reload services')
45
- await runtimeApiClient.restart()
46
- })
47
-
48
- app.get('/services', async () => {
49
- return runtimeApiClient.getServices()
50
- })
51
-
52
- app.get('/services/:id', async (request) => {
53
- const { id } = request.params
54
- app.log.debug('get service details', { id })
55
- return runtimeApiClient.getServiceDetails(id)
56
- })
57
-
58
- app.get('/services/:id/config', async (request) => {
59
- const { id } = request.params
60
- app.log.debug('get service config', { id })
61
- return runtimeApiClient.getServiceConfig(id)
62
- })
63
-
64
- app.post('/services/:id/start', async (request) => {
65
- const { id } = request.params
66
- app.log.debug('start service', { id })
67
- await runtimeApiClient.startService(id)
68
- })
22
+ const runtime = opts.runtime
23
+
24
+ app.get('/metadata', async () => {
25
+ return runtime.getRuntimeMetadata()
26
+ })
27
+
28
+ app.get('/config', async () => {
29
+ return runtime.getRuntimeConfig()
30
+ })
31
+
32
+ app.get('/env', async () => {
33
+ return process.env
34
+ })
35
+
36
+ app.post('/stop', async () => {
37
+ app.log.debug('stop services')
38
+ await runtime.close()
39
+ })
40
+
41
+ app.post('/reload', async () => {
42
+ app.log.debug('reload services')
43
+ await runtime.restart()
44
+ })
45
+
46
+ app.get('/services', async () => {
47
+ return runtime.getServices()
48
+ })
49
+
50
+ app.get('/services/:id', async (request) => {
51
+ const { id } = request.params
52
+ app.log.debug('get service details', { id })
53
+ return runtime.getServiceDetails(id)
54
+ })
55
+
56
+ app.get('/services/:id/config', async (request) => {
57
+ const { id } = request.params
58
+ app.log.debug('get service config', { id })
59
+ return runtime.getServiceConfig(id)
60
+ })
61
+
62
+ app.post('/services/:id/start', async (request) => {
63
+ const { id } = request.params
64
+ app.log.debug('start service', { id })
65
+ await runtime.startService(id)
66
+ })
67
+
68
+ app.post('/services/:id/stop', async (request) => {
69
+ const { id } = request.params
70
+ app.log.debug('stop service', { id })
71
+ await runtime.stopService(id)
72
+ })
73
+
74
+ app.all('/services/:id/proxy/*', async (request, reply) => {
75
+ const { id, '*': requestUrl } = request.params
76
+ app.log.debug('proxy request', { id, requestUrl })
77
+
78
+ delete request.headers.connection
79
+ delete request.headers['content-length']
80
+ delete request.headers['content-encoding']
81
+ delete request.headers['transfer-encoding']
82
+
83
+ const injectParams = {
84
+ method: request.method,
85
+ url: requestUrl || '/',
86
+ headers: request.headers,
87
+ query: request.query,
88
+ body: request.body
89
+ }
69
90
 
70
- app.post('/services/:id/stop', async (request) => {
71
- const { id } = request.params
72
- app.log.debug('stop service', { id })
73
- await runtimeApiClient.stopService(id)
74
- })
91
+ const res = await runtime.inject(id, injectParams)
92
+
93
+ reply
94
+ .code(res.statusCode)
95
+ .headers(res.headers)
96
+ .send(res.body)
97
+ })
98
+
99
+ app.get('/metrics/live', { websocket: true }, async (socket) => {
100
+ const cachedMetrics = runtime.getCachedMetrics()
101
+ if (cachedMetrics.length > 0) {
102
+ const serializedMetrics = cachedMetrics
103
+ .map((metric) => JSON.stringify(metric))
104
+ .join('\n')
105
+ socket.send(serializedMetrics + '\n')
106
+ }
75
107
 
76
- app.all('/services/:id/proxy/*', async (request, reply) => {
77
- const { id, '*': requestUrl } = request.params
78
- app.log.debug('proxy request', { id, requestUrl })
79
-
80
- delete request.headers.connection
81
- delete request.headers['content-length']
82
- delete request.headers['content-encoding']
83
- delete request.headers['transfer-encoding']
84
-
85
- const injectParams = {
86
- method: request.method,
87
- url: requestUrl || '/',
88
- headers: request.headers,
89
- query: request.query,
90
- body: request.body
91
- }
108
+ const eventHandler = (metrics) => {
109
+ const serializedMetrics = JSON.stringify(metrics)
110
+ socket.send(serializedMetrics + '\n')
111
+ }
92
112
 
93
- const res = await runtimeApiClient.inject(id, injectParams)
113
+ runtime.on('metrics', eventHandler)
94
114
 
95
- reply
96
- .code(res.statusCode)
97
- .headers(res.headers)
98
- .send(res.body)
115
+ socket.on('error', () => {
116
+ runtime.off('metrics', eventHandler)
99
117
  })
100
118
 
101
- app.get('/metrics/live', { websocket: true }, async (socket) => {
102
- const cachedMetrics = runtimeApiClient.getCachedMetrics()
103
- if (cachedMetrics.length > 0) {
104
- const serializedMetrics = cachedMetrics
105
- .map((metric) => JSON.stringify(metric))
106
- .join('\n')
107
- socket.send(serializedMetrics + '\n')
108
- }
109
-
110
- const eventHandler = (metrics) => {
111
- const serializedMetrics = JSON.stringify(metrics)
112
- socket.send(serializedMetrics + '\n')
113
- }
114
-
115
- runtimeApiClient.on('metrics', eventHandler)
116
-
117
- socket.on('error', () => {
118
- runtimeApiClient.off('metrics', eventHandler)
119
- })
120
-
121
- socket.on('close', () => {
122
- runtimeApiClient.off('metrics', eventHandler)
123
- })
119
+ socket.on('close', () => {
120
+ runtime.off('metrics', eventHandler)
124
121
  })
122
+ })
125
123
 
126
- app.get('/logs/live', { websocket: true }, async (socket, req) => {
127
- const startLogId = req.query.start ? parseInt(req.query.start) : null
124
+ app.get('/logs/live', { websocket: true }, async (socket, req) => {
125
+ const startLogId = req.query.start ? parseInt(req.query.start) : null
128
126
 
129
- if (startLogId) {
130
- const logIds = await runtimeApiClient.getLogIds()
131
- if (!logIds.includes(startLogId)) {
132
- throw new errors.LogFileNotFound(startLogId)
133
- }
127
+ if (startLogId) {
128
+ const logIds = await runtime.getLogIds()
129
+ if (!logIds.includes(startLogId)) {
130
+ throw new errors.LogFileNotFound(startLogId)
134
131
  }
132
+ }
135
133
 
136
- const stream = ws.createWebSocketStream(socket)
137
- runtimeApiClient.pipeLogsStream(stream, req.log, startLogId)
138
- })
139
-
140
- app.get('/logs/indexes', async (req) => {
141
- const returnAllIds = req.query.all === 'true'
134
+ const stream = ws.createWebSocketStream(socket)
135
+ runtime.pipeLogsStream(stream, req.log, startLogId)
136
+ })
142
137
 
143
- if (returnAllIds) {
144
- const runtimesLogsIds = await runtimeApiClient.getAllLogIds()
145
- return runtimesLogsIds
146
- }
138
+ app.get('/logs/indexes', async (req) => {
139
+ const returnAllIds = req.query.all === 'true'
147
140
 
148
- const runtimeLogsIds = await runtimeApiClient.getLogIds()
149
- return { indexes: runtimeLogsIds }
150
- })
141
+ if (returnAllIds) {
142
+ const runtimesLogsIds = await runtime.getAllLogIds()
143
+ return runtimesLogsIds
144
+ }
151
145
 
152
- app.get('/logs/all', async (req, reply) => {
153
- const runtimePID = parseInt(req.query.pid) || process.pid
146
+ const runtimeLogsIds = await runtime.getLogIds()
147
+ return { indexes: runtimeLogsIds }
148
+ })
154
149
 
155
- const logsIds = await runtimeApiClient.getLogIds(runtimePID)
156
- const startLogId = logsIds.at(0)
157
- const endLogId = logsIds.at(-1)
150
+ app.get('/logs/all', async (req, reply) => {
151
+ const runtimePID = parseInt(req.query.pid) || process.pid
158
152
 
159
- reply.hijack()
153
+ const logsIds = await runtime.getLogIds(runtimePID)
154
+ const startLogId = logsIds.at(0)
155
+ const endLogId = logsIds.at(-1)
160
156
 
161
- runtimeApiClient.pipeLogsStream(
162
- reply.raw,
163
- req.log,
164
- startLogId,
165
- endLogId,
166
- runtimePID
167
- )
168
- })
157
+ reply.hijack()
169
158
 
170
- app.get('/logs/:id', async (req) => {
171
- const logId = parseInt(req.params.id)
172
- const runtimePID = parseInt(req.query.pid) || process.pid
159
+ runtime.pipeLogsStream(
160
+ reply.raw,
161
+ req.log,
162
+ startLogId,
163
+ endLogId,
164
+ runtimePID
165
+ )
166
+ })
173
167
 
174
- const logIds = await runtimeApiClient.getLogIds(runtimePID)
175
- if (!logIds || !logIds.includes(logId)) {
176
- throw new errors.LogFileNotFound(logId)
177
- }
168
+ app.get('/logs/:id', async (req) => {
169
+ const logId = parseInt(req.params.id)
170
+ const runtimePID = parseInt(req.query.pid) || process.pid
178
171
 
179
- const logFileStream = await runtimeApiClient.getLogFileStream(
180
- logId,
181
- runtimePID
182
- )
183
- return logFileStream
184
- })
185
- }, { prefix: '/api/v1' })
172
+ const logIds = await runtime.getLogIds(runtimePID)
173
+ if (!logIds || !logIds.includes(logId)) {
174
+ throw new errors.LogFileNotFound(logId)
175
+ }
186
176
 
187
- return app
177
+ const logFileStream = await runtime.getLogFileStream(
178
+ logId,
179
+ runtimePID
180
+ )
181
+ return logFileStream
182
+ })
188
183
  }
189
184
 
190
- async function startManagementApi (runtimeApiClient, configManager) {
185
+ async function startManagementApi (runtime, configManager) {
191
186
  const runtimePID = process.pid
192
187
 
193
188
  try {
@@ -208,7 +203,9 @@ async function startManagementApi (runtimeApiClient, configManager) {
208
203
  socketPath = join(runtimePIDDir, 'socket')
209
204
  }
210
205
 
211
- const managementApi = await createManagementApi(runtimeApiClient)
206
+ const managementApi = fastify()
207
+ managementApi.register(require('@fastify/websocket'))
208
+ managementApi.register(managementApiPlugin, { runtime, prefix: '/api/v1' })
212
209
 
213
210
  managementApi.addHook('onClose', async () => {
214
211
  if (platform() !== 'win32') {
@@ -225,4 +222,4 @@ async function startManagementApi (runtimeApiClient, configManager) {
225
222
  }
226
223
  }
227
224
 
228
- module.exports = { startManagementApi, createManagementApi }
225
+ module.exports = { startManagementApi, managementApiPlugin }
package/lib/schema.js CHANGED
@@ -172,6 +172,13 @@ const platformaticRuntimeSchema = {
172
172
  }
173
173
  ]
174
174
  },
175
+ restartOnError: {
176
+ default: true,
177
+ anyOf: [
178
+ { type: 'boolean' },
179
+ { type: 'string' }
180
+ ]
181
+ },
175
182
  services: {
176
183
  type: 'array',
177
184
  items: {
package/lib/start.js CHANGED
@@ -4,7 +4,6 @@ const { once } = require('node:events')
4
4
  const inspector = require('node:inspector')
5
5
  const { join, resolve, dirname } = require('node:path')
6
6
  const { writeFile } = require('node:fs/promises')
7
- const { setTimeout: sleep } = require('node:timers/promises')
8
7
  const { pathToFileURL } = require('node:url')
9
8
  const { Worker } = require('node:worker_threads')
10
9
  const { start: serviceStart } = require('@platformatic/service')
@@ -13,7 +12,6 @@ const closeWithGrace = require('close-with-grace')
13
12
  const { loadConfig } = require('./load-config')
14
13
  const { startManagementApi } = require('./management-api')
15
14
  const { startPrometheusServer } = require('./prom-server.js')
16
- const { WorkerExitCodeError } = require('./errors')
17
15
  const { parseInspectorOptions, wrapConfigInRuntimeConfig } = require('./config')
18
16
  const { RuntimeApiClient, getRuntimeLogsDir } = require('./api-client.js')
19
17
  const errors = require('./errors')
@@ -64,19 +62,6 @@ async function buildRuntime (configManager, env = process.env) {
64
62
 
65
63
  let managementApi = null
66
64
 
67
- let exiting = false
68
- closeWithGrace((event, cb) => {
69
- exiting = true
70
- worker.postMessage(event)
71
- worker.once('exit', (code) => {
72
- if (code !== 0) {
73
- cb(new WorkerExitCodeError(code))
74
- return
75
- }
76
- cb()
77
- })
78
- })
79
-
80
65
  if (config.hotReload) {
81
66
  /* c8 ignore next 3 */
82
67
  process.on('SIGUSR2', () => {
@@ -93,7 +78,7 @@ async function buildRuntime (configManager, env = process.env) {
93
78
  worker.on('exit', (code) => {
94
79
  // runtimeApiClient.started can be false if a stop command was issued
95
80
  // via the management API.
96
- if (exiting || !runtimeApiClient.started) {
81
+ if (config.restartOnError === false || !runtimeApiClient.started) {
97
82
  // We must stop those here in case the `closeWithGrace` callback
98
83
  // was not called.
99
84
  configManager.fileWatcher?.stopWatching()
@@ -103,8 +88,12 @@ async function buildRuntime (configManager, env = process.env) {
103
88
 
104
89
  worker = startWorker({ config, dirname, runtimeLogsDir }, env)
105
90
  setupExit()
106
- once(worker, 'message').then(() => {
107
- runtimeApiClient.setWorker(worker)
91
+
92
+ once(worker, 'message').then((msg) => {
93
+ runtimeApiClient.setWorker(worker).catch(() => {
94
+ // TODO: currently we restart if the worker fails to start intermitently
95
+ // should we limit this to a number of retries?
96
+ })
108
97
  })
109
98
  })
110
99
  }
@@ -162,7 +151,16 @@ async function startCommand (args) {
162
151
  runtime = await buildRuntime(wrappedConfig)
163
152
  }
164
153
 
165
- return await runtime.start()
154
+ const res = await runtime.start()
155
+
156
+ closeWithGrace(async (event) => {
157
+ if (event.err instanceof Error) {
158
+ console.error(event.err)
159
+ }
160
+ await runtime.close()
161
+ })
162
+
163
+ return res
166
164
  } catch (err) {
167
165
  if (err.code === 'PLT_CONFIG_NO_CONFIG_FILE_FOUND' && args.length === 1) {
168
166
  const config = {
@@ -188,12 +186,6 @@ async function startCommand (args) {
188
186
  return startCommand(['--config', toWrite])
189
187
  }
190
188
 
191
- if (err.code === 'PLT_RUNTIME_RUNTIME_EXIT') {
192
- console.log('Runtime exited before startup was completed, restarting')
193
- await sleep(1000)
194
- return startCommand(args)
195
- }
196
-
197
189
  if (err.filenames) {
198
190
  console.error(`Missing config file!
199
191
  Be sure to have a config file with one of the following names:
@@ -207,12 +199,7 @@ async function startCommand (args) {
207
199
  process.exit(1)
208
200
  }
209
201
 
210
- delete err?.stack
211
- console.error(err?.message)
212
-
213
- if (err?.cause) {
214
- console.error(`${err.cause}`)
215
- }
202
+ console.error(err)
216
203
 
217
204
  process.exit(1)
218
205
  }
@@ -0,0 +1,11 @@
1
+ 'use strict'
2
+
3
+ module.exports = {
4
+ version: '1.36.0',
5
+ up: function (config) {
6
+ if (config.restartOnError === undefined) {
7
+ config.restartOnError = false
8
+ }
9
+ return config
10
+ }
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "1.36.0",
3
+ "version": "1.36.2",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -20,7 +20,7 @@
20
20
  "@fastify/express": "^3.0.0",
21
21
  "@fastify/formbody": "^7.4.0",
22
22
  "@matteo.collina/tspl": "^0.1.1",
23
- "borp": "^0.11.0",
23
+ "borp": "^0.12.0",
24
24
  "c8": "^9.1.0",
25
25
  "execa": "^8.0.1",
26
26
  "express": "^4.18.3",
@@ -34,8 +34,8 @@
34
34
  "typescript": "^5.4.2",
35
35
  "undici-oidc-interceptor": "^0.5.0",
36
36
  "why-is-node-running": "^2.2.2",
37
- "@platformatic/sql-graphql": "1.36.0",
38
- "@platformatic/sql-mapper": "1.36.0"
37
+ "@platformatic/sql-mapper": "1.36.2",
38
+ "@platformatic/sql-graphql": "1.36.2"
39
39
  },
40
40
  "dependencies": {
41
41
  "@fastify/error": "^3.4.1",
@@ -56,20 +56,20 @@
56
56
  "help-me": "^5.0.0",
57
57
  "minimist": "^1.2.8",
58
58
  "pino": "^8.19.0",
59
- "pino-pretty": "^10.3.1",
59
+ "pino-pretty": "^11.0.0",
60
60
  "pino-roll": "^1.0.0",
61
61
  "semgrator": "^0.3.0",
62
62
  "tail-file-stream": "^0.1.0",
63
63
  "undici": "^6.9.0",
64
64
  "why-is-node-running": "^2.2.2",
65
65
  "ws": "^8.16.0",
66
- "@platformatic/db": "1.36.0",
67
- "@platformatic/config": "1.36.0",
68
- "@platformatic/composer": "1.36.0",
69
- "@platformatic/service": "1.36.0",
70
- "@platformatic/generators": "1.36.0",
71
- "@platformatic/telemetry": "1.36.0",
72
- "@platformatic/utils": "1.36.0"
66
+ "@platformatic/composer": "1.36.2",
67
+ "@platformatic/config": "1.36.2",
68
+ "@platformatic/db": "1.36.2",
69
+ "@platformatic/generators": "1.36.2",
70
+ "@platformatic/service": "1.36.2",
71
+ "@platformatic/telemetry": "1.36.2",
72
+ "@platformatic/utils": "1.36.2"
73
73
  },
74
74
  "standard": {
75
75
  "ignore": [