1
1
import * as date from 'date.js' ;
2
2
import * as debug from 'debug' ;
3
3
import { ObjectId } from 'mongodb' ;
4
+ import { fork } from 'child_process' ;
5
+ import * as getFunctionLocation from 'get-function-location' ;
4
6
import type { Agenda } from './index' ;
5
7
import type { DefinitionProcessor } from './types/JobDefinition' ;
6
8
import { IJobParameters , datefields , TJobDatefield } from './types/JobParameters' ;
7
9
import { JobPriority , parsePriority } from './utils/priority' ;
8
10
import { computeFromInterval , computeFromRepeatAt } from './utils/nextRunAt' ;
9
11
12
+ const controller = new AbortController ( ) ;
13
+ const { signal } = controller ;
14
+
10
15
const log = debug ( 'agenda:job' ) ;
11
16
12
17
/**
@@ -15,6 +20,8 @@ const log = debug('agenda:job');
15
20
export class Job < DATA = unknown | void > {
16
21
readonly attrs : IJobParameters < DATA > ;
17
22
23
+ static functionLocationCache : { [ key : string ] : string } = { } ;
24
+
18
25
/** this flag is set to true, if a job got canceled (e.g. due to a timeout or other exception),
19
26
* you can use it for long running tasks to periodically check if canceled is true,
20
27
* also touch will check if and throws that the job got canceled
@@ -209,7 +216,7 @@ export class Job<DATA = unknown | void> {
209
216
* @returns Whether or not job is running at the moment (true for running)
210
217
*/
211
218
async isRunning ( ) : Promise < boolean > {
212
- if ( ! this . byJobProcessor ) {
219
+ if ( ! this . byJobProcessor || this . attrs . fork ) {
213
220
// we have no job definition, therfore we are not the job processor, but a client call
214
221
// so we get the real state from database
215
222
await this . fetchStatus ( ) ;
@@ -237,6 +244,10 @@ export class Job<DATA = unknown | void> {
237
244
* Saves a job to database
238
245
*/
239
246
async save ( ) : Promise < Job > {
247
+ if ( this . agenda . forkedWorker ) {
248
+ console . warn ( 'calling save() on a Job during a forkedWorker has no effect!' ) ;
249
+ return this as Job ;
250
+ }
240
251
// ensure db connection is ready
241
252
await this . agenda . ready ;
242
253
return this . agenda . db . saveJob ( this as Job ) ;
@@ -250,7 +261,7 @@ export class Job<DATA = unknown | void> {
250
261
}
251
262
252
263
async isDead ( ) : Promise < boolean > {
253
- if ( ! this . byJobProcessor ) {
264
+ if ( ! this . byJobProcessor || this . attrs . fork ) {
254
265
// we have no job definition, therfore we are not the job processor, but a client call
255
266
// so we get the real state from database
256
267
await this . fetchStatus ( ) ;
@@ -259,7 +270,13 @@ export class Job<DATA = unknown | void> {
259
270
return this . isExpired ( ) ;
260
271
}
261
272
262
- isExpired ( ) : boolean {
273
+ async isExpired ( ) : Promise < boolean > {
274
+ if ( ! this . byJobProcessor || this . attrs . fork ) {
275
+ // we have no job definition, therfore we are not the job processor, but a client call
276
+ // so we get the real state from database
277
+ await this . fetchStatus ( ) ;
278
+ }
279
+
263
280
const definition = this . agenda . definitions [ this . attrs . name ] ;
264
281
265
282
const lockDeadline = new Date ( Date . now ( ) - definition . lockLifetime ) ;
@@ -317,8 +334,6 @@ export class Job<DATA = unknown | void> {
317
334
}
318
335
319
336
async run ( ) : Promise < void > {
320
- const definition = this . agenda . definitions [ this . attrs . name ] ;
321
-
322
337
this . attrs . lastRunAt = new Date ( ) ;
323
338
log (
324
339
'[%s:%s] setting lastRunAt to: %s' ,
@@ -333,33 +348,58 @@ export class Job<DATA = unknown | void> {
333
348
this . agenda . emit ( 'start' , this ) ;
334
349
this . agenda . emit ( `start:${ this . attrs . name } ` , this ) ;
335
350
log ( '[%s:%s] starting job' , this . attrs . name , this . attrs . _id ) ;
336
- if ( ! definition ) {
337
- log ( '[%s:%s] has no definition, can not run' , this . attrs . name , this . attrs . _id ) ;
338
- throw new Error ( 'Undefined job' ) ;
339
- }
340
351
341
- if ( definition . fn . length === 2 ) {
342
- log ( '[%s:%s] process function being called' , this . attrs . name , this . attrs . _id ) ;
352
+ if ( this . attrs . fork && this . agenda . forkHelper ) {
353
+ const { forkHelper } = this . agenda ;
354
+ const location =
355
+ Job . functionLocationCache [ this . attrs . name ] ||
356
+ ( await getFunctionLocation ( this . agenda . definitions [ this . attrs . name ] . fn ) ) . source . replace (
357
+ / ^ f i l e : \/ \/ / ,
358
+ ''
359
+ ) ;
360
+
361
+ if ( ! Job . functionLocationCache [ this . attrs . name ] ) {
362
+ Job . functionLocationCache [ this . attrs . name ] = location ;
363
+ }
364
+ // console.log('location', location);
365
+
343
366
await new Promise < void > ( ( resolve , reject ) => {
344
- try {
345
- const result = definition . fn ( this as Job , error => {
346
- if ( error ) {
347
- reject ( error ) ;
348
- return ;
349
- }
367
+ let stillRunning = true ;
368
+ const child = fork ( forkHelper , [ this . attrs . name , this . attrs . _id ! . toString ( ) , location ] , {
369
+ signal
370
+ } ) ;
371
+
372
+ child . on ( 'close' , code => {
373
+ console . log ( `child process exited with code ${ code } ` ) ;
374
+ stillRunning = false ;
375
+ if ( code ) {
376
+ reject ( code ) ;
377
+ } else {
350
378
resolve ( ) ;
351
- } ) ;
352
-
353
- if ( this . isPromise ( result ) ) {
354
- result . catch ( ( error : Error ) => reject ( error ) ) ;
355
379
}
356
- } catch ( error ) {
357
- reject ( error ) ;
358
- }
380
+ } ) ;
381
+ child . on ( 'message' , message => {
382
+ console . log ( `Message from child.js: ${ message } ` , JSON . stringify ( message ) ) ;
383
+ if ( typeof message === 'string' ) {
384
+ reject ( JSON . parse ( message ) ) ;
385
+ } else {
386
+ reject ( message ) ;
387
+ }
388
+ } ) ;
389
+
390
+ // check if job is still alive
391
+ const checkCancel = ( ) =>
392
+ setTimeout ( ( ) => {
393
+ if ( this . canceled ) {
394
+ controller . abort ( ) ; // Stops the child process
395
+ } else if ( stillRunning ) {
396
+ setTimeout ( checkCancel , 10000 ) ;
397
+ }
398
+ } ) ;
399
+ checkCancel ( ) ;
359
400
} ) ;
360
401
} else {
361
- log ( '[%s:%s] process function being called' , this . attrs . name , this . attrs . _id ) ;
362
- await ( definition . fn as DefinitionProcessor < DATA , void > ) ( this ) ;
402
+ await this . runJob ( ) ;
363
403
}
364
404
365
405
this . attrs . lastFinishedAt = new Date ( ) ;
@@ -397,6 +437,39 @@ export class Job<DATA = unknown | void> {
397
437
}
398
438
}
399
439
440
+ async runJob ( ) {
441
+ const definition = this . agenda . definitions [ this . attrs . name ] ;
442
+
443
+ if ( ! definition ) {
444
+ log ( '[%s:%s] has no definition, can not run' , this . attrs . name , this . attrs . _id ) ;
445
+ throw new Error ( 'Undefined job' ) ;
446
+ }
447
+
448
+ if ( definition . fn . length === 2 ) {
449
+ log ( '[%s:%s] process function being called' , this . attrs . name , this . attrs . _id ) ;
450
+ await new Promise < void > ( ( resolve , reject ) => {
451
+ try {
452
+ const result = definition . fn ( this as Job , error => {
453
+ if ( error ) {
454
+ reject ( error ) ;
455
+ return ;
456
+ }
457
+ resolve ( ) ;
458
+ } ) ;
459
+
460
+ if ( this . isPromise ( result ) ) {
461
+ result . catch ( ( error : Error ) => reject ( error ) ) ;
462
+ }
463
+ } catch ( error ) {
464
+ reject ( error ) ;
465
+ }
466
+ } ) ;
467
+ } else {
468
+ log ( '[%s:%s] process function being called' , this . attrs . name , this . attrs . _id ) ;
469
+ await ( definition . fn as DefinitionProcessor < DATA , void > ) ( this ) ;
470
+ }
471
+ }
472
+
400
473
private isPromise ( value : unknown ) : value is Promise < void > {
401
474
return ! ! ( value && typeof ( value as Promise < void > ) . then === 'function' ) ;
402
475
}
0 commit comments