@quave/agenda 1.0.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/LICENSE.md +25 -0
- package/README.md +380 -0
- package/dist/Job.d.ts +190 -0
- package/dist/Job.js +689 -0
- package/dist/Job.js.map +1 -0
- package/dist/JobProcessingQueue.d.ts +47 -0
- package/dist/JobProcessingQueue.js +107 -0
- package/dist/JobProcessingQueue.js.map +1 -0
- package/dist/JobProcessor.d.ts +72 -0
- package/dist/JobProcessor.js +512 -0
- package/dist/JobProcessor.js.map +1 -0
- package/dist/backends/index.d.ts +18 -0
- package/dist/backends/index.js +20 -0
- package/dist/backends/index.js.map +1 -0
- package/dist/cjs/Job.d.ts +190 -0
- package/dist/cjs/Job.js +696 -0
- package/dist/cjs/Job.js.map +1 -0
- package/dist/cjs/JobProcessingQueue.d.ts +47 -0
- package/dist/cjs/JobProcessingQueue.js +111 -0
- package/dist/cjs/JobProcessingQueue.js.map +1 -0
- package/dist/cjs/JobProcessor.d.ts +72 -0
- package/dist/cjs/JobProcessor.js +519 -0
- package/dist/cjs/JobProcessor.js.map +1 -0
- package/dist/cjs/backends/index.d.ts +17 -0
- package/dist/cjs/backends/index.js +20 -0
- package/dist/cjs/backends/index.js.map +1 -0
- package/dist/cjs/decorators/Define.d.ts +33 -0
- package/dist/cjs/decorators/Define.js +47 -0
- package/dist/cjs/decorators/Define.js.map +1 -0
- package/dist/cjs/decorators/Every.d.ts +43 -0
- package/dist/cjs/decorators/Every.js +58 -0
- package/dist/cjs/decorators/Every.js.map +1 -0
- package/dist/cjs/decorators/JobsController.d.ts +43 -0
- package/dist/cjs/decorators/JobsController.js +40 -0
- package/dist/cjs/decorators/JobsController.js.map +1 -0
- package/dist/cjs/decorators/index.d.ts +41 -0
- package/dist/cjs/decorators/index.js +57 -0
- package/dist/cjs/decorators/index.js.map +1 -0
- package/dist/cjs/decorators/metadata.d.ts +50 -0
- package/dist/cjs/decorators/metadata.js +41 -0
- package/dist/cjs/decorators/metadata.js.map +1 -0
- package/dist/cjs/decorators/register.d.ts +52 -0
- package/dist/cjs/decorators/register.js +145 -0
- package/dist/cjs/decorators/register.js.map +1 -0
- package/dist/cjs/index.d.ts +498 -0
- package/dist/cjs/index.js +776 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/notifications/BaseNotificationChannel.d.ts +47 -0
- package/dist/cjs/notifications/BaseNotificationChannel.js +112 -0
- package/dist/cjs/notifications/BaseNotificationChannel.js.map +1 -0
- package/dist/cjs/notifications/InMemoryNotificationChannel.d.ts +13 -0
- package/dist/cjs/notifications/InMemoryNotificationChannel.js +37 -0
- package/dist/cjs/notifications/InMemoryNotificationChannel.js.map +1 -0
- package/dist/cjs/notifications/index.d.ts +2 -0
- package/dist/cjs/notifications/index.js +8 -0
- package/dist/cjs/notifications/index.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/types/AgendaBackend.d.ts +61 -0
- package/dist/cjs/types/AgendaBackend.js +3 -0
- package/dist/cjs/types/AgendaBackend.js.map +1 -0
- package/dist/cjs/types/AgendaConfig.d.ts +10 -0
- package/dist/cjs/types/AgendaConfig.js +3 -0
- package/dist/cjs/types/AgendaConfig.js.map +1 -0
- package/dist/cjs/types/AgendaStatus.d.ts +32 -0
- package/dist/cjs/types/AgendaStatus.js +3 -0
- package/dist/cjs/types/AgendaStatus.js.map +1 -0
- package/dist/cjs/types/DbOptions.d.ts +4 -0
- package/dist/cjs/types/DbOptions.js +3 -0
- package/dist/cjs/types/DbOptions.js.map +1 -0
- package/dist/cjs/types/DrainOptions.d.ts +37 -0
- package/dist/cjs/types/DrainOptions.js +3 -0
- package/dist/cjs/types/DrainOptions.js.map +1 -0
- package/dist/cjs/types/JobDefinition.d.ts +49 -0
- package/dist/cjs/types/JobDefinition.js +3 -0
- package/dist/cjs/types/JobDefinition.js.map +1 -0
- package/dist/cjs/types/JobLogger.d.ts +111 -0
- package/dist/cjs/types/JobLogger.js +3 -0
- package/dist/cjs/types/JobLogger.js.map +1 -0
- package/dist/cjs/types/JobParameters.d.ts +106 -0
- package/dist/cjs/types/JobParameters.js +21 -0
- package/dist/cjs/types/JobParameters.js.map +1 -0
- package/dist/cjs/types/JobQuery.d.ts +79 -0
- package/dist/cjs/types/JobQuery.js +36 -0
- package/dist/cjs/types/JobQuery.js.map +1 -0
- package/dist/cjs/types/JobRepository.d.ts +101 -0
- package/dist/cjs/types/JobRepository.js +3 -0
- package/dist/cjs/types/JobRepository.js.map +1 -0
- package/dist/cjs/types/NotificationChannel.d.ts +120 -0
- package/dist/cjs/types/NotificationChannel.js +3 -0
- package/dist/cjs/types/NotificationChannel.js.map +1 -0
- package/dist/cjs/utils/backoff.d.ts +152 -0
- package/dist/cjs/utils/backoff.js +175 -0
- package/dist/cjs/utils/backoff.js.map +1 -0
- package/dist/cjs/utils/dateConstraints.d.ts +48 -0
- package/dist/cjs/utils/dateConstraints.js +127 -0
- package/dist/cjs/utils/dateConstraints.js.map +1 -0
- package/dist/cjs/utils/isValidDate.d.ts +1 -0
- package/dist/cjs/utils/isValidDate.js +8 -0
- package/dist/cjs/utils/isValidDate.js.map +1 -0
- package/dist/cjs/utils/nextRunAt.d.ts +10 -0
- package/dist/cjs/utils/nextRunAt.js +119 -0
- package/dist/cjs/utils/nextRunAt.js.map +1 -0
- package/dist/cjs/utils/priority.d.ts +13 -0
- package/dist/cjs/utils/priority.js +23 -0
- package/dist/cjs/utils/priority.js.map +1 -0
- package/dist/cjs/utils/processEvery.d.ts +1 -0
- package/dist/cjs/utils/processEvery.js +13 -0
- package/dist/cjs/utils/processEvery.js.map +1 -0
- package/dist/cjs/utils/stack.d.ts +1 -0
- package/dist/cjs/utils/stack.js +21 -0
- package/dist/cjs/utils/stack.js.map +1 -0
- package/dist/cjs/version.d.ts +9 -0
- package/dist/cjs/version.js +13 -0
- package/dist/cjs/version.js.map +1 -0
- package/dist/decorators/Define.d.ts +33 -0
- package/dist/decorators/Define.js +44 -0
- package/dist/decorators/Define.js.map +1 -0
- package/dist/decorators/Every.d.ts +43 -0
- package/dist/decorators/Every.js +55 -0
- package/dist/decorators/Every.js.map +1 -0
- package/dist/decorators/JobsController.d.ts +43 -0
- package/dist/decorators/JobsController.js +37 -0
- package/dist/decorators/JobsController.js.map +1 -0
- package/dist/decorators/index.d.ts +41 -0
- package/dist/decorators/index.js +46 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/decorators/metadata.d.ts +50 -0
- package/dist/decorators/metadata.js +35 -0
- package/dist/decorators/metadata.js.map +1 -0
- package/dist/decorators/register.d.ts +52 -0
- package/dist/decorators/register.js +139 -0
- package/dist/decorators/register.js.map +1 -0
- package/dist/index.d.ts +498 -0
- package/dist/index.js +744 -0
- package/dist/index.js.map +1 -0
- package/dist/notifications/BaseNotificationChannel.d.ts +47 -0
- package/dist/notifications/BaseNotificationChannel.js +108 -0
- package/dist/notifications/BaseNotificationChannel.js.map +1 -0
- package/dist/notifications/InMemoryNotificationChannel.d.ts +13 -0
- package/dist/notifications/InMemoryNotificationChannel.js +33 -0
- package/dist/notifications/InMemoryNotificationChannel.js.map +1 -0
- package/dist/notifications/index.d.ts +2 -0
- package/dist/notifications/index.js +3 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/types/AgendaBackend.d.ts +61 -0
- package/dist/types/AgendaBackend.js +2 -0
- package/dist/types/AgendaBackend.js.map +1 -0
- package/dist/types/AgendaConfig.d.ts +10 -0
- package/dist/types/AgendaConfig.js +2 -0
- package/dist/types/AgendaConfig.js.map +1 -0
- package/dist/types/AgendaStatus.d.ts +32 -0
- package/dist/types/AgendaStatus.js +2 -0
- package/dist/types/AgendaStatus.js.map +1 -0
- package/dist/types/DbOptions.d.ts +4 -0
- package/dist/types/DbOptions.js +2 -0
- package/dist/types/DbOptions.js.map +1 -0
- package/dist/types/DrainOptions.d.ts +37 -0
- package/dist/types/DrainOptions.js +2 -0
- package/dist/types/DrainOptions.js.map +1 -0
- package/dist/types/JobDefinition.d.ts +49 -0
- package/dist/types/JobDefinition.js +2 -0
- package/dist/types/JobDefinition.js.map +1 -0
- package/dist/types/JobLogger.d.ts +111 -0
- package/dist/types/JobLogger.js +2 -0
- package/dist/types/JobLogger.js.map +1 -0
- package/dist/types/JobParameters.d.ts +106 -0
- package/dist/types/JobParameters.js +17 -0
- package/dist/types/JobParameters.js.map +1 -0
- package/dist/types/JobQuery.d.ts +79 -0
- package/dist/types/JobQuery.js +33 -0
- package/dist/types/JobQuery.js.map +1 -0
- package/dist/types/JobRepository.d.ts +101 -0
- package/dist/types/JobRepository.js +2 -0
- package/dist/types/JobRepository.js.map +1 -0
- package/dist/types/NotificationChannel.d.ts +120 -0
- package/dist/types/NotificationChannel.js +2 -0
- package/dist/types/NotificationChannel.js.map +1 -0
- package/dist/utils/backoff.d.ts +152 -0
- package/dist/utils/backoff.js +167 -0
- package/dist/utils/backoff.js.map +1 -0
- package/dist/utils/dateConstraints.d.ts +48 -0
- package/dist/utils/dateConstraints.js +117 -0
- package/dist/utils/dateConstraints.js.map +1 -0
- package/dist/utils/isValidDate.d.ts +1 -0
- package/dist/utils/isValidDate.js +5 -0
- package/dist/utils/isValidDate.js.map +1 -0
- package/dist/utils/nextRunAt.d.ts +10 -0
- package/dist/utils/nextRunAt.js +110 -0
- package/dist/utils/nextRunAt.js.map +1 -0
- package/dist/utils/priority.d.ts +13 -0
- package/dist/utils/priority.js +20 -0
- package/dist/utils/priority.js.map +1 -0
- package/dist/utils/processEvery.d.ts +1 -0
- package/dist/utils/processEvery.js +7 -0
- package/dist/utils/processEvery.js.map +1 -0
- package/dist/utils/stack.d.ts +1 -0
- package/dist/utils/stack.js +18 -0
- package/dist/utils/stack.js.map +1 -0
- package/dist/version.d.ts +9 -0
- package/dist/version.js +10 -0
- package/dist/version.js.map +1 -0
- package/package.json +79 -0
- package/test/fixtures/forkHelper.js +40 -0
- package/test/fixtures/someJobDefinition.ts +12 -0
- package/test/shared/agenda-test-suite.ts +1876 -0
- package/test/shared/backoff-test-suite.ts +408 -0
- package/test/shared/debounce-test-suite.ts +377 -0
- package/test/shared/full-test-suite.ts +161 -0
- package/test/shared/index.ts +50 -0
- package/test/shared/joblogger-test-suite.ts +305 -0
- package/test/shared/jobprocessor-test-suite.ts +266 -0
- package/test/shared/notification-channel-test-suite.ts +471 -0
- package/test/shared/removeoncomplete-test-suite.ts +228 -0
- package/test/shared/repository-test-suite.ts +708 -0
- package/test/shared/retry-test-suite.ts +174 -0
- package/test/shared/test-utils.ts +100 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backoff test factory
|
|
3
|
+
*
|
|
4
|
+
* This file exports a test factory for job backoff/retry tests.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll } from 'vitest';
|
|
8
|
+
import type { AgendaBackend, RetryDetails, JobWithId } from '../../src/index.js';
|
|
9
|
+
import {
|
|
10
|
+
Agenda,
|
|
11
|
+
backoffStrategies,
|
|
12
|
+
constant,
|
|
13
|
+
linear,
|
|
14
|
+
exponential,
|
|
15
|
+
combine,
|
|
16
|
+
when
|
|
17
|
+
} from '../../src/index.js';
|
|
18
|
+
|
|
19
|
+
export interface BackoffTestConfig {
|
|
20
|
+
/** Name for the test suite */
|
|
21
|
+
name: string;
|
|
22
|
+
/** Factory to create a fresh backend instance */
|
|
23
|
+
createBackend: () => Promise<AgendaBackend>;
|
|
24
|
+
/** Cleanup function called after tests */
|
|
25
|
+
cleanupBackend: (backend: AgendaBackend) => Promise<void>;
|
|
26
|
+
/** Clear all jobs between tests */
|
|
27
|
+
clearJobs: (backend: AgendaBackend) => Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Backoff/Retry tests
|
|
32
|
+
*/
|
|
33
|
+
export function backoffTestSuite(config: BackoffTestConfig): void {
|
|
34
|
+
describe(`${config.name} - Backoff Strategies`, () => {
|
|
35
|
+
let backend: AgendaBackend;
|
|
36
|
+
let agenda: Agenda;
|
|
37
|
+
|
|
38
|
+
beforeAll(async () => {
|
|
39
|
+
backend = await config.createBackend();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterAll(async () => {
|
|
43
|
+
await config.cleanupBackend(backend);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
beforeEach(async () => {
|
|
47
|
+
await config.clearJobs(backend);
|
|
48
|
+
agenda = new Agenda({
|
|
49
|
+
backend,
|
|
50
|
+
processEvery: 50 // Fast polling for tests
|
|
51
|
+
});
|
|
52
|
+
await agenda.ready;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(async () => {
|
|
56
|
+
if (agenda) {
|
|
57
|
+
await agenda.stop();
|
|
58
|
+
}
|
|
59
|
+
await config.clearJobs(backend);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('constant backoff', () => {
|
|
63
|
+
it('should return the same delay for each attempt', () => {
|
|
64
|
+
const strategy = constant({ delay: 1000, maxRetries: 3 });
|
|
65
|
+
|
|
66
|
+
expect(strategy({ attempt: 1, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(1000);
|
|
67
|
+
expect(strategy({ attempt: 2, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(1000);
|
|
68
|
+
expect(strategy({ attempt: 3, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(1000);
|
|
69
|
+
expect(strategy({ attempt: 4, error: new Error('test'), jobName: 'test', jobData: {} })).toBeNull();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should respect maxDelay', () => {
|
|
73
|
+
const strategy = constant({ delay: 5000, maxDelay: 2000, maxRetries: 3 });
|
|
74
|
+
|
|
75
|
+
expect(strategy({ attempt: 1, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(2000);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('linear backoff', () => {
|
|
80
|
+
it('should increase delay linearly', () => {
|
|
81
|
+
const strategy = linear({ delay: 1000, increment: 1000, maxRetries: 4 });
|
|
82
|
+
|
|
83
|
+
expect(strategy({ attempt: 1, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(1000);
|
|
84
|
+
expect(strategy({ attempt: 2, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(2000);
|
|
85
|
+
expect(strategy({ attempt: 3, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(3000);
|
|
86
|
+
expect(strategy({ attempt: 4, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(4000);
|
|
87
|
+
expect(strategy({ attempt: 5, error: new Error('test'), jobName: 'test', jobData: {} })).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should use delay as default increment', () => {
|
|
91
|
+
const strategy = linear({ delay: 500, maxRetries: 3 });
|
|
92
|
+
|
|
93
|
+
expect(strategy({ attempt: 1, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(500);
|
|
94
|
+
expect(strategy({ attempt: 2, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(1000);
|
|
95
|
+
expect(strategy({ attempt: 3, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(1500);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should respect maxDelay', () => {
|
|
99
|
+
const strategy = linear({ delay: 1000, increment: 2000, maxDelay: 3000, maxRetries: 5 });
|
|
100
|
+
|
|
101
|
+
expect(strategy({ attempt: 1, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(1000);
|
|
102
|
+
expect(strategy({ attempt: 2, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(3000); // capped
|
|
103
|
+
expect(strategy({ attempt: 3, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(3000); // capped
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('exponential backoff', () => {
|
|
108
|
+
it('should increase delay exponentially', () => {
|
|
109
|
+
const strategy = exponential({ delay: 100, factor: 2, maxRetries: 5 });
|
|
110
|
+
|
|
111
|
+
expect(strategy({ attempt: 1, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(100);
|
|
112
|
+
expect(strategy({ attempt: 2, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(200);
|
|
113
|
+
expect(strategy({ attempt: 3, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(400);
|
|
114
|
+
expect(strategy({ attempt: 4, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(800);
|
|
115
|
+
expect(strategy({ attempt: 5, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(1600);
|
|
116
|
+
expect(strategy({ attempt: 6, error: new Error('test'), jobName: 'test', jobData: {} })).toBeNull();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should respect maxDelay', () => {
|
|
120
|
+
const strategy = exponential({ delay: 100, factor: 2, maxDelay: 500, maxRetries: 10 });
|
|
121
|
+
|
|
122
|
+
expect(strategy({ attempt: 1, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(100);
|
|
123
|
+
expect(strategy({ attempt: 2, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(200);
|
|
124
|
+
expect(strategy({ attempt: 3, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(400);
|
|
125
|
+
expect(strategy({ attempt: 4, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(500); // capped
|
|
126
|
+
expect(strategy({ attempt: 5, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(500); // capped
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should apply jitter when configured', () => {
|
|
130
|
+
const strategy = exponential({ delay: 1000, factor: 2, maxRetries: 3, jitter: 0.5 });
|
|
131
|
+
|
|
132
|
+
// Run multiple times to check jitter variation
|
|
133
|
+
const results = new Set<number>();
|
|
134
|
+
for (let i = 0; i < 20; i++) {
|
|
135
|
+
const delay = strategy({ attempt: 1, error: new Error('test'), jobName: 'test', jobData: {} });
|
|
136
|
+
if (delay !== null) results.add(delay);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// With 50% jitter, we should see variation (500-1500 range)
|
|
140
|
+
expect(results.size).toBeGreaterThan(1);
|
|
141
|
+
|
|
142
|
+
// All values should be within jitter range
|
|
143
|
+
for (const delay of results) {
|
|
144
|
+
expect(delay).toBeGreaterThanOrEqual(500);
|
|
145
|
+
expect(delay).toBeLessThanOrEqual(1500);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('combine strategy', () => {
|
|
151
|
+
it('should try strategies in sequence', () => {
|
|
152
|
+
const strategy = combine(
|
|
153
|
+
// First 2 attempts: 100ms
|
|
154
|
+
(ctx) => ctx.attempt <= 2 ? 100 : null,
|
|
155
|
+
// Next 2 attempts: use exponential-like custom logic
|
|
156
|
+
// (since exponential counts globally, we need custom logic to handle offset)
|
|
157
|
+
(ctx) => {
|
|
158
|
+
if (ctx.attempt > 4) return null;
|
|
159
|
+
// Exponential from attempt 3 onwards
|
|
160
|
+
const adjustedAttempt = ctx.attempt - 2; // 3->1, 4->2
|
|
161
|
+
return 1000 * Math.pow(2, adjustedAttempt - 1);
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
expect(strategy({ attempt: 1, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(100);
|
|
166
|
+
expect(strategy({ attempt: 2, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(100);
|
|
167
|
+
expect(strategy({ attempt: 3, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(1000);
|
|
168
|
+
expect(strategy({ attempt: 4, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(2000);
|
|
169
|
+
expect(strategy({ attempt: 5, error: new Error('test'), jobName: 'test', jobData: {} })).toBeNull();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('when (conditional) strategy', () => {
|
|
174
|
+
it('should only retry when condition is met', () => {
|
|
175
|
+
const strategy = when(
|
|
176
|
+
(ctx) => ctx.error.message.includes('timeout'),
|
|
177
|
+
constant({ delay: 1000, maxRetries: 3 })
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const timeoutError = new Error('connection timeout');
|
|
181
|
+
const otherError = new Error('validation failed');
|
|
182
|
+
|
|
183
|
+
expect(strategy({ attempt: 1, error: timeoutError, jobName: 'test', jobData: {} })).toBe(1000);
|
|
184
|
+
expect(strategy({ attempt: 1, error: otherError, jobName: 'test', jobData: {} })).toBeNull();
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('preset strategies', () => {
|
|
189
|
+
it('aggressive should have fast retries', () => {
|
|
190
|
+
const strategy = backoffStrategies.aggressive();
|
|
191
|
+
|
|
192
|
+
expect(strategy({ attempt: 1, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(100);
|
|
193
|
+
expect(strategy({ attempt: 2, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(200);
|
|
194
|
+
expect(strategy({ attempt: 3, error: new Error('test'), jobName: 'test', jobData: {} })).toBe(400);
|
|
195
|
+
expect(strategy({ attempt: 4, error: new Error('test'), jobName: 'test', jobData: {} })).toBeNull();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('standard should have balanced retries', () => {
|
|
199
|
+
const strategy = backoffStrategies.standard();
|
|
200
|
+
|
|
201
|
+
// First attempt should be around 1000 (with some jitter)
|
|
202
|
+
const delay = strategy({ attempt: 1, error: new Error('test'), jobName: 'test', jobData: {} });
|
|
203
|
+
expect(delay).toBeGreaterThanOrEqual(900);
|
|
204
|
+
expect(delay).toBeLessThanOrEqual(1100);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('auto-retry integration', () => {
|
|
209
|
+
it('should automatically retry with backoff strategy', async () => {
|
|
210
|
+
let attempts = 0;
|
|
211
|
+
const retryDelays: number[] = [];
|
|
212
|
+
|
|
213
|
+
agenda.define(
|
|
214
|
+
'auto-retry-job',
|
|
215
|
+
async () => {
|
|
216
|
+
attempts++;
|
|
217
|
+
if (attempts < 3) {
|
|
218
|
+
throw new Error(`Attempt ${attempts} failed`);
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
backoff: constant({ delay: 100, maxRetries: 5 })
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
agenda.on('retry:auto-retry-job', (_job: JobWithId, details: RetryDetails) => {
|
|
227
|
+
retryDelays.push(details.delay);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const successPromise = new Promise(resolve => {
|
|
231
|
+
agenda.on('success:auto-retry-job', resolve);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
await agenda.now('auto-retry-job');
|
|
235
|
+
await agenda.start();
|
|
236
|
+
await successPromise;
|
|
237
|
+
|
|
238
|
+
expect(attempts).toBe(3);
|
|
239
|
+
expect(retryDelays).toHaveLength(2);
|
|
240
|
+
expect(retryDelays[0]).toBe(100);
|
|
241
|
+
expect(retryDelays[1]).toBe(100);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should emit retry exhausted when max retries reached', async () => {
|
|
245
|
+
let attempts = 0;
|
|
246
|
+
|
|
247
|
+
agenda.define(
|
|
248
|
+
'exhausted-job',
|
|
249
|
+
async () => {
|
|
250
|
+
attempts++;
|
|
251
|
+
throw new Error('Always fails');
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
backoff: constant({ delay: 50, maxRetries: 2 })
|
|
255
|
+
}
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const exhaustedPromise = new Promise<{ error: Error; job: JobWithId }>(resolve => {
|
|
259
|
+
agenda.on('retry exhausted:exhausted-job', (error: Error, job: JobWithId) => {
|
|
260
|
+
resolve({ error, job });
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
await agenda.now('exhausted-job');
|
|
265
|
+
await agenda.start();
|
|
266
|
+
|
|
267
|
+
const { error, job } = await exhaustedPromise;
|
|
268
|
+
|
|
269
|
+
expect(attempts).toBe(3); // Initial + 2 retries
|
|
270
|
+
expect(error.message).toBe('Always fails');
|
|
271
|
+
expect(job.attrs.failCount).toBe(3);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should provide retry details in event', async () => {
|
|
275
|
+
let retryDetails: RetryDetails | null = null;
|
|
276
|
+
|
|
277
|
+
agenda.define(
|
|
278
|
+
'details-job',
|
|
279
|
+
async () => {
|
|
280
|
+
throw new Error('test error');
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
backoff: exponential({ delay: 100, maxRetries: 1 })
|
|
284
|
+
}
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
agenda.on('retry:details-job', (_job: JobWithId, details: RetryDetails) => {
|
|
288
|
+
retryDetails = details;
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const exhaustedPromise = new Promise(resolve => {
|
|
292
|
+
agenda.on('retry exhausted:details-job', resolve);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
await agenda.now('details-job');
|
|
296
|
+
await agenda.start();
|
|
297
|
+
await exhaustedPromise;
|
|
298
|
+
|
|
299
|
+
expect(retryDetails).not.toBeNull();
|
|
300
|
+
expect(retryDetails!.attempt).toBe(1);
|
|
301
|
+
expect(retryDetails!.delay).toBe(100);
|
|
302
|
+
expect(retryDetails!.error.message).toBe('test error');
|
|
303
|
+
expect(retryDetails!.nextRunAt).toBeInstanceOf(Date);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should not auto-retry jobs without backoff configured', { timeout: 5000 }, async () => {
|
|
307
|
+
let attempts = 0;
|
|
308
|
+
let retryCount = 0;
|
|
309
|
+
|
|
310
|
+
agenda.define('no-backoff-job', async () => {
|
|
311
|
+
attempts++;
|
|
312
|
+
throw new Error('Always fails');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
agenda.on('retry:no-backoff-job', () => {
|
|
316
|
+
retryCount++;
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const failPromise = new Promise(resolve => {
|
|
320
|
+
agenda.on('fail:no-backoff-job', resolve);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
await agenda.now('no-backoff-job');
|
|
324
|
+
await agenda.start();
|
|
325
|
+
await failPromise;
|
|
326
|
+
|
|
327
|
+
// Wait a bit to ensure no retries happen
|
|
328
|
+
await new Promise(r => setTimeout(r, 200));
|
|
329
|
+
|
|
330
|
+
expect(attempts).toBe(1);
|
|
331
|
+
expect(retryCount).toBe(0);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('should auto-retry repeating jobs when backoff is configured', { timeout: 5000 }, async () => {
|
|
335
|
+
let attempts = 0;
|
|
336
|
+
let retryCount = 0;
|
|
337
|
+
|
|
338
|
+
agenda.define(
|
|
339
|
+
'repeating-job',
|
|
340
|
+
async () => {
|
|
341
|
+
attempts++;
|
|
342
|
+
if (attempts === 1) {
|
|
343
|
+
throw new Error('First attempt fails');
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
backoff: constant({ delay: 50, maxRetries: 3 })
|
|
348
|
+
}
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
agenda.on('retry:repeating-job', () => {
|
|
352
|
+
retryCount++;
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Use every() which creates a repeating job
|
|
356
|
+
await agenda.every('1 second', 'repeating-job');
|
|
357
|
+
await agenda.start();
|
|
358
|
+
|
|
359
|
+
// Wait for job to succeed (after retry)
|
|
360
|
+
await new Promise(resolve => {
|
|
361
|
+
agenda.on('success:repeating-job', resolve);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Should have triggered auto-retry since backoff is configured
|
|
365
|
+
expect(retryCount).toBe(1);
|
|
366
|
+
expect(attempts).toBe(2); // First attempt failed, second succeeded
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('should work with custom backoff function', async () => {
|
|
370
|
+
let attempts = 0;
|
|
371
|
+
const delays: number[] = [];
|
|
372
|
+
|
|
373
|
+
agenda.define(
|
|
374
|
+
'custom-backoff-job',
|
|
375
|
+
async () => {
|
|
376
|
+
attempts++;
|
|
377
|
+
if (attempts < 4) {
|
|
378
|
+
throw new Error('Failing');
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
backoff: (ctx) => {
|
|
383
|
+
// Custom: fibonacci-like sequence
|
|
384
|
+
if (ctx.attempt > 3) return null;
|
|
385
|
+
const fibDelays = [100, 100, 200];
|
|
386
|
+
return fibDelays[ctx.attempt - 1];
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
agenda.on('retry:custom-backoff-job', (_job: JobWithId, details: RetryDetails) => {
|
|
392
|
+
delays.push(details.delay);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const successPromise = new Promise(resolve => {
|
|
396
|
+
agenda.on('success:custom-backoff-job', resolve);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
await agenda.now('custom-backoff-job');
|
|
400
|
+
await agenda.start();
|
|
401
|
+
await successPromise;
|
|
402
|
+
|
|
403
|
+
expect(attempts).toBe(4);
|
|
404
|
+
expect(delays).toEqual([100, 100, 200]);
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
}
|