nmtjs 0.15.0-beta.21 → 0.15.0-beta.22
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/dist/index.d.ts +2 -0
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/injectables.d.ts +5 -0
- package/dist/runtime/injectables.js +4 -0
- package/dist/runtime/injectables.js.map +1 -1
- package/dist/runtime/jobs/manager.d.ts +19 -26
- package/dist/runtime/jobs/manager.js +64 -18
- package/dist/runtime/jobs/manager.js.map +1 -1
- package/dist/runtime/jobs/router.d.ts +21 -10
- package/dist/runtime/jobs/router.js +27 -12
- package/dist/runtime/jobs/router.js.map +1 -1
- package/dist/runtime/jobs/runner.d.ts +27 -4
- package/dist/runtime/jobs/runner.js +74 -11
- package/dist/runtime/jobs/runner.js.map +1 -1
- package/dist/runtime/jobs/types.d.ts +80 -0
- package/dist/runtime/jobs/types.js +5 -0
- package/dist/runtime/jobs/types.js.map +1 -0
- package/dist/runtime/server/jobs.js +19 -10
- package/dist/runtime/server/jobs.js.map +1 -1
- package/dist/runtime/workers/job.js.map +1 -1
- package/package.json +12 -12
- package/src/runtime/index.ts +1 -0
- package/src/runtime/injectables.ts +13 -0
- package/src/runtime/jobs/manager.ts +87 -43
- package/src/runtime/jobs/router.ts +55 -22
- package/src/runtime/jobs/runner.ts +120 -15
- package/src/runtime/jobs/types.ts +110 -0
- package/src/runtime/server/jobs.ts +26 -10
- package/src/runtime/workers/job.ts +2 -7
|
@@ -3,7 +3,6 @@ import type { TProcedureContract, TRouterContract } from '@nmtjs/contract'
|
|
|
3
3
|
import type { Dependencies, DependencyContext, Metadata } from '@nmtjs/core'
|
|
4
4
|
import type { NullableType, OptionalType } from '@nmtjs/type'
|
|
5
5
|
import type { NeverType } from '@nmtjs/type/never'
|
|
6
|
-
import type { JobState } from 'bullmq'
|
|
7
6
|
import { CoreInjectables } from '@nmtjs/core'
|
|
8
7
|
import { t } from '@nmtjs/type'
|
|
9
8
|
|
|
@@ -12,6 +11,7 @@ import type { AnyMiddleware } from '../application/api/middlewares.ts'
|
|
|
12
11
|
import type { AnyProcedure } from '../application/api/procedure.ts'
|
|
13
12
|
import type { AnyRouter, Router } from '../application/api/router.ts'
|
|
14
13
|
import type { AnyJob } from './job.ts'
|
|
14
|
+
import type { JobStatus } from './types.ts'
|
|
15
15
|
import { createProcedure } from '../application/api/procedure.ts'
|
|
16
16
|
import { createRouter } from '../application/api/router.ts'
|
|
17
17
|
import { jobManager } from '../injectables.ts'
|
|
@@ -142,21 +142,35 @@ export type CreateJobsRouterOptions<Jobs extends Record<string, AnyJob>> = {
|
|
|
142
142
|
// Router Contract Types
|
|
143
143
|
// ============================================================================
|
|
144
144
|
|
|
145
|
+
/** Type-level schema for JobProgressCheckpoint - typed per job */
|
|
146
|
+
type JobProgressCheckpointSchemaType<T extends AnyJob> = t.ObjectType<{
|
|
147
|
+
stepIndex: t.NumberType
|
|
148
|
+
stepLabel: OptionalType<t.StringType>
|
|
149
|
+
result: t.RecordType<t.StringType, t.AnyType>
|
|
150
|
+
stepResults: t.ArrayType<
|
|
151
|
+
t.ObjectType<{
|
|
152
|
+
data: NullableType<t.RecordType<t.StringType, t.AnyType>>
|
|
153
|
+
duration: t.NumberType
|
|
154
|
+
}>
|
|
155
|
+
>
|
|
156
|
+
progress: T['progress']
|
|
157
|
+
}>
|
|
158
|
+
|
|
145
159
|
/** Type-level representation of createJobItemSchema output */
|
|
146
160
|
type JobItemSchemaType<T extends AnyJob> = t.ObjectType<{
|
|
147
161
|
id: t.StringType
|
|
148
|
-
queueName: t.StringType
|
|
149
|
-
priority: OptionalType<t.NumberType>
|
|
150
|
-
progress: t.AnyType
|
|
151
162
|
name: t.StringType
|
|
163
|
+
queue: t.StringType
|
|
152
164
|
data: T['input']
|
|
153
|
-
|
|
154
|
-
attemptsMade: t.NumberType
|
|
155
|
-
processedOn: OptionalType<t.NumberType>
|
|
156
|
-
finishedOn: OptionalType<t.NumberType>
|
|
157
|
-
failedReason: OptionalType<t.StringType>
|
|
158
|
-
stacktrace: OptionalType<t.ArrayType<t.StringType>>
|
|
165
|
+
output: OptionalType<NullableType<T['output']>>
|
|
159
166
|
status: t.StringType
|
|
167
|
+
priority: OptionalType<t.NumberType>
|
|
168
|
+
progress: OptionalType<JobProgressCheckpointSchemaType<T>>
|
|
169
|
+
attempts: t.NumberType
|
|
170
|
+
startedAt: OptionalType<t.NumberType>
|
|
171
|
+
completedAt: OptionalType<t.NumberType>
|
|
172
|
+
error: OptionalType<t.StringType>
|
|
173
|
+
stacktrace: OptionalType<t.ArrayType<t.StringType>>
|
|
160
174
|
}>
|
|
161
175
|
|
|
162
176
|
/** Type-level representation of createListOutputSchema output */
|
|
@@ -216,7 +230,7 @@ export type JobsRouter<Jobs extends Record<string, AnyJob>> = Router<
|
|
|
216
230
|
const listInputSchema = t.object({
|
|
217
231
|
page: t.number().optional(),
|
|
218
232
|
limit: t.number().optional(),
|
|
219
|
-
|
|
233
|
+
status: t.array(t.string()).optional(),
|
|
220
234
|
})
|
|
221
235
|
|
|
222
236
|
/** Input schema for get operation */
|
|
@@ -242,22 +256,41 @@ const infoOutputSchema = t.object({
|
|
|
242
256
|
),
|
|
243
257
|
})
|
|
244
258
|
|
|
259
|
+
/** Schema for step result entry */
|
|
260
|
+
const stepResultEntrySchema = t.object({
|
|
261
|
+
data: t.record(t.string(), t.any()).nullable(),
|
|
262
|
+
duration: t.number(),
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
/** Creates JobProgressCheckpoint schema for a specific job */
|
|
266
|
+
function createJobProgressCheckpointSchema<T extends AnyJob>(
|
|
267
|
+
job: T,
|
|
268
|
+
): JobProgressCheckpointSchemaType<T> {
|
|
269
|
+
return t.object({
|
|
270
|
+
stepIndex: t.number(),
|
|
271
|
+
stepLabel: t.string().optional(),
|
|
272
|
+
result: t.record(t.string(), t.any()),
|
|
273
|
+
stepResults: t.array(stepResultEntrySchema),
|
|
274
|
+
progress: job.progress,
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
|
|
245
278
|
/** JobItem schema for list/get responses - typed per job */
|
|
246
279
|
function createJobItemSchema<T extends AnyJob>(job: T): JobItemSchemaType<T> {
|
|
247
280
|
return t.object({
|
|
248
281
|
id: t.string(),
|
|
249
|
-
queueName: t.string(),
|
|
250
|
-
priority: t.number().optional(),
|
|
251
|
-
progress: t.any(),
|
|
252
282
|
name: t.string(),
|
|
283
|
+
queue: t.string(),
|
|
253
284
|
data: job.input,
|
|
254
|
-
|
|
255
|
-
attemptsMade: t.number(),
|
|
256
|
-
processedOn: t.number().optional(),
|
|
257
|
-
finishedOn: t.number().optional(),
|
|
258
|
-
failedReason: t.string().optional(),
|
|
259
|
-
stacktrace: t.array(t.string()).optional(),
|
|
285
|
+
output: job.output.nullish(),
|
|
260
286
|
status: t.string(),
|
|
287
|
+
priority: t.number().optional(),
|
|
288
|
+
progress: createJobProgressCheckpointSchema(job).optional(),
|
|
289
|
+
attempts: t.number(),
|
|
290
|
+
startedAt: t.number().optional(),
|
|
291
|
+
completedAt: t.number().optional(),
|
|
292
|
+
error: t.string().optional(),
|
|
293
|
+
stacktrace: t.array(t.string()).optional(),
|
|
261
294
|
})
|
|
262
295
|
}
|
|
263
296
|
|
|
@@ -424,14 +457,14 @@ function createListProcedure(
|
|
|
424
457
|
jobName: job.options.name,
|
|
425
458
|
page: input.page,
|
|
426
459
|
limit: input.limit,
|
|
427
|
-
|
|
460
|
+
status: input.status,
|
|
428
461
|
},
|
|
429
462
|
'Listing jobs',
|
|
430
463
|
)
|
|
431
464
|
const result = await ctx.jobManager.list(job, {
|
|
432
465
|
page: input.page,
|
|
433
466
|
limit: input.limit,
|
|
434
|
-
|
|
467
|
+
status: input.status as JobStatus[],
|
|
435
468
|
})
|
|
436
469
|
ctx.logger.debug(
|
|
437
470
|
{ jobName: job.options.name, total: result.total, pages: result.pages },
|
|
@@ -7,15 +7,21 @@ import { UnrecoverableError } from 'bullmq'
|
|
|
7
7
|
import type { LifecycleHooks } from '../core/hooks.ts'
|
|
8
8
|
import type { AnyJob } from './job.ts'
|
|
9
9
|
import type { AnyJobStep } from './step.ts'
|
|
10
|
+
import type {
|
|
11
|
+
JobExecutionContext,
|
|
12
|
+
JobProgressCheckpoint,
|
|
13
|
+
StepResultEntry,
|
|
14
|
+
} from './types.ts'
|
|
10
15
|
import { LifecycleHook } from '../enums.ts'
|
|
11
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
currentJobInfo,
|
|
18
|
+
jobAbortSignal,
|
|
19
|
+
saveJobProgress,
|
|
20
|
+
} from '../injectables.ts'
|
|
12
21
|
|
|
13
22
|
export type JobRunnerOptions = { logging?: LoggingOptions }
|
|
14
23
|
|
|
15
|
-
export
|
|
16
|
-
data: Record<string, unknown> | null
|
|
17
|
-
duration: number
|
|
18
|
-
}
|
|
24
|
+
export type { StepResultEntry } from './types.ts'
|
|
19
25
|
|
|
20
26
|
export interface JobRunnerRunOptions {
|
|
21
27
|
signal: AbortSignal
|
|
@@ -42,6 +48,18 @@ export interface JobRunnerRunAfterStepParams<
|
|
|
42
48
|
stepResult: StepResultEntry
|
|
43
49
|
}
|
|
44
50
|
|
|
51
|
+
/** Context for saveProgress function - contains mutable state references */
|
|
52
|
+
export interface SaveProgressContext<
|
|
53
|
+
Options extends JobRunnerRunOptions = JobRunnerRunOptions,
|
|
54
|
+
> {
|
|
55
|
+
job: AnyJob
|
|
56
|
+
progress: Record<string, unknown>
|
|
57
|
+
result: Record<string, unknown>
|
|
58
|
+
stepResults: StepResultEntry[] | null
|
|
59
|
+
currentStepIndex: number
|
|
60
|
+
options: Options | null
|
|
61
|
+
}
|
|
62
|
+
|
|
45
63
|
export class JobRunner<
|
|
46
64
|
RunOptions extends JobRunnerRunOptions = JobRunnerRunOptions,
|
|
47
65
|
> {
|
|
@@ -61,6 +79,28 @@ export class JobRunner<
|
|
|
61
79
|
return this.runtime.container
|
|
62
80
|
}
|
|
63
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Creates a function that saves the current job progress state.
|
|
84
|
+
* Override in subclasses to implement actual persistence.
|
|
85
|
+
*/
|
|
86
|
+
protected createSaveProgressFn(
|
|
87
|
+
_context: SaveProgressContext<RunOptions>,
|
|
88
|
+
): () => Promise<void> {
|
|
89
|
+
// No-op in base runner - subclasses implement actual persistence
|
|
90
|
+
return async () => {}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Creates the current job info object.
|
|
95
|
+
* Override in subclasses to include additional info (e.g., BullMQ job ID).
|
|
96
|
+
*/
|
|
97
|
+
protected createJobInfo(
|
|
98
|
+
job: AnyJob,
|
|
99
|
+
_options: Partial<RunOptions>,
|
|
100
|
+
): JobExecutionContext {
|
|
101
|
+
return { name: job.options.name }
|
|
102
|
+
}
|
|
103
|
+
|
|
64
104
|
async runJob<T extends AnyJob>(
|
|
65
105
|
job: T,
|
|
66
106
|
data: any,
|
|
@@ -91,6 +131,23 @@ export class JobRunner<
|
|
|
91
131
|
const signal = anyAbortSignal(runSignal, stopListener.signal)
|
|
92
132
|
await using container = this.container.fork(Scope.Global)
|
|
93
133
|
await container.provide(jobAbortSignal, signal)
|
|
134
|
+
await container.provide(currentJobInfo, this.createJobInfo(job, options))
|
|
135
|
+
|
|
136
|
+
// Create mutable state context for saveProgress
|
|
137
|
+
const progressContext = {
|
|
138
|
+
job,
|
|
139
|
+
progress,
|
|
140
|
+
result,
|
|
141
|
+
stepResults: null as StepResultEntry[] | null, // Will be set below
|
|
142
|
+
currentStepIndex,
|
|
143
|
+
options: null as RunOptions | null, // Will be set below
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Provide saveProgress injectable
|
|
147
|
+
await container.provide(
|
|
148
|
+
saveJobProgress,
|
|
149
|
+
this.createSaveProgressFn(progressContext),
|
|
150
|
+
)
|
|
94
151
|
|
|
95
152
|
const jobDependencyContext = await container.createContext(job.dependencies)
|
|
96
153
|
const jobData = job.options.data
|
|
@@ -116,6 +173,10 @@ export class JobRunner<
|
|
|
116
173
|
...rest,
|
|
117
174
|
} satisfies JobRunnerRunOptions
|
|
118
175
|
|
|
176
|
+
// Update mutable context references
|
|
177
|
+
progressContext.stepResults = stepResults
|
|
178
|
+
progressContext.options = runOptions
|
|
179
|
+
|
|
119
180
|
for (
|
|
120
181
|
let stepIndex = currentStepIndex;
|
|
121
182
|
stepIndex < steps.length;
|
|
@@ -279,6 +340,51 @@ export class ApplicationWorkerJobRunner extends JobRunner<
|
|
|
279
340
|
super(runtime)
|
|
280
341
|
}
|
|
281
342
|
|
|
343
|
+
protected createJobInfo(
|
|
344
|
+
job: AnyJob,
|
|
345
|
+
options: Partial<JobRunnerRunOptions & { queueJob: Job }>,
|
|
346
|
+
): JobExecutionContext {
|
|
347
|
+
const { queueJob } = options
|
|
348
|
+
return {
|
|
349
|
+
name: job.options.name,
|
|
350
|
+
id: queueJob?.id,
|
|
351
|
+
queue: queueJob?.queueName,
|
|
352
|
+
attempts: queueJob?.attemptsMade,
|
|
353
|
+
stepIndex: options.currentStepIndex,
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
protected createSaveProgressFn(
|
|
358
|
+
context: SaveProgressContext<JobRunnerRunOptions & { queueJob: Job }>,
|
|
359
|
+
): () => Promise<void> {
|
|
360
|
+
return async () => {
|
|
361
|
+
const { job, progress, result, stepResults, options } = context
|
|
362
|
+
if (!options || !stepResults) return
|
|
363
|
+
|
|
364
|
+
const { queueJob } = options
|
|
365
|
+
|
|
366
|
+
// Find current step index based on completed steps
|
|
367
|
+
let currentStepIndex = 0
|
|
368
|
+
for (let i = 0; i < stepResults.length; i++) {
|
|
369
|
+
if (stepResults[i]) currentStepIndex = i + 1
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Encode progress before persisting if schema is defined
|
|
373
|
+
const encodedProgress = job.progress
|
|
374
|
+
? job.progress.encode(progress)
|
|
375
|
+
: progress
|
|
376
|
+
|
|
377
|
+
const checkpoint: JobProgressCheckpoint = {
|
|
378
|
+
stepIndex: currentStepIndex,
|
|
379
|
+
result,
|
|
380
|
+
stepResults,
|
|
381
|
+
progress: encodedProgress,
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
await queueJob.updateProgress(checkpoint)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
282
388
|
protected async afterStep(
|
|
283
389
|
params: JobRunnerRunAfterStepParams<
|
|
284
390
|
JobRunnerRunOptions & { queueJob: Job }
|
|
@@ -295,26 +401,25 @@ export class ApplicationWorkerJobRunner extends JobRunner<
|
|
|
295
401
|
options: { queueJob, progress },
|
|
296
402
|
} = params
|
|
297
403
|
const nextStepIndex = stepIndex + 1
|
|
298
|
-
const totalSteps = job.steps.length
|
|
299
|
-
const percentage = Math.round((nextStepIndex / totalSteps) * 100)
|
|
300
404
|
|
|
301
405
|
// Encode progress before persisting if schema is defined
|
|
302
406
|
const encodedProgress = job.progress
|
|
303
407
|
? job.progress.encode(progress)
|
|
304
408
|
: progress
|
|
305
409
|
|
|
410
|
+
const checkpoint: JobProgressCheckpoint = {
|
|
411
|
+
stepIndex: nextStepIndex,
|
|
412
|
+
stepLabel: step.label,
|
|
413
|
+
result,
|
|
414
|
+
stepResults,
|
|
415
|
+
progress: encodedProgress,
|
|
416
|
+
}
|
|
417
|
+
|
|
306
418
|
await Promise.all([
|
|
307
419
|
queueJob.log(
|
|
308
420
|
`Step ${step.label || nextStepIndex} completed in ${(stepResult.duration / 1000).toFixed(3)}s`,
|
|
309
421
|
),
|
|
310
|
-
queueJob.updateProgress(
|
|
311
|
-
stepIndex: nextStepIndex,
|
|
312
|
-
stepLabel: step.label,
|
|
313
|
-
result,
|
|
314
|
-
stepResults,
|
|
315
|
-
progress: encodedProgress,
|
|
316
|
-
percentage,
|
|
317
|
-
}),
|
|
422
|
+
queueJob.updateProgress(checkpoint),
|
|
318
423
|
])
|
|
319
424
|
}
|
|
320
425
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Job Definition Types (metadata about job structure)
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
/** Metadata about a job step in a job definition */
|
|
6
|
+
export interface JobStepInfo {
|
|
7
|
+
/** Optional human-readable label for the step */
|
|
8
|
+
label?: string
|
|
9
|
+
/** Whether this step has a condition that may skip it */
|
|
10
|
+
conditional: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Metadata about a job definition (not a running job instance) */
|
|
14
|
+
export interface JobDefinitionInfo {
|
|
15
|
+
/** Job name from definition */
|
|
16
|
+
name: string
|
|
17
|
+
/** Information about each step in the job */
|
|
18
|
+
steps: JobStepInfo[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Job Execution Types (runtime state of a running job)
|
|
23
|
+
// ============================================================================
|
|
24
|
+
|
|
25
|
+
/** Result of a single step execution */
|
|
26
|
+
export interface StepResultEntry {
|
|
27
|
+
/** Output data produced by the step, or null if skipped */
|
|
28
|
+
data: Record<string, unknown> | null
|
|
29
|
+
/** Duration in milliseconds */
|
|
30
|
+
duration: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Checkpoint data persisted for job resume support */
|
|
34
|
+
export interface JobProgressCheckpoint {
|
|
35
|
+
/** Index of the next step to execute (0 = not started, length = completed) */
|
|
36
|
+
stepIndex: number
|
|
37
|
+
/** Label of the last completed step */
|
|
38
|
+
stepLabel?: string
|
|
39
|
+
/** Accumulated result from all completed steps */
|
|
40
|
+
result: Record<string, unknown>
|
|
41
|
+
/** Results of each individual step */
|
|
42
|
+
stepResults: StepResultEntry[]
|
|
43
|
+
/** User-defined progress state */
|
|
44
|
+
progress: Record<string, unknown>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Information about the currently executing job, available via injectable */
|
|
48
|
+
export interface JobExecutionContext {
|
|
49
|
+
/** Job definition name */
|
|
50
|
+
name: string
|
|
51
|
+
/** Queue job ID */
|
|
52
|
+
id?: string
|
|
53
|
+
/** Queue name */
|
|
54
|
+
queue?: string
|
|
55
|
+
/** Number of attempts made so far */
|
|
56
|
+
attempts?: number
|
|
57
|
+
/** Current step index being executed */
|
|
58
|
+
stepIndex?: number
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// Job Item Types (job instances from queue)
|
|
63
|
+
// ============================================================================
|
|
64
|
+
|
|
65
|
+
/** Vendor-agnostic job status */
|
|
66
|
+
export type JobStatus =
|
|
67
|
+
| 'pending' // Queued, waiting to be picked up
|
|
68
|
+
| 'active' // Currently executing
|
|
69
|
+
| 'completed' // Successfully finished
|
|
70
|
+
| 'failed' // Failed (may retry)
|
|
71
|
+
| 'delayed' // Scheduled for future
|
|
72
|
+
| 'cancelled' // Manually cancelled
|
|
73
|
+
| 'unknown' // State cannot be determined
|
|
74
|
+
|
|
75
|
+
/** A job instance retrieved from the queue */
|
|
76
|
+
export interface JobItem<TInput = unknown, TOutput = unknown> {
|
|
77
|
+
/** Unique job instance ID */
|
|
78
|
+
id: string
|
|
79
|
+
/** Job definition name */
|
|
80
|
+
name: string
|
|
81
|
+
/** Queue name this job belongs to */
|
|
82
|
+
queue: string
|
|
83
|
+
/** Input data passed to the job */
|
|
84
|
+
data: TInput
|
|
85
|
+
/** Output produced by the job (if completed) */
|
|
86
|
+
output?: TOutput | null
|
|
87
|
+
/** Current job status */
|
|
88
|
+
status: JobStatus
|
|
89
|
+
/** Job priority (lower = higher priority) */
|
|
90
|
+
priority?: number
|
|
91
|
+
/** Job progress checkpoint (includes step state and user progress) */
|
|
92
|
+
progress?: JobProgressCheckpoint
|
|
93
|
+
/** Number of execution attempts */
|
|
94
|
+
attempts: number
|
|
95
|
+
/** Timestamp when job started processing (ms) */
|
|
96
|
+
startedAt?: number
|
|
97
|
+
/** Timestamp when job completed (ms) */
|
|
98
|
+
completedAt?: number
|
|
99
|
+
/** Error message if job failed */
|
|
100
|
+
error?: string
|
|
101
|
+
/** Stack trace if job failed */
|
|
102
|
+
stacktrace?: string[]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ============================================================================
|
|
106
|
+
// Injectable Types
|
|
107
|
+
// ============================================================================
|
|
108
|
+
|
|
109
|
+
/** Function to manually trigger saving job progress state */
|
|
110
|
+
export type SaveJobProgress = () => Promise<void>
|
|
@@ -203,6 +203,8 @@ export class ApplicationServerJobs {
|
|
|
203
203
|
|
|
204
204
|
async stop() {
|
|
205
205
|
const { logger } = this.params
|
|
206
|
+
// TODO: make configurable
|
|
207
|
+
const closeTimeout = 10_000 // 10 seconds timeout for graceful close
|
|
206
208
|
|
|
207
209
|
if (this.ui) {
|
|
208
210
|
await new Promise<void>((resolve) => {
|
|
@@ -212,29 +214,43 @@ export class ApplicationServerJobs {
|
|
|
212
214
|
})
|
|
213
215
|
}
|
|
214
216
|
|
|
217
|
+
// Stop accepting new jobs first.
|
|
215
218
|
await Promise.all(
|
|
216
|
-
this.
|
|
219
|
+
[...this.queueWorkers].map(async (worker) => {
|
|
217
220
|
try {
|
|
218
|
-
|
|
221
|
+
// Try graceful close with timeout
|
|
222
|
+
const closePromise = worker.close()
|
|
223
|
+
const timeoutPromise = new Promise<'timeout'>((resolve) =>
|
|
224
|
+
setTimeout(() => resolve('timeout'), closeTimeout),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
const result = await Promise.race([closePromise, timeoutPromise])
|
|
228
|
+
|
|
229
|
+
if (result === 'timeout') {
|
|
230
|
+
logger.warn(
|
|
231
|
+
{ worker: worker.name },
|
|
232
|
+
'Worker close timed out, forcing close',
|
|
233
|
+
)
|
|
234
|
+
await worker.close(true)
|
|
235
|
+
}
|
|
219
236
|
} catch (error) {
|
|
220
|
-
logger.warn({ error }, 'Failed to close
|
|
237
|
+
logger.warn({ error }, 'Failed to close BullMQ worker')
|
|
221
238
|
}
|
|
222
239
|
}),
|
|
223
240
|
)
|
|
224
|
-
this.
|
|
225
|
-
this.ui = undefined
|
|
241
|
+
this.queueWorkers.clear()
|
|
226
242
|
|
|
227
|
-
// Stop accepting new jobs first.
|
|
228
243
|
await Promise.all(
|
|
229
|
-
|
|
244
|
+
this.uiQueues.map(async (queue) => {
|
|
230
245
|
try {
|
|
231
|
-
await
|
|
246
|
+
await queue.close()
|
|
232
247
|
} catch (error) {
|
|
233
|
-
logger.warn({ error }, 'Failed to close
|
|
248
|
+
logger.warn({ error }, 'Failed to close Jobs UI queue')
|
|
234
249
|
}
|
|
235
250
|
}),
|
|
236
251
|
)
|
|
237
|
-
this.
|
|
252
|
+
this.uiQueues = []
|
|
253
|
+
this.ui = undefined
|
|
238
254
|
|
|
239
255
|
await Promise.all(
|
|
240
256
|
Array.from(this.pools.values()).map(async (pool) => {
|
|
@@ -3,7 +3,7 @@ import type { MessagePort } from 'node:worker_threads'
|
|
|
3
3
|
import { UnrecoverableError } from 'bullmq'
|
|
4
4
|
|
|
5
5
|
import type { JobWorkerPool } from '../enums.ts'
|
|
6
|
-
import type {
|
|
6
|
+
import type { JobProgressCheckpoint } from '../jobs/types.ts'
|
|
7
7
|
import type { ServerConfig } from '../server/config.ts'
|
|
8
8
|
import type { ServerPortMessage, ThreadPortMessage } from '../types.ts'
|
|
9
9
|
import { LifecycleHook, WorkerType } from '../enums.ts'
|
|
@@ -95,12 +95,7 @@ export class JobWorkerRuntime extends BaseWorkerRuntime {
|
|
|
95
95
|
|
|
96
96
|
// Load checkpoint from BullMQ progress for resume support
|
|
97
97
|
const checkpoint = bullJob.progress as
|
|
98
|
-
|
|
|
99
|
-
stepIndex: number
|
|
100
|
-
result: Record<string, unknown>
|
|
101
|
-
stepResults: StepResultEntry[]
|
|
102
|
-
progress: Record<string, unknown>
|
|
103
|
-
}
|
|
98
|
+
| JobProgressCheckpoint
|
|
104
99
|
| undefined
|
|
105
100
|
|
|
106
101
|
const result = await this.jobRunner.runJob(job, task.data, {
|