@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.
@@ -83,7 +83,7 @@ export class ScalingAlgorithm {
83
83
 
84
84
  const newWorkersCount = appMinWorkers - workersCount
85
85
  totalWorkersCount += newWorkersCount
86
- totalAvailableMemory += newWorkersCount * avgHeapUsed
86
+ totalAvailableMemory -= newWorkersCount * avgHeapUsed
87
87
  continue
88
88
  }
89
89
 
@@ -96,7 +96,7 @@ export class ScalingAlgorithm {
96
96
 
97
97
  const removedWorkersCount = workersCount - appMaxWorkers
98
98
  totalWorkersCount -= removedWorkersCount
99
- totalAvailableMemory -= removedWorkersCount * avgHeapUsed
99
+ totalAvailableMemory += removedWorkersCount * avgHeapUsed
100
100
  continue
101
101
  }
102
102
 
@@ -111,7 +111,7 @@ export class ScalingAlgorithm {
111
111
 
112
112
  const removedWorkersCount = 1
113
113
  totalWorkersCount -= removedWorkersCount
114
- totalAvailableMemory -= removedWorkersCount * avgHeapUsed
114
+ totalAvailableMemory += removedWorkersCount * avgHeapUsed
115
115
  }
116
116
  }
117
117
  }
@@ -95,7 +95,8 @@ export class Controller extends EventEmitter {
95
95
  worker: workerData?.worker,
96
96
  resourceLimits: workerData?.resourceLimits,
97
97
  hasManagementApi: !!runtimeConfig.managementApi,
98
- fetchApplicationUrl: fetchApplicationUrl.bind(null, applicationConfig)
98
+ fetchApplicationUrl: fetchApplicationUrl.bind(null, applicationConfig),
99
+ strictEnv: runtimeConfig.strictEnv
99
100
  }
100
101
  }
101
102
 
