Skip to content

Commit 912ad7e

Browse files
Merge pull request #1246 from lorenzo-romano/feat/issue-1129-webhook-jitter-dashboard
feat(webhooks): jittered exponential backoff and delivery log dashboard endpoint
2 parents 3e04246 + 22d53d8 commit 912ad7e

2 files changed

Lines changed: 48 additions & 2 deletions

File tree

backend/src/routes/webhooks.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
import {
88
canonicalizeWebhookPayload,
99
enqueueWebhookDeliveries,
10+
getDeliveryHistory,
1011
verifyWebhookSignature,
1112
} from '../services/webhooks/index.js';
1213
import logger from '../utils/logger.js';
@@ -55,6 +56,39 @@ router.get('/health', async (_req: Request, res: Response) => {
5556
});
5657
});
5758

59+
// GET /deliveries — delivery log inspection for the admin dashboard.
60+
// Returns recent webhook delivery attempts (in-memory rolling window),
61+
// optionally filtered by destination URL or event type.
62+
router.get('/deliveries', async (req: Request, res: Response) => {
63+
try {
64+
const { url, event, state, limit } = req.query;
65+
66+
let deliveries = getDeliveryHistory();
67+
68+
if (typeof url === 'string' && url.length > 0) {
69+
deliveries = deliveries.filter((d) => d.destinationUrl.includes(url));
70+
}
71+
if (typeof event === 'string' && event.length > 0) {
72+
deliveries = deliveries.filter((d) => d.eventType === event);
73+
}
74+
if (typeof state === 'string' && state.length > 0) {
75+
deliveries = deliveries.filter((d) => d.state === state);
76+
}
77+
78+
const parsedLimit = Number(limit);
79+
const capped = Number.isFinite(parsedLimit) && parsedLimit > 0 ? Math.min(parsedLimit, 500) : 100;
80+
81+
return res.status(200).json({
82+
status: 'success',
83+
count: deliveries.length,
84+
deliveries: deliveries.slice(0, capped),
85+
});
86+
} catch (error) {
87+
logger.error('Failed to list webhook deliveries:', error);
88+
return res.status(500).json({ error: 'Failed to list webhook deliveries' });
89+
}
90+
});
91+
5892
import {
5993
getStellarWebhookSecret,
6094
processStellarHorizonWebhook,

backend/src/services/webhooks/queue.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,23 @@ import type { WebhookDeliveryJobData } from './types.js';
55
export const WEBHOOK_DELIVERY_QUEUE_NAME = 'webhook-delivery-queue';
66
export const WEBHOOK_DEAD_LETTER_QUEUE_NAME = 'webhook-dead-letter-queue';
77

8+
/**
9+
* Jittered exponential backoff (Issue #1129): BullMQ's built-in exponential
10+
* strategy with a 50% jitter factor so concurrent retries to the same
11+
* destination don't stampede in lockstep. `delay` is the base delay in ms.
12+
*/
13+
export const WEBHOOK_BACKOFF_DELAY_MS = Number(process.env.WEBHOOK_BACKOFF_DELAY_MS || '1000');
14+
15+
export const WEBHOOK_MAX_ATTEMPTS = Number(process.env.WEBHOOK_MAX_ATTEMPTS || '5');
16+
17+
export const WEBHOOK_BACKOFF_JITTER = 0.5;
18+
819
const createDefaultJobOptions = () => ({
9-
attempts: Number(process.env.WEBHOOK_MAX_ATTEMPTS || '5'),
20+
attempts: WEBHOOK_MAX_ATTEMPTS,
1021
backoff: {
1122
type: 'exponential' as const,
12-
delay: Number(process.env.WEBHOOK_BACKOFF_DELAY_MS || '1000'),
23+
delay: WEBHOOK_BACKOFF_DELAY_MS,
24+
jitter: WEBHOOK_BACKOFF_JITTER,
1325
},
1426
removeOnComplete: {
1527
age: 24 * 60 * 60,

0 commit comments

Comments
 (0)