Skip to content
This repository was archived by the owner on Nov 14, 2025. It is now read-only.

Commit 4da304e

Browse files
sapientpantsclaudeCopilot
authored
feat: Add automatic retry with exponential backoff and circuit breaker (#180)
* feat: Add automatic retry with exponential backoff and circuit breaker Implements intelligent retry logic for improved reliability when the DeepSource API experiences transient failures. This feature ensures high availability without user intervention during API instability, rate limit spikes, or temporary network issues. Key features: - Exponential backoff with jitter to prevent thundering herd - Per-endpoint circuit breaker pattern to prevent cascade failures - Retry budget management to limit resource consumption - Respect for Retry-After headers from the API - Automatic handling of transient failures (network, 502, 503, 504) - Rate-limited requests (429) are automatically retried Configuration via environment variables: - RETRY_MAX_ATTEMPTS (default: 3) - RETRY_BASE_DELAY_MS (default: 1000ms) - RETRY_MAX_DELAY_MS (default: 30000ms) - RETRY_BUDGET_PER_MINUTE (default: 10) - CIRCUIT_BREAKER_THRESHOLD (default: 5) - CIRCUIT_BREAKER_TIMEOUT_MS (default: 30000ms) Safety features: - Only retries idempotent operations (queries/GET requests) - Never retries mutations (update operations) - Transparent to MCP clients - no user-visible errors during transient failures Closes #153 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * fix: resolve DeepSource code quality issues - Remove unused beforeEach import from test file - Replace non-null assertions with proper error handling - Fix lexical declaration scoping in switch statements - Update test expectations to match corrected behavior - Remove unused @playwright/test dev dependency Fixes issues identified in PR #180 by DeepSource: - JS-0356: Unused variables (1 major issue) - JS-0339: Non-null assertions (2 major issues) - JS-0054: Lexical declarations in case clauses (3 minor issues) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * fix: use ESLint disable comment for non-null assertions The non-null assertions are safe because we just set the value in the Map immediately before retrieving it. Using ESLint disable comments instead of runtime checks avoids adding uncovered branches that would fail coverage thresholds. * fix: properly handle non-null assertions without suppression Refactored getBreaker and getBudget methods to avoid non-null assertions entirely by storing the created instance in a variable before setting it in the Map. This eliminates the DeepSource JS-0339 issue while maintaining the required test coverage thresholds. * fix: resolve unhandled promise rejection in retry executor test Fixed the test case 'should throw error when all retries fail' to properly handle the promise rejection, preventing unhandled rejection warnings and test failures in CI. * fix: resolve remaining deepsource code quality issues - JS-0105: Make isAxiosError method static as it doesn't use 'this' - JS-0047: Add default cases to switch statements in recordSuccess and recordFailure - JS-0045: Add explicit return statement in extractRetryAfter for consistency All DeepSource issues have been resolved. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * fix: resolve last deepsource issue js-0045 Remove async keyword from sleep function and ensure all code paths return a Promise consistently. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * Update src/utils/retry/retry-budget.ts Co-authored-by: Copilot <[email protected]> * Update src/utils/retry/circuit-breaker.ts Co-authored-by: Copilot <[email protected]> * test: improve test coverage for retry modules - Add tests for circuit breaker default case handling (99.53% coverage) - Add comprehensive tests for RetryBudget and RetryBudgetManager (100% coverage) - Test getAllStats, resetAll, and clear methods - Overall retry module coverage improved to 97.26% 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * fix: add undefined check and deepsource skip comments - Add undefined check when accessing retryTimestamps array element - Add DeepSource skip comments for intentional any usage in tests - Fix formatting in circuit-breaker.ts 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 74870dc commit 4da304e

18 files changed

+3293
-11
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
'deepsource-mcp-server': minor
3+
---
4+
5+
Add automatic retry with exponential backoff and circuit breaker
6+
7+
Implements intelligent retry logic with the following features:
8+
9+
- Exponential backoff with jitter to prevent thundering herd
10+
- Circuit breaker pattern per endpoint to prevent cascade failures
11+
- Retry budget management to limit resource consumption
12+
- Respect for Retry-After headers from the API
13+
- Automatic handling of transient failures (network, 502, 503, 504)
14+
- Rate-limited requests (429) are automatically retried with appropriate delays
15+
16+
Configuration via environment variables:
17+
18+
- `RETRY_MAX_ATTEMPTS`: Maximum retry attempts (default: 3)
19+
- `RETRY_BASE_DELAY_MS`: Base delay for exponential backoff (default: 1000ms)
20+
- `RETRY_MAX_DELAY_MS`: Maximum delay between retries (default: 30000ms)
21+
- `RETRY_BUDGET_PER_MINUTE`: Max retries per minute (default: 10)
22+
- `CIRCUIT_BREAKER_THRESHOLD`: Failures before opening circuit (default: 5)
23+
- `CIRCUIT_BREAKER_TIMEOUT_MS`: Recovery timeout (default: 30000ms)
24+
25+
Safety features:
26+
27+
- Only retries idempotent operations (queries/GET requests)
28+
- Never retries mutations (update operations)
29+
- Transparent to MCP clients - no user-visible errors during transient failures
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'deepsource-mcp-server': patch
3+
---
4+
5+
Fix all DeepSource code quality issues
6+
7+
- JS-0356: Remove unused import in exponential-backoff test file
8+
- JS-0339: Replace non-null assertions with proper null checks and error handling in circuit-breaker and retry-budget modules
9+
- JS-0054: Fix lexical declaration scoping in switch case statements by adding block scope braces
10+
- JS-0105: Make isAxiosError method static in base-client.ts since it doesn't use 'this'
11+
- JS-0047: Add default cases to switch statements in recordSuccess and recordFailure methods
12+
- JS-0045: Add explicit return statement in extractRetryAfter arrow function for consistency
13+
- Update test expectations to match corrected circuit breaker behavior
14+
- Fix unhandled promise rejection in retry-executor test

.env.example

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# DeepSource MCP Server Environment Variables
2+
3+
# Required: Your DeepSource API key for authentication
4+
DEEPSOURCE_API_KEY=your-deepsource-api-key
5+
6+
# Optional: Logging configuration
7+
LOG_FILE=/tmp/deepsource-mcp.log
8+
LOG_LEVEL=INFO
9+
10+
# Optional: Retry configuration (all have sensible defaults)
11+
# Maximum number of retry attempts for failed requests
12+
RETRY_MAX_ATTEMPTS=3
13+
14+
# Base delay in milliseconds for exponential backoff
15+
RETRY_BASE_DELAY_MS=1000
16+
17+
# Maximum delay in milliseconds between retries
18+
RETRY_MAX_DELAY_MS=30000
19+
20+
# Maximum retries allowed per minute across all operations
21+
RETRY_BUDGET_PER_MINUTE=10
22+
23+
# Optional: Circuit breaker configuration
24+
# Number of failures before circuit breaker opens
25+
CIRCUIT_BREAKER_THRESHOLD=5
26+
27+
# Time in milliseconds before circuit breaker attempts recovery
28+
CIRCUIT_BREAKER_TIMEOUT_MS=30000

README.md

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,13 @@ The simplest way to use the DeepSource MCP Server:
8989
"env": {
9090
"DEEPSOURCE_API_KEY": "your-deepsource-api-key",
9191
"LOG_FILE": "/tmp/deepsource-mcp.log",
92-
"LOG_LEVEL": "INFO"
92+
"LOG_LEVEL": "INFO",
93+
"RETRY_MAX_ATTEMPTS": "3",
94+
"RETRY_BASE_DELAY_MS": "1000",
95+
"RETRY_MAX_DELAY_MS": "30000",
96+
"RETRY_BUDGET_PER_MINUTE": "10",
97+
"CIRCUIT_BREAKER_THRESHOLD": "5",
98+
"CIRCUIT_BREAKER_TIMEOUT_MS": "30000"
9399
}
94100
}
95101
}
@@ -149,16 +155,28 @@ For development or customization:
149155

