@@ -17,17 +17,27 @@ import {
17
17
WorkerEvents ,
18
18
} from "./interfaces" ;
19
19
import {
20
+ calculateDelay ,
20
21
coerceError ,
21
22
CompiledOptions ,
22
23
CompiledSharedOptions ,
23
24
Releasers ,
25
+ RetryOptions ,
26
+ sleep ,
24
27
} from "./lib" ;
25
28
26
29
interface CronRequirements {
27
30
pgPool : Pool ;
28
31
events : WorkerEvents ;
29
32
}
30
33
34
+ const CRON_RETRY : RetryOptions = {
35
+ maxAttempts : Infinity ,
36
+ minDelay : 200 ,
37
+ maxDelay : 60_000 ,
38
+ multiplier : 1.5 ,
39
+ } ;
40
+
31
41
/**
32
42
* This function looks through all the cron items we have (e.g. from our
33
43
* crontab file) and compares them to the items we already know about. If the
@@ -117,13 +127,15 @@ async function scheduleCronJobs(
117
127
jobsAndIdentifiers : JobAndCronIdentifier [ ] ,
118
128
ts : string ,
119
129
useNodeTime : boolean ,
130
+ workerSchema : string ,
131
+ preparedStatements : boolean ,
120
132
) {
121
133
// TODO: refactor this to use `add_jobs`
122
134
123
135
// Note that `identifier` is guaranteed to be unique for every record
124
136
// in `specs`.
125
- await pgPool . query (
126
- `
137
+ await pgPool . query ( {
138
+ text : `
127
139
with specs as (
128
140
select
129
141
index,
@@ -166,26 +178,45 @@ async function scheduleCronJobs(
166
178
inner join locks on (locks.identifier = specs.identifier)
167
179
order by specs.index asc
168
180
` ,
169
- [
181
+ values : [
170
182
JSON . stringify ( jobsAndIdentifiers ) ,
171
183
ts ,
172
184
useNodeTime ? new Date ( ) . toISOString ( ) : null ,
173
185
] ,
174
- ) ;
186
+ name : ! preparedStatements
187
+ ? undefined
188
+ : `cron${ useNodeTime ? "N" : "" } /${ workerSchema } ` ,
189
+ } ) ;
175
190
}
176
191
177
192
/**
178
193
* Marks any previously unknown crontab identifiers as now being known. Then
179
194
* performs backfilling on any crontab tasks that need it.
180
195
*/
181
- async function registerAndBackfillItems (
182
- ctx : CompiledSharedOptions ,
183
- { pgPool, events, cron } : { pgPool : Pool ; events : WorkerEvents ; cron : Cron } ,
184
- escapedWorkerSchema : string ,
185
- parsedCronItems : ParsedCronItem [ ] ,
186
- startTime : Date ,
187
- useNodeTime : boolean ,
188
- ) {
196
+ async function registerAndBackfillItems ( details : {
197
+ ctx : CompiledSharedOptions ;
198
+ pgPool : Pool ;
199
+ events : WorkerEvents ;
200
+ cron : Cron ;
201
+ workerSchema : string ;
202
+ preparedStatements : boolean ;
203
+ escapedWorkerSchema : string ;
204
+ parsedCronItems : ParsedCronItem [ ] ;
205
+ startTime : Date ;
206
+ useNodeTime : boolean ;
207
+ } ) {
208
+ const {
209
+ ctx,
210
+ pgPool,
211
+ events,
212
+ cron,
213
+ workerSchema,
214
+ preparedStatements,
215
+ escapedWorkerSchema,
216
+ parsedCronItems,
217
+ startTime,
218
+ useNodeTime,
219
+ } = details ;
189
220
// First, scan the DB to get our starting point.
190
221
const { rows } = await pgPool . query < KnownCrontab > (
191
222
`SELECT * FROM ${ escapedWorkerSchema } ._private_known_crontabs as known_crontabs` ,
@@ -273,6 +304,8 @@ async function registerAndBackfillItems(
273
304
itemsToBackfill ,
274
305
ts ,
275
306
useNodeTime ,
307
+ workerSchema ,
308
+ preparedStatements ,
276
309
) ;
277
310
}
278
311
@@ -305,9 +338,10 @@ export const runCron = (
305
338
const {
306
339
logger,
307
340
escapedWorkerSchema,
341
+ workerSchema,
308
342
events,
309
343
resolvedPreset : {
310
- worker : { useNodeTime } ,
344
+ worker : { useNodeTime, preparedStatements = true } ,
311
345
} ,
312
346
} = compiledSharedOptions ;
313
347
@@ -323,7 +357,7 @@ export const runCron = (
323
357
if ( ! stopCalled ) {
324
358
stopCalled = true ;
325
359
if ( e ) {
326
- promise . reject ( e ) ;
360
+ promise . reject ( coerceError ( e ) ) ;
327
361
} else {
328
362
promise . resolve ( ) ;
329
363
}
@@ -334,6 +368,17 @@ export const runCron = (
334
368
}
335
369
}
336
370
371
+ let attempts = 0 ;
372
+ function restartCronAfterDelay ( error : Error ) {
373
+ ++ attempts ;
374
+ const delay = calculateDelay ( attempts - 1 , CRON_RETRY ) ;
375
+ logger . error (
376
+ `Cron hit an error; restarting in ${ delay } ms (attempt ${ attempts } ): ${ error } ` ,
377
+ { error, attempts } ,
378
+ ) ;
379
+ sleep ( delay ) . then ( cronMain ) . catch ( restartCronAfterDelay ) ;
380
+ }
381
+
337
382
async function cronMain ( ) {
338
383
if ( ! cron . _active ) {
339
384
return stop ( ) ;
@@ -345,14 +390,18 @@ export const runCron = (
345
390
346
391
// We must backfill BEFORE scheduling any new jobs otherwise backfill won't
347
392
// work due to known_crontabs.last_execution having been updated.
348
- await registerAndBackfillItems (
393
+ await registerAndBackfillItems ( {
349
394
ctx,
350
- { pgPool, events, cron } ,
395
+ pgPool,
396
+ events,
397
+ cron,
398
+ workerSchema,
399
+ preparedStatements,
351
400
escapedWorkerSchema,
352
401
parsedCronItems,
353
- new Date ( + start ) ,
402
+ startTime : new Date ( + start ) ,
354
403
useNodeTime,
355
- ) ;
404
+ } ) ;
356
405
357
406
events . emit ( "cron:started" , { ctx, cron, start } ) ;
358
407
@@ -383,6 +432,8 @@ export const runCron = (
383
432
if ( ! cron . _active ) {
384
433
return stop ( ) ;
385
434
}
435
+ // Healthy!
436
+ attempts = 0 ;
386
437
387
438
// THIS MUST COME BEFORE nextTimestamp IS MUTATED
388
439
const digest = digestTimestamp ( nextTimestamp ) ;
@@ -466,6 +517,8 @@ export const runCron = (
466
517
jobsAndIdentifiers ,
467
518
ts ,
468
519
useNodeTime ,
520
+ workerSchema ,
521
+ preparedStatements ,
469
522
) ;
470
523
events . emit ( "cron:scheduled" , {
471
524
ctx,
@@ -486,9 +539,10 @@ export const runCron = (
486
539
// timestamps on error).
487
540
scheduleNextLoop ( ) ;
488
541
} catch ( e ) {
489
- // If something goes wrong; abort. The calling code should re-schedule
490
- // which will re-trigger the backfilling code.
491
- return stop ( coerceError ( e ) ) ;
542
+ // If something goes wrong; abort the current loop and restart cron
543
+ // after an exponential back-off. This is essential because we need to
544
+ // re-trigger the backfilling code.
545
+ return restartCronAfterDelay ( coerceError ( e ) ) ;
492
546
}
493
547
}
494
548
@@ -510,7 +564,7 @@ export const runCron = (
510
564
promise,
511
565
} ;
512
566
513
- cronMain ( ) . catch ( stop ) ;
567
+ cronMain ( ) . catch ( restartCronAfterDelay ) ;
514
568
515
569
return cron ;
516
570
} ;
0 commit comments