diff --git a/listener/src/index.ts b/listener/src/index.ts index 1c71017..38f13d9 100644 --- a/listener/src/index.ts +++ b/listener/src/index.ts @@ -41,6 +41,15 @@ async function main() { if (config.analytics?.enabled) { initNotificationAnalyticsAggregator(config.analytics); } + const retryCount = process.env.DISCORD_RETRY_COUNT + ? parseInt(process.env.DISCORD_RETRY_COUNT, 10) + : undefined; + const backoffBaseSeconds = process.env.DISCORD_BACKOFF_BASE_SECONDS + ? parseFloat(process.env.DISCORD_BACKOFF_BASE_SECONDS) + : undefined; + + return { webhookUrl, webhookId, retryCount, backoffBaseSeconds }; +} try { logger.info('Initializing database'); diff --git a/listener/src/services/discord-notification.ts b/listener/src/services/discord-notification.ts index 9fbae5f..6e9593d 100644 --- a/listener/src/services/discord-notification.ts +++ b/listener/src/services/discord-notification.ts @@ -75,13 +75,30 @@ export class DiscordNotificationService { logger.info('Sending Discord notification', logContext); const message = this.formatEventMessage(event, contractConfig); - const startTime = Date.now(); + const maxRetries = this.config.retryCount ?? 5; + const backoffBaseSeconds = this.config.backoffBaseSeconds ?? 1; - try { - const response = await this.sendWebhook(message); - const durationMs = Date.now() - startTime; + let attempt = 0; + while (attempt <= maxRetries) { + const attemptStart = Date.now(); + try { + const response = await this.sendWebhook(message); + const durationMs = Date.now() - attemptStart; + + if (response.ok) { + this.deduplicator.markSent(fingerprint); + logger.info('Discord notification sent successfully', { + eventId: event.id, + contractAddress: contractConfig.address, + }); + logger.info('Discord notification delivered', { + ...logContext, + durationMs, + attempt, + }); + return true; + } - if (!response.ok) { const errorText = await response.text(); this.analytics?.record({ notificationType: NotificationType.DISCORD, @@ -97,10 +114,24 @@ export class DiscordNotificationService { statusText: response.statusText, error: errorText, durationMs, + attempt, + }); + } catch (error) { + const durationMs = Date.now() - attemptStart; + logger.error('Error sending Discord notification', { + ...logContext, + error, + durationMs, + attempt, }); - return false; } + // If we've exhausted retries, break and return false + if (attempt >= maxRetries) break; + + // Exponential backoff: base * 2^attempt (seconds) + const delayMs = Math.pow(2, attempt) * backoffBaseSeconds * 1000; + logger.warn('Retrying Discord webhook', { this.deduplicator.markSent(fingerprint); this.analytics?.record({ notificationType: NotificationType.DISCORD, @@ -127,11 +158,19 @@ export class DiscordNotificationService { }); logger.error('Error sending Discord notification', { ...logContext, - error, - durationMs: Date.now() - startTime, + attempt: attempt + 1, + nextDelayMs: delayMs, + maxRetries, }); - return false; + await this.delay(delayMs); + attempt++; } + + logger.error('Exceeded max Discord retry attempts', { + ...logContext, + maxRetries, + }); + return false; } getMetrics() { @@ -186,6 +225,10 @@ export class DiscordNotificationService { } } + private delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + private async sendWebhook(message: DiscordMessage): Promise { try { const response = await sendWebhook(this.config.webhookUrl, message, { diff --git a/listener/src/types/index.ts b/listener/src/types/index.ts index 824a6cb..ffc392a 100644 --- a/listener/src/types/index.ts +++ b/listener/src/types/index.ts @@ -8,6 +8,8 @@ export interface ContractConfig { export interface DiscordConfig { webhookUrl: string; webhookId: string; + retryCount?: number; + backoffBaseSeconds?: number; deduplicationWindowMs?: number; deduplicationMaxSize?: number; timeoutMs?: number;