@platformatic/runtime 3.63.0-alpha.2 → 3.63.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.
- package/config.d.ts +4 -0
- package/index.d.ts +138 -45
- package/lib/errors.js +30 -11
- package/lib/management-api.js +0 -17
- package/lib/management-handlers.js +0 -20
- package/lib/prom-server.js +81 -8
- package/lib/runtime.js +538 -134
- package/lib/scheduler.js +60 -192
- package/lib/worker/controller.js +4 -2
- package/lib/worker/itc.js +38 -44
- package/lib/worker/main.js +14 -4
- package/lib/worker/management.js +0 -16
- package/lib/worker/messaging.js +20 -7
- package/lib/worker-scaler.js +20 -9
- package/package.json +17 -17
- package/schema.json +13 -2
package/lib/scheduler.js
CHANGED
|
@@ -1,176 +1,68 @@
|
|
|
1
1
|
import { CronJob, validateCronExpression } from 'cron'
|
|
2
2
|
import { setTimeout } from 'node:timers/promises'
|
|
3
3
|
import { request } from 'undici'
|
|
4
|
-
import { DuplicateSchedulerJobError, SchedulerJobNotFoundError } from './errors.js'
|
|
5
4
|
|
|
6
5
|
export class SchedulerService {
|
|
7
6
|
constructor (schedulerConfig, dispatcher, logger) {
|
|
8
7
|
this.logger = logger
|
|
8
|
+
this.jobsConfig = []
|
|
9
|
+
this.cronJobs = []
|
|
9
10
|
this.dispatcher = dispatcher
|
|
10
|
-
this.
|
|
11
|
-
this.started = false
|
|
12
|
-
|
|
13
|
-
for (const config of schedulerConfig) {
|
|
14
|
-
this.addJob({ ...config, source: 'config' })
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
addJob (config, executor) {
|
|
19
|
-
// Skip disabled schedulers
|
|
20
|
-
if (config.enabled === false) {
|
|
21
|
-
return null
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Validate cron expression
|
|
25
|
-
const validation = validateCronExpression(config.cron)
|
|
26
|
-
if (!validation.valid) {
|
|
27
|
-
throw new Error(`Invalid cron expression "${config.cron}" for scheduler "${config.name}"`)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Set defaults for optional fields
|
|
31
|
-
const job = {
|
|
32
|
-
...config,
|
|
33
|
-
headers: config.headers || {},
|
|
34
|
-
body: config.body || {},
|
|
35
|
-
maxRetries: config.maxRetries ?? 3,
|
|
36
|
-
source: config.source ?? 'application',
|
|
37
|
-
paused: false,
|
|
38
|
-
lastExecutedAt: null,
|
|
39
|
-
lastStatus: null,
|
|
40
|
-
cronJob: null,
|
|
41
|
-
executor
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (this.jobs.has(job.name)) {
|
|
45
|
-
throw new DuplicateSchedulerJobError(job.name)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
this.jobs.set(job.name, job)
|
|
49
|
-
|
|
50
|
-
if (this.started) {
|
|
51
|
-
this.#startJob(job)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return this.#serializeJob(job)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
start () {
|
|
58
|
-
this.started = true
|
|
59
|
-
|
|
60
|
-
for (const job of this.jobs.values()) {
|
|
61
|
-
this.#startJob(job)
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async stop () {
|
|
66
|
-
this.started = false
|
|
67
|
-
|
|
68
|
-
for (const job of this.jobs.values()) {
|
|
69
|
-
await this.#stopJob(job)
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
getJobs () {
|
|
74
|
-
return Array.from(this.jobs.values(), job => this.#serializeJob(job))
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Stops the local cron trigger for a job without removing its configuration.
|
|
78
|
-
// Used when an external coordinator (e.g. ICC) takes over the scheduling, so
|
|
79
|
-
// that multiple instances of the same runtime do not execute the job concurrently.
|
|
80
|
-
async pauseJob (name) {
|
|
81
|
-
const job = this.#getJobOrThrow(name)
|
|
82
|
-
|
|
83
|
-
if (job.paused) {
|
|
84
|
-
return this.#serializeJob(job)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
job.paused = true
|
|
88
|
-
await this.#stopJob(job)
|
|
89
|
-
this.logger.info(`Scheduler "${name}" paused`)
|
|
90
|
-
return this.#serializeJob(job)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async resumeJob (name) {
|
|
94
|
-
const job = this.#getJobOrThrow(name)
|
|
95
|
-
|
|
96
|
-
if (!job.paused) {
|
|
97
|
-
return this.#serializeJob(job)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
job.paused = false
|
|
101
|
-
|
|
102
|
-
if (this.started) {
|
|
103
|
-
this.#startJob(job)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
this.logger.info(`Scheduler "${name}" resumed`)
|
|
107
|
-
return this.#serializeJob(job)
|
|
11
|
+
this.validateCronSchedulers(schedulerConfig)
|
|
108
12
|
}
|
|
109
13
|
|
|
110
|
-
|
|
111
|
-
for (const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
14
|
+
validateCronSchedulers (schedulerConfig) {
|
|
15
|
+
for (const config of schedulerConfig) {
|
|
16
|
+
// Skip disabled schedulers
|
|
17
|
+
if (config.enabled === false) {
|
|
18
|
+
continue
|
|
115
19
|
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
20
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (
|
|
122
|
-
|
|
21
|
+
// Validate cron expression
|
|
22
|
+
const validation = validateCronExpression(config.cron)
|
|
23
|
+
if (!validation.valid) {
|
|
24
|
+
throw new Error(`Invalid cron expression "${config.cron}" for scheduler "${config.name}"`)
|
|
123
25
|
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Executes a job immediately, regardless of its cron schedule or paused state.
|
|
128
|
-
// Used by an external coordinator to trigger the execution in this instance.
|
|
129
|
-
async runJob (name) {
|
|
130
|
-
const job = this.#getJobOrThrow(name)
|
|
131
|
-
return this.executeCallback(job)
|
|
132
|
-
}
|
|
133
26
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
27
|
+
// Set defaults for optional fields
|
|
28
|
+
const job = {
|
|
29
|
+
...config,
|
|
30
|
+
headers: config.headers || {},
|
|
31
|
+
body: config.body || {},
|
|
32
|
+
maxRetries: config.maxRetries || 3
|
|
33
|
+
}
|
|
34
|
+
this.jobsConfig.push(job)
|
|
138
35
|
}
|
|
139
|
-
return job
|
|
140
36
|
}
|
|
141
37
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
38
|
+
start () {
|
|
39
|
+
for (const job of this.jobsConfig) {
|
|
40
|
+
this.logger.info(`Configuring scheduler "${job.name}" with cron "${job.cron}"`)
|
|
41
|
+
const cronJob = CronJob.from({
|
|
42
|
+
cronTime: job.cron,
|
|
43
|
+
onTick: async () => {
|
|
44
|
+
this.logger.info(`Executing scheduler "${job.name}"`)
|
|
45
|
+
// This cannot throw, the try/catch is inside
|
|
46
|
+
await this.executeCallback(job)
|
|
47
|
+
},
|
|
48
|
+
start: true,
|
|
49
|
+
timeZone: 'UTC',
|
|
50
|
+
waitForCompletion: true
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
this.cronJobs.push(cronJob)
|
|
145
54
|
}
|
|
146
|
-
|
|
147
|
-
this.logger.info(`Configuring scheduler "${job.name}" with cron "${job.cron}"`)
|
|
148
|
-
job.cronJob = CronJob.from({
|
|
149
|
-
cronTime: job.cron,
|
|
150
|
-
onTick: async () => {
|
|
151
|
-
this.logger.info(`Executing scheduler "${job.name}"`)
|
|
152
|
-
// This cannot throw, the try/catch is inside
|
|
153
|
-
await this.executeCallback(job)
|
|
154
|
-
},
|
|
155
|
-
start: true,
|
|
156
|
-
timeZone: 'UTC',
|
|
157
|
-
waitForCompletion: true
|
|
158
|
-
})
|
|
159
55
|
}
|
|
160
56
|
|
|
161
|
-
async
|
|
162
|
-
|
|
163
|
-
|
|
57
|
+
async stop () {
|
|
58
|
+
for (const job of this.cronJobs) {
|
|
59
|
+
await job.stop()
|
|
164
60
|
}
|
|
165
|
-
|
|
166
|
-
await job.cronJob.stop()
|
|
167
|
-
job.cronJob = null
|
|
168
61
|
}
|
|
169
62
|
|
|
170
63
|
async executeCallback (scheduler) {
|
|
171
64
|
let attempt = 0
|
|
172
65
|
let success = false
|
|
173
|
-
const scheduledTime = Date.now()
|
|
174
66
|
|
|
175
67
|
while (!success && attempt < scheduler.maxRetries) {
|
|
176
68
|
try {
|
|
@@ -182,32 +74,29 @@ export class SchedulerService {
|
|
|
182
74
|
)
|
|
183
75
|
await setTimeout(delay)
|
|
184
76
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const headers = {
|
|
189
|
-
'x-retry-attempt': attempt + 1,
|
|
190
|
-
...scheduler.headers
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const bodyString = typeof scheduler.body === 'string' ? scheduler.body : JSON.stringify(scheduler.body)
|
|
194
|
-
const response = await request(scheduler.callbackUrl, {
|
|
195
|
-
method: scheduler.method,
|
|
196
|
-
headers,
|
|
197
|
-
body: bodyString,
|
|
198
|
-
dispatcher: this.dispatcher
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
// Consume the body to release the connection.
|
|
202
|
-
await response.body.dump()
|
|
203
|
-
|
|
204
|
-
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
205
|
-
throw new Error(`HTTP error ${response.statusCode}`)
|
|
206
|
-
}
|
|
77
|
+
const headers = {
|
|
78
|
+
'x-retry-attempt': attempt + 1,
|
|
79
|
+
...scheduler.headers
|
|
207
80
|
}
|
|
208
81
|
|
|
209
|
-
|
|
210
|
-
|
|
82
|
+
const bodyString = typeof scheduler.body === 'string' ? scheduler.body : JSON.stringify(scheduler.body)
|
|
83
|
+
const response = await request(scheduler.callbackUrl, {
|
|
84
|
+
method: scheduler.method,
|
|
85
|
+
headers,
|
|
86
|
+
body: bodyString,
|
|
87
|
+
dispatcher: this.dispatcher
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// Consumes the body, but we are not interested in the body content,
|
|
91
|
+
// we don't save it anywere, so we just dump it
|
|
92
|
+
await response.body.dump()
|
|
93
|
+
|
|
94
|
+
if (response.statusCode >= 200 && response.statusCode < 300) {
|
|
95
|
+
this.logger.info(`Scheduler "${scheduler.name}" executed successfully`)
|
|
96
|
+
success = true
|
|
97
|
+
} else {
|
|
98
|
+
throw new Error(`HTTP error ${response.statusCode}`)
|
|
99
|
+
}
|
|
211
100
|
} catch (error) {
|
|
212
101
|
this.logger.error(
|
|
213
102
|
`Error executing scheduler "${scheduler.name}" (attempt ${attempt + 1}/${scheduler.maxRetries}):`,
|
|
@@ -217,30 +106,9 @@ export class SchedulerService {
|
|
|
217
106
|
}
|
|
218
107
|
}
|
|
219
108
|
|
|
220
|
-
scheduler.lastExecutedAt = new Date().toISOString()
|
|
221
|
-
scheduler.lastStatus = success ? 'success' : 'failed'
|
|
222
|
-
|
|
223
109
|
if (!success) {
|
|
224
110
|
this.logger.error(`Scheduler "${scheduler.name}" failed after ${scheduler.maxRetries} attempts`)
|
|
225
111
|
}
|
|
226
|
-
|
|
227
|
-
return { name: scheduler.name, success, executedAt: scheduler.lastExecutedAt }
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
#serializeJob (job) {
|
|
231
|
-
const { cronJob, executor, ...serializable } = job
|
|
232
|
-
let nextRunAt = null
|
|
233
|
-
|
|
234
|
-
if (cronJob) {
|
|
235
|
-
try {
|
|
236
|
-
nextRunAt = cronJob.nextDate().toJSDate().toISOString()
|
|
237
|
-
/* c8 ignore next 3 */
|
|
238
|
-
} catch {
|
|
239
|
-
nextRunAt = null
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return { ...serializable, nextRunAt }
|
|
244
112
|
}
|
|
245
113
|
}
|
|
246
114
|
|
package/lib/worker/controller.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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)
|
|
@@ -195,36 +205,36 @@ export async function setupITC (controller, application, dispatcher, sharedConte
|
|
|
195
205
|
}
|
|
196
206
|
|
|
197
207
|
dispatcher.replaceServer(await controller.capability.getDispatchTarget())
|
|
198
|
-
|
|
199
|
-
const scheduledTasks =
|
|
200
|
-
typeof controller.capability.getScheduledTasks === 'function'
|
|
201
|
-
? await controller.capability.getScheduledTasks()
|
|
202
|
-
: []
|
|
203
|
-
|
|
204
|
-
return {
|
|
205
|
-
url: application.entrypoint ? controller.capability.getUrl() : null,
|
|
206
|
-
scheduledTasks
|
|
207
|
-
}
|
|
208
|
+
return application.entrypoint ? controller.capability.getUrl() : null
|
|
208
209
|
},
|
|
209
210
|
|
|
210
211
|
async stop ({ force, dependents }) {
|
|
211
|
-
|
|
212
|
+
try {
|
|
213
|
+
const status = controller.getStatus()
|
|
212
214
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
215
|
+
if (!force && status === 'starting') {
|
|
216
|
+
await once(controller, 'start')
|
|
217
|
+
}
|
|
216
218
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
219
|
+
if (force || status.startsWith('start')) {
|
|
220
|
+
// This gives a chance to a capability to perform custom logic
|
|
221
|
+
const events = getEvents()
|
|
222
|
+
events.emit('stop')
|
|
221
223
|
|
|
222
|
-
|
|
224
|
+
await controller.stop(force, dependents)
|
|
225
|
+
}
|
|
226
|
+
} finally {
|
|
227
|
+
// Always schedule cleanup, even when stop throws. Otherwise the worker
|
|
228
|
+
// keeps open handles and runtime shutdown hangs until the grace timeout.
|
|
229
|
+
once(itc, 'application:worker:stop:processed').then(() => {
|
|
230
|
+
closeITC(dispatcher, itc, messaging).catch(err => {
|
|
231
|
+
logger.error(
|
|
232
|
+
{ err: ensureLoggableError(err) },
|
|
233
|
+
'Failed to close the worker ITC after stop.'
|
|
234
|
+
)
|
|
235
|
+
})
|
|
236
|
+
})
|
|
223
237
|
}
|
|
224
|
-
|
|
225
|
-
once(itc, 'application:worker:stop:processed').then(() => {
|
|
226
|
-
closeITC(dispatcher, itc, messaging).catch(() => {})
|
|
227
|
-
})
|
|
228
238
|
},
|
|
229
239
|
|
|
230
240
|
async getDependencies () {
|
|
@@ -295,22 +305,6 @@ export async function setupITC (controller, application, dispatcher, sharedConte
|
|
|
295
305
|
}
|
|
296
306
|
},
|
|
297
307
|
|
|
298
|
-
async getApplicationScheduledTasks () {
|
|
299
|
-
if (typeof controller.capability.getScheduledTasks !== 'function') {
|
|
300
|
-
return []
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return controller.capability.getScheduledTasks()
|
|
304
|
-
},
|
|
305
|
-
|
|
306
|
-
async runApplicationScheduledTasks ({ scheduleId, scheduledTime }) {
|
|
307
|
-
if (typeof controller.capability.runScheduledTasks !== 'function') {
|
|
308
|
-
throw new Error(`Application "${application.id}" does not support scheduled task execution`)
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
return controller.capability.runScheduledTasks(scheduleId, scheduledTime)
|
|
312
|
-
},
|
|
313
|
-
|
|
314
308
|
async getApplicationMeta () {
|
|
315
309
|
try {
|
|
316
310
|
return await controller.capability.getMeta()
|
package/lib/worker/main.js
CHANGED
|
@@ -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'
|
|
393
|
-
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'
|
|
405
|
-
value =
|
|
414
|
+
if (name.toLowerCase() === 'location') {
|
|
415
|
+
value = prependBasePath(value)
|
|
406
416
|
}
|
|
407
417
|
}
|
|
408
418
|
originSetHeader.call(this, name, value)
|
package/lib/worker/management.js
CHANGED
|
@@ -70,22 +70,6 @@ export class ManagementClient {
|
|
|
70
70
|
return this.#send('getMetrics', { format })
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
getScheduler () {
|
|
74
|
-
return this.#send('getScheduler')
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
pauseSchedulerJob (name) {
|
|
78
|
-
return this.#send('pauseSchedulerJob', { name })
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
resumeSchedulerJob (name) {
|
|
82
|
-
return this.#send('resumeSchedulerJob', { name })
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
runSchedulerJob (name) {
|
|
86
|
-
return this.#send('runSchedulerJob', { name })
|
|
87
|
-
}
|
|
88
|
-
|
|
89
73
|
startApplication (id) {
|
|
90
74
|
return this.#send('startApplication', { id })
|
|
91
75
|
}
|
package/lib/worker/messaging.js
CHANGED
|
@@ -114,14 +114,21 @@ export class MessagingITC extends ITC {
|
|
|
114
114
|
context.meta = { ...options?.meta, ...telemetry?.meta }
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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 }
|
package/lib/worker-scaler.js
CHANGED
|
@@ -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
|
-
|
|
137
|
-
this.#
|
|
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
|
|
3
|
+
"version": "3.63.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
|
|
43
|
-
"@platformatic/db": "3.63.0
|
|
44
|
-
"@platformatic/gateway": "3.63.0
|
|
45
|
-
"@platformatic/
|
|
46
|
-
"@platformatic/sql-graphql": "3.63.0
|
|
47
|
-
"@platformatic/
|
|
48
|
-
"@platformatic/
|
|
49
|
-
"@platformatic/
|
|
42
|
+
"@platformatic/composer": "3.63.0",
|
|
43
|
+
"@platformatic/db": "3.63.0",
|
|
44
|
+
"@platformatic/gateway": "3.63.0",
|
|
45
|
+
"@platformatic/service": "3.63.0",
|
|
46
|
+
"@platformatic/sql-graphql": "3.63.0",
|
|
47
|
+
"@platformatic/sql-mapper": "3.63.0",
|
|
48
|
+
"@platformatic/node": "3.63.0",
|
|
49
|
+
"@platformatic/wattpm-pprof-capture": "3.63.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": "^
|
|
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
|
|
81
|
-
"@platformatic/
|
|
82
|
-
"@platformatic/
|
|
83
|
-
"@platformatic/globals": "3.63.0
|
|
84
|
-
"@platformatic/metrics": "3.63.0
|
|
85
|
-
"@platformatic/telemetry": "3.63.0
|
|
86
|
-
"@platformatic/itc": "3.63.0
|
|
80
|
+
"@platformatic/basic": "3.63.0",
|
|
81
|
+
"@platformatic/generators": "3.63.0",
|
|
82
|
+
"@platformatic/foundation": "3.63.0",
|
|
83
|
+
"@platformatic/globals": "3.63.0",
|
|
84
|
+
"@platformatic/metrics": "3.63.0",
|
|
85
|
+
"@platformatic/telemetry": "3.63.0",
|
|
86
|
+
"@platformatic/itc": "3.63.0"
|
|
87
87
|
},
|
|
88
88
|
"engines": {
|
|
89
89
|
"node": ">=22.19.0"
|