diff --git a/backend/src/services/payment-monitor.service.ts b/backend/src/services/payment-monitor.service.ts index 999e081..a31e854 100644 --- a/backend/src/services/payment-monitor.service.ts +++ b/backend/src/services/payment-monitor.service.ts @@ -3,10 +3,12 @@ import invoiceService from './invoice.service'; import { SELLER_PUBLIC_KEY } from '../config/stellar'; import { pool } from '../config/database'; import { checkInvoiceIsPayable } from './payment-verification'; +import { monitorBackoffMs } from '../utils/monitor-retry-backoff'; class PaymentMonitorService { private closeHandler: (() => void) | null = null; private isRunning: boolean = false; + private failureCount: number = 0; /** * Start monitoring payments for the seller account @@ -31,6 +33,7 @@ class PaymentMonitorService { this.startExpirationCheck(); console.log('✅ Payment monitor started successfully'); + this.failureCount = 0; } /** @@ -167,15 +170,17 @@ class PaymentMonitorService { */ private handleError(error: Error) { console.error('❌ Payment stream error:', error); + this.failureCount += 1; + const delayMs = monitorBackoffMs(this.failureCount); - // Attempt to restart after delay + // Attempt to restart after capped exponential backoff setTimeout(() => { if (this.isRunning) { console.log('🔄 Attempting to restart payment stream...'); this.stop(); this.start(); } - }, 5000); + }, delayMs); } /** diff --git a/backend/src/utils/monitor-retry-backoff.ts b/backend/src/utils/monitor-retry-backoff.ts new file mode 100644 index 0000000..0bc5397 --- /dev/null +++ b/backend/src/utils/monitor-retry-backoff.ts @@ -0,0 +1,17 @@ +/** + * Capped exponential backoff for the payment monitor retry loop. + * + * Returns the number of milliseconds to wait before the next reconnect attempt, + * starting at 1 second and doubling each failure up to a 30-second cap. + */ +export function monitorBackoffMs(failureCount: number): number { + if (!Number.isFinite(failureCount) || failureCount < 0) { + return 1000; + } + + const baseMs = 1000; + const capMs = 30000; + const delay = baseMs * 2 ** failureCount; + + return Math.min(delay, capMs); +} diff --git a/backend/tests/fixtures/monitor-retry-backoff.fixture.ts b/backend/tests/fixtures/monitor-retry-backoff.fixture.ts new file mode 100644 index 0000000..988a48b --- /dev/null +++ b/backend/tests/fixtures/monitor-retry-backoff.fixture.ts @@ -0,0 +1,19 @@ +/** + * Fixture data for monitor retry backoff tests. + */ + +export const BACKOFF_CASES = [ + { failureCount: 0, expected: 1000 }, + { failureCount: 1, expected: 2000 }, + { failureCount: 2, expected: 4000 }, + { failureCount: 3, expected: 8000 }, + { failureCount: 4, expected: 16000 }, + { failureCount: 5, expected: 30000 }, + { failureCount: 10, expected: 30000 }, +]; + +export const INVALID_COUNTS = [ + { failureCount: -1, expected: 1000 }, + { failureCount: Number.NaN, expected: 1000 }, + { failureCount: Number.POSITIVE_INFINITY, expected: 1000 }, +]; diff --git a/backend/tests/monitor-retry-backoff.test.ts b/backend/tests/monitor-retry-backoff.test.ts new file mode 100644 index 0000000..d4cecf8 --- /dev/null +++ b/backend/tests/monitor-retry-backoff.test.ts @@ -0,0 +1,19 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { monitorBackoffMs } from '../src/utils/monitor-retry-backoff'; +import { + BACKOFF_CASES, + INVALID_COUNTS, +} from './fixtures/monitor-retry-backoff.fixture'; + +for (const { failureCount, expected } of BACKOFF_CASES) { + test(`monitorBackoffMs(${failureCount}) returns ${expected}ms`, () => { + assert.equal(monitorBackoffMs(failureCount), expected); + }); +} + +for (const { failureCount, expected } of INVALID_COUNTS) { + test(`monitorBackoffMs(${String(failureCount)}) falls back to ${expected}ms`, () => { + assert.equal(monitorBackoffMs(failureCount), expected); + }); +}