150156
### Environment Variables
151157

152-
| Variable | Required | Default | Description |
153-
| -------------------- | -------- | ------- | --------------------------------------------------- |
154-
| `DEEPSOURCE_API_KEY` | Yes | - | Your DeepSource API key for authentication |
155-
| `LOG_FILE` | No | - | Path to log file. If not set, no logs are written |
156-
| `LOG_LEVEL` | No | `DEBUG` | Minimum log level: `DEBUG`, `INFO`, `WARN`, `ERROR` |
158+
| Variable | Required | Default | Description |
159+
| ---------------------------- | -------- | ------- | ------------------------------------------------------------- |
160+
| `DEEPSOURCE_API_KEY` | Yes | - | Your DeepSource API key for authentication |
161+
| `LOG_FILE` | No | - | Path to log file. If not set, no logs are written |
162+
| `LOG_LEVEL` | No | `DEBUG` | Minimum log level: `DEBUG`, `INFO`, `WARN`, `ERROR` |
163+
| `RETRY_MAX_ATTEMPTS` | No | `3` | Maximum number of retry attempts for failed requests |
164+
| `RETRY_BASE_DELAY_MS` | No | `1000` | Base delay in milliseconds for exponential backoff |
165+
| `RETRY_MAX_DELAY_MS` | No | `30000` | Maximum delay in milliseconds between retries |
166+
| `RETRY_BUDGET_PER_MINUTE` | No | `10` | Maximum retries allowed per minute across all operations |
167+
| `CIRCUIT_BREAKER_THRESHOLD` | No | `5` | Number of failures before circuit breaker opens |
168+
| `CIRCUIT_BREAKER_TIMEOUT_MS` | No | `30000` | Time in milliseconds before circuit breaker attempts recovery |
157169

