Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ import {
stopDefaultCheckerScheduler,
} from "./services/defaultChecker.js";
import {
startWebhookRetryScheduler,
stopWebhookRetryScheduler,
} from "./services/webhookRetryScheduler.js";
startWebhookRetryProcessor,
stopWebhookRetryProcessor,
} from "./services/webhookRetryProcessor.js";
import { eventStreamService } from "./services/eventStreamService.js";
import {
startNotificationCleanupScheduler,
Expand Down Expand Up @@ -61,8 +61,8 @@ const server = app.listen(port, () => {
// Start periodic on-chain default checks (if configured)
startDefaultCheckerScheduler();

// Start webhook retry scheduler
startWebhookRetryScheduler();
// Start webhook retry processor
startWebhookRetryProcessor();

// Start scheduled score reconciliation against on-chain state
startScoreReconciliationScheduler();
Expand All @@ -87,7 +87,7 @@ const shutdown = async (signal: "SIGTERM" | "SIGINT") => {
try {
await stopIndexer();
stopDefaultCheckerScheduler();
stopWebhookRetryScheduler();
stopWebhookRetryProcessor();
stopScoreReconciliationScheduler();
stopNotificationCleanupScheduler();

Expand Down
29 changes: 29 additions & 0 deletions src/services/__tests__/webhookRetryProcessor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { jest, describe, it, expect } from "@jest/globals";
import {
WEBHOOK_RETRY_CONFIG,
getRetryDelayMs,
} from "../webhookService.js";

describe("Webhook Retry Processor", () => {
it("respects the configured backoff delays", () => {
expect(WEBHOOK_RETRY_CONFIG.RETRY_DELAYS_MS).toEqual([
5 * 60 * 1000,
15 * 60 * 1000,
45 * 60 * 1000,
]);

expect(getRetryDelayMs(1)).toBe(WEBHOOK_RETRY_CONFIG.RETRY_DELAYS_MS[0]);
expect(getRetryDelayMs(2)).toBe(WEBHOOK_RETRY_CONFIG.RETRY_DELAYS_MS[1]);
expect(getRetryDelayMs(3)).toBe(WEBHOOK_RETRY_CONFIG.RETRY_DELAYS_MS[2]);
});

it("caps the backoff delay at the last configured value", () => {
const maxDelay = WEBHOOK_RETRY_CONFIG.RETRY_DELAYS_MS[WEBHOOK_RETRY_CONFIG.RETRY_DELAYS_MS.length - 1];
expect(getRetryDelayMs(WEBHOOK_RETRY_CONFIG.MAX_RETRY_ATTEMPTS)).toBe(maxDelay);
expect(getRetryDelayMs(WEBHOOK_RETRY_CONFIG.MAX_RETRY_ATTEMPTS + 5)).toBe(maxDelay);
});

it("configures exactly 4 max attempts", () => {
expect(WEBHOOK_RETRY_CONFIG.MAX_RETRY_ATTEMPTS).toBe(4);
});
});
118 changes: 0 additions & 118 deletions src/services/webhookRetryScheduler.ts

This file was deleted.

35 changes: 16 additions & 19 deletions src/services/webhookService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,22 +270,19 @@ async function postWebhook(
}
}

// Retry configuration for webhook delivery.
// This yields retry attempts at ~5m, ~15m, and ~45m after a failed delivery,
// for a total retry window a little over one hour after the initial attempt.
const RETRY_DELAYS_MS = [
5 * 60 * 1000,
15 * 60 * 1000,
45 * 60 * 1000,
] as const;

const MAX_RETRY_ATTEMPTS = RETRY_DELAYS_MS.length + 1;
export const WEBHOOK_RETRY_CONFIG = {
RETRY_DELAYS_MS: [
5 * 60 * 1000,
15 * 60 * 1000,
45 * 60 * 1000,
] as const,
MAX_RETRY_ATTEMPTS: 4,
};

export const getRetryDelayMs = (attemptNumber: number): number => {
const delayIndex = Math.min(attemptNumber - 1, RETRY_DELAYS_MS.length - 1);
return (
RETRY_DELAYS_MS[delayIndex] ?? RETRY_DELAYS_MS[RETRY_DELAYS_MS.length - 1]!
);
const delays = WEBHOOK_RETRY_CONFIG.RETRY_DELAYS_MS;
const delayIndex = Math.min(attemptNumber - 1, delays.length - 1);
return delays[delayIndex] ?? delays[delays.length - 1]!;
};

export class WebhookService {
Expand All @@ -296,8 +293,8 @@ export class WebhookService {
try {
const now = new Date();
const result = await query(
`SELECT id, subscription_id, callback_url, secret, event_id, event_type,
payload, attempt_count
`SELECT wd.id, wd.subscription_id, ws.callback_url, ws.secret, wd.event_id, wd.event_type,
wd.payload, wd.attempt_count
FROM webhook_deliveries wd
JOIN webhook_subscriptions ws ON wd.subscription_id = ws.id
WHERE wd.delivered_at IS NULL
Expand All @@ -306,7 +303,7 @@ export class WebhookService {
AND wd.attempt_count < $2
ORDER BY wd.next_retry_at ASC
LIMIT 100`,
[now, MAX_RETRY_ATTEMPTS],
[now, WEBHOOK_RETRY_CONFIG.MAX_RETRY_ATTEMPTS],
);

if (result.rows.length === 0) {
Expand Down Expand Up @@ -397,7 +394,7 @@ export class WebhookService {
} else {
// Schedule next retry or mark as permanently failed
const nextRetryTime =
newAttemptCount < MAX_RETRY_ATTEMPTS
newAttemptCount < WEBHOOK_RETRY_CONFIG.MAX_RETRY_ATTEMPTS
? new Date(Date.now() + getRetryDelayMs(newAttemptCount))
: null;

Expand Down Expand Up @@ -446,7 +443,7 @@ export class WebhookService {
} catch (error) {
const newAttemptCount = attemptCount + 1;
const nextRetryTime =
newAttemptCount < MAX_RETRY_ATTEMPTS
newAttemptCount < WEBHOOK_RETRY_CONFIG.MAX_RETRY_ATTEMPTS
? new Date(Date.now() + getRetryDelayMs(newAttemptCount))
: null;

Expand Down