@@ -141,7 +142,8 @@ export class Controller extends EventEmitter {
141
142
  if (appConfig.config) {
142
143
  // Parse the configuration file the first time to obtain the schema
143
144
  const unvalidatedConfig = await loadConfiguration(appConfig.config, null, {
144
- onMissingEnv: this.#context.fetchApplicationUrl
145
+ onMissingEnv: this.#context.fetchApplicationUrl,
146
+ strictEnv: this.#context.strictEnv
145
147
  })
146
148
  const pkg = await loadConfigurationModule(appConfig.path, unvalidatedConfig)
147
149
  this.capability = await pkg.create(appConfig.path, appConfig.config, this.#context)
package/lib/worker/itc.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ensureLoggableError, executeInParallel, executeWithTimeout, kTimeout } from '@platformatic/foundation'
2
2
  import { getEvents, getLogger, getMessaging, updateGlobals } from '@platformatic/globals'
3
- import { initializeITCTelemetry, ITC } from '@platformatic/itc'
3
+ import { ITC, initializeITCTelemetry } from '@platformatic/itc'
4
4
  import { Unpromise } from '@watchable/unpromise'
5
5
  import { once } from 'node:events'
6
6
  import { createRequire } from 'node:module'
@@ -112,9 +112,14 @@ async function safeHandleInITC (worker, fn) {
112
112
  }
113
113
 
114
114
  async function closeITC (dispatcher, itc, messaging) {
115
- await dispatcher.interceptor.close()
116
- itc.close()
117
- messaging.close()
115
+ try {
116
+ await dispatcher.interceptor.close()
117
+ itc.close()
118
+ messaging.close()
119
+ } finally {
120
+ const events = getEvents()
121
+ events.emit('exit')
122
+ }
118
123
  }
119
124
 
120
125
  export async function sendViaITC (worker, name, message, transferList) {
@@ -183,7 +188,12 @@ export async function setupITC (controller, application, dispatcher, sharedConte
183
188
 
184
189
  // Reply to the runtime that the start failed, so it can cleanup
185
190
  once(itc, 'application:worker:start:processed').then(() => {
186
- closeITC(dispatcher, itc, messaging).catch(() => {})
191
+ closeITC(dispatcher, itc, messaging).catch(err => {
192
+ logger.error(
193
+ { err: ensureLoggableError(err) },
194
+ 'Failed to close the worker ITC after a failed start.'
195
+ )
196
+ })
187
197
  })
188
198
 
189
199
  throw ensureLoggableError(e)
@@ -208,23 +218,32 @@ export async function setupITC (controller, application, dispatcher, sharedConte
208
218
  },
209
219
 
210
220
  async stop ({ force, dependents }) {
211
- const status = controller.getStatus()
221
+ try {
222
+ const status = controller.getStatus()
212
223
 
213
- if (!force && status === 'starting') {
214
- await once(controller, 'start')
215
- }
224
+ if (!force && status === 'starting') {
225
+ await once(controller, 'start')
226
+ }
216
227
 
217
- if (force || status.startsWith('start')) {
218
- // This gives a chance to a capability to perform custom logic
219
- const events = getEvents()
220
- events.emit('stop')
228
+ if (force || status.startsWith('start')) {
229
+ // This gives a chance to a capability to perform custom logic
230
+ const events = getEvents()
231
+ events.emit('stop')
221
232
 
222
- await controller.stop(force, dependents)
233
+ await controller.stop(force, dependents)
234
+ }
235
+ } finally {
236
+ // Always schedule cleanup, even when stop throws. Otherwise the worker
237
+ // keeps open handles and runtime shutdown hangs until the grace timeout.
238
+ once(itc, 'application:worker:stop:processed').then(() => {
239
+ closeITC(dispatcher, itc, messaging).catch(err => {
240
+ logger.error(
241
+ { err: ensureLoggableError(err) },
242
+ 'Failed to close the worker ITC after stop.'
243
+ )
244
+ })
245
+ })
223
246
  }
224
-
225
- once(itc, 'application:worker:stop:processed').then(() => {
226
- closeITC(dispatcher, itc, messaging).catch(() => {})
227
- })
228
247
  },
229
248
 
230
249
  async getDependencies () {
@@ -364,6 +364,16 @@ async function main () {
364
364
 
365
365
  function stripBasePath (basePath) {
366
366
  const kBasePath = Symbol('kBasePath')
367
+ const absoluteUrlPattern = /^https?:\/\//i
368
+
369
+ function prependBasePath (value) {
370
+ // Absolute and protocol-relative URLs (RFC 7231 Location) must not be rewritten
371
+ if (typeof value !== 'string' || absoluteUrlPattern.test(value) || value.startsWith('//') || value.startsWith(basePath)) {
372
+ return value
373
+ }
374
+
375
+ return basePath + value
376
+ }
367
377
 
368
378
  subscribe('http.server.request.start', ({ request, response }) => {
369
379
  if (request.url.startsWith(basePath)) {
@@ -389,8 +399,8 @@ function stripBasePath (basePath) {
389
399
 
390
400
  if (headers) {
391
401
  for (const key in headers) {
392
- if (key.toLowerCase() === 'location' && !headers[key].startsWith(basePath)) {
393
- headers[key] = basePath + headers[key]
402
+ if (key.toLowerCase() === 'location') {
403
+ headers[key] = prependBasePath(headers[key])
394
404
  }
395
405
  }
396
406
  }
@@ -401,8 +411,8 @@ function stripBasePath (basePath) {
401
411
 
402
412
  ServerResponse.prototype.setHeader = function (name, value) {
403
413
  if (this[kBasePath]) {
404
- if (name.toLowerCase() === 'location' && !value.startsWith(basePath)) {
405
- value = basePath + value
414
+ if (name.toLowerCase() === 'location') {
415
+ value = prependBasePath(value)
406
416
  }
407
417
  }
408
418
  originSetHeader.call(this, name, value)
@@ -70,8 +70,8 @@ export class ManagementClient {
70
70
  return this.#send('getMetrics', { format })
71
71
  }
72
72
 
73
- getScheduler () {
74
- return this.#send('getScheduler')
73
+ getSchedulerJobs () {
74
+ return this.#send('getSchedulerJobs')
75
75
  }
76
76
 
77
77
  pauseSchedulerJob (name) {
@@ -114,14 +114,21 @@ export class MessagingITC extends ITC {
114
114
  context.meta = { ...options?.meta, ...telemetry?.meta }
115
115
  }
116
116
 
117
- const send = () => executeWithTimeout(super.send(name, message, context), this.#timeout)
118
- const response = telemetry ? await telemetry.run(send) : await send()
119
-
120
- if (response === kTimeout) {
121
- throw new MessagingError(application, 'Timeout while waiting for a response.')
117
+ const abortController = new AbortController()
118
+ const timeout = setTimeout(() => abortController.abort(), this.#timeout).unref()
119
+ context.signal = abortController.signal
120
+
121
+ try {
122
+ const send = () => super.send(name, message, context)
123
+ return telemetry ? await telemetry.run(send) : await send()
124
+ } catch (err) {
125
+ if (abortController.signal.aborted) {
126
+ throw new MessagingError(application, 'Timeout while waiting for a response.')
127
+ }
128
+ throw err
129
+ } finally {
130
+ clearTimeout(timeout)
122
131
  }
123
-
124
- return response
125
132
  } catch (err) {
126
133
  error = err
127
134
  throw err
@@ -181,6 +188,12 @@ export class MessagingITC extends ITC {
181
188
  channel.postMessage(sanitize(request, transferList), transferList)
182
189
  }
183
190
 
191
+ _onRequestSettled (reqId, context) {
192
+ if (context.trackResponse) {
193
+ context.channel[kPendingResponses].delete(reqId)
194
+ }
195
+ }
196
+
184
197
  _createClosePromise () {
185
198
  const { promise, resolve, reject } = Promise.withResolvers()
186
199
  this.#closeResolvers = { resolve, reject }
@@ -121,20 +121,31 @@ export class DynamicWorkersScaler {
121
121
 
122
122
  if (config.minWorkers > 1) {
123
123
  const update = { application: application.id, workers: config.minWorkers }
124
-
125
- if (!this.#status === 'started') {
126
- await this.#runtime.updateApplicationsResources([update])
127
- } else {
128
- this.#initialUpdates.push(update)
129
- }
124
+ this.#initialUpdates.push(update)
130
125
  }
131
126
 
132
127
  this.#appsConfigs[application.id] = config
133
128
  this.#algorithm.addApplication(application.id, config)
134
129
  }
135
130
 
136
- remove (application) {
137
- this.#algorithm.removeApplication(application.id)
131
+ async applyPendingUpdate (applicationId) {
132
+ if (this.#status !== 'started') {
133
+ return
134
+ }
135
+
136
+ const update = this.#initialUpdates.find(update => update.application === applicationId)
137
+ if (!update) {
138
+ return
139
+ }
140
+
141
+ await this.#runtime.updateApplicationsResources([update])
142
+ this.#initialUpdates = this.#initialUpdates.filter(pending => pending !== update)
143
+ }
144
+
145
+ remove (applicationId) {
146
+ delete this.#appsConfigs[applicationId]
147
+ this.#initialUpdates = this.#initialUpdates.filter(update => update.application !== applicationId)
148
+ this.#algorithm.removeApplication(applicationId)
138
149
  }
139
150
 
140
151
  async #chechHealth () {
@@ -175,7 +186,7 @@ export class DynamicWorkersScaler {
175
186
  shouldCheckForScaling = true
176
187
  }
177
188
  } catch (err) {
178
- this.logger.error({ err }, 'Failed to get health for worker')
189
+ this.#runtime.logger.error({ err }, 'Failed to get health for worker')
179
190
  }
180
191
  }
181
192
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "3.63.0-alpha.3",
3
+ "version": "3.64.0",
4
4
  "description": "Platformatic runtime",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -39,14 +39,14 @@
39
39
  "typescript": "^5.9.3",
40
40
  "undici-oidc-interceptor": "^0.9.0",
41
41
  "why-is-node-running": "^2.2.2",
42
- "@platformatic/composer": "3.63.0-alpha.3",
43
- "@platformatic/db": "3.63.0-alpha.3",
44
- "@platformatic/gateway": "3.63.0-alpha.3",
45
- "@platformatic/node": "3.63.0-alpha.3",
46
- "@platformatic/service": "3.63.0-alpha.3",
47
- "@platformatic/sql-graphql": "3.63.0-alpha.3",
48
- "@platformatic/wattpm-pprof-capture": "3.63.0-alpha.3",
49
- "@platformatic/sql-mapper": "3.63.0-alpha.3"
42
+ "@platformatic/db": "3.64.0",
43
+ "@platformatic/composer": "3.64.0",
44
+ "@platformatic/gateway": "3.64.0",
45
+ "@platformatic/service": "3.64.0",
46
+ "@platformatic/node": "3.64.0",
47
+ "@platformatic/sql-graphql": "3.64.0",
48
+ "@platformatic/wattpm-pprof-capture": "3.64.0",
49
+ "@platformatic/sql-mapper": "3.64.0"
50
50
  },
51
51
  "dependencies": {
52
52
  "@fastify/accepts": "^5.0.0",
@@ -69,7 +69,7 @@
69
69
  "help-me": "^5.0.0",
70
70
  "minimist": "^1.2.8",
71
71
  "pino": "^10.1.0",
72
- "pino-opentelemetry-transport": "^3.0.0",
72
+ "pino-opentelemetry-transport": "^4.0.2",
73
73
  "pino-pretty": "^13.0.0",
74
74
  "semgrator": "^0.3.0",
75
75
  "sonic-boom": "^4.2.0",
@@ -77,13 +77,13 @@
77
77
  "undici": "^7.27.2",
78
78
  "undici-thread-interceptor": "^1.3.1",
79
79
  "ws": "^8.16.0",
80
- "@platformatic/basic": "3.63.0-alpha.3",
81
- "@platformatic/foundation": "3.63.0-alpha.3",
82
- "@platformatic/generators": "3.63.0-alpha.3",
83
- "@platformatic/itc": "3.63.0-alpha.3",
84
- "@platformatic/metrics": "3.63.0-alpha.3",
85
- "@platformatic/globals": "3.63.0-alpha.3",
86
- "@platformatic/telemetry": "3.63.0-alpha.3"
80
+ "@platformatic/foundation": "3.64.0",
81
+ "@platformatic/globals": "3.64.0",
82
+ "@platformatic/generators": "3.64.0",
83
+ "@platformatic/itc": "3.64.0",
84
+ "@platformatic/basic": "3.64.0",
85
+ "@platformatic/metrics": "3.64.0",
86
+ "@platformatic/telemetry": "3.64.0"
87
87
  },
88
88
  "engines": {
89
89
  "node": ">=22.19.0"
package/schema.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.63.0-alpha.3.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.64.0.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "title": "Platformatic Runtime Config",
5
5
  "type": "object",
@@ -2234,7 +2234,7 @@
2234
2234
  "type": "string"
2235
2235
  }
2236
2236
  ],
2237
- "default": 10000
2237
+ "default": 30000
2238
2238
  },
2239
2239
  "application": {
2240
2240
  "anyOf": [
@@ -3492,6 +3492,17 @@
3492
3492
  "envfile": {
3493
3493
  "type": "string"
3494
3494
  },
3495
+ "strictEnv": {
3496
+ "anyOf": [
3497
+ {
3498
+ "type": "boolean"
3499
+ },
3500
+ {
3501
+ "type": "string"
3502
+ }
3503
+ ],
3504
+ "description": "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."
3505
+ },
3495
3506
  "sourceMaps": {
3496
3507
  "type": "boolean",
3497
3508
  "default": false