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
9 changes: 7 additions & 2 deletions backend/src/services/payment-monitor.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -31,6 +33,7 @@ class PaymentMonitorService {
this.startExpirationCheck();

console.log('✅ Payment monitor started successfully');
this.failureCount = 0;
}

/**
Expand Down Expand Up @@ -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);
}

/**
Expand Down
17 changes: 17 additions & 0 deletions backend/src/utils/monitor-retry-backoff.ts
Original file line number Diff line number Diff line change
@@ -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);
}
19 changes: 19 additions & 0 deletions backend/tests/fixtures/monitor-retry-backoff.fixture.ts
Original file line number Diff line number Diff line change
@@ -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 },
];
19 changes: 19 additions & 0 deletions backend/tests/monitor-retry-backoff.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
}