158170
### Performance Considerations
159171

160172
- **Pagination**: Use appropriate page sizes (10-50 items) to balance response time and data completeness
161-
- **Rate Limits**: DeepSource API has rate limits. The server implements automatic retry with exponential backoff
173+
- **Automatic Retry**: The server implements intelligent retry logic with:
174+
- Exponential backoff with jitter to prevent thundering herd
175+
- Circuit breaker pattern to prevent cascade failures
176+
- Retry budget to limit resource consumption
177+
- Respect for Retry-After headers from the API
178+
- **Rate Limits**: Rate-limited requests (429) are automatically retried with appropriate delays
179+
- **Fault Tolerance**: Transient failures (network, 502, 503, 504) are handled gracefully
162180
- **Caching**: Results are not cached. Consider implementing caching for frequently accessed data
163181

164182
## Available Tools

src/__tests__/error-handlers.test.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -380,12 +380,19 @@ describe('Error Handlers', () => {
380380
});
381381

382382
it('should handle 500+ errors', () => {
383-
const serverErrors = [500, 502, 503, 504, 520].map((status) => createMockAxiosError(status));
383+
const serverErrors = [
384+
{ status: 500, expectedMessage: 'Server error' },
385+
{ status: 502, expectedMessage: 'Bad Gateway' },
386+
{ status: 503, expectedMessage: 'Service Unavailable' },
387+
{ status: 504, expectedMessage: 'Gateway Timeout' },
388+
{ status: 520, expectedMessage: 'Server error' },
389+
];
384390

385-
for (const error of serverErrors) {
391+
for (const { status, expectedMessage } of serverErrors) {
392+
const error = createMockAxiosError(status);
386393
const result = handleHttpStatusError(error);
387394
expect(result).not.toBeNull();
388-
expect(result?.message).toContain('Server error');
395+
expect(result?.message).toContain(expectedMessage);
389396
expect(result?.category).toBe(ErrorCategory.SERVER);
390397
expect(result?.originalError).toBe(error);
391398
}

0 commit comments

Comments
 (0)