tdpw
Version:
CLI tool for uploading Playwright test reports to TestDino platform with Azure storage support
180 lines • 6.57 kB
JavaScript
;
/**
* Retry utility with exponential backoff and circuit breaker
* Handles network failures and temporary service unavailability
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.CircuitBreaker = void 0;
exports.withRetry = withRetry;
exports.withFileUploadRetry = withFileUploadRetry;
exports.createApiCircuitBreaker = createApiCircuitBreaker;
const types_1 = require("../types");
/**
* Default retry configuration
*/
const DEFAULT_OPTIONS = {
maxAttempts: 3,
baseDelay: 1000,
maxDelay: 30000,
factor: 2,
timeout: 120000, // 2 minutes as specified
shouldRetry: (error) => {
// Retry on network errors, timeouts, and 5xx server errors
if (error instanceof types_1.NetworkError)
return true;
if (error.message.includes('timeout'))
return true;
if (error.message.includes('ECONNRESET'))
return true;
if (error.message.includes('ENOTFOUND'))
return true;
if (error.message.includes('fetch failed'))
return true;
// Check for HTTP status codes that warrant retry
if (error.message.match(/50[0-9]/))
return true; // 500-509
if (error.message.includes('429'))
return true; // Rate limiting
return false;
},
};
/**
* Execute an operation with retry logic and exponential backoff
*/
async function withRetry(operation, options = {}) {
const config = { ...DEFAULT_OPTIONS, ...options };
let lastError;
for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
try {
// Wrap operation with timeout
const result = await Promise.race([
operation(),
new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`Operation timeout after ${config.timeout}ms`));
}, config.timeout);
}),
]);
return result;
}
catch (error) {
lastError = error;
// Check if we should retry this error
if (!config.shouldRetry(lastError)) {
throw lastError;
}
// If this is the last attempt, throw the error
if (attempt === config.maxAttempts) {
throw new types_1.NetworkError(`Operation failed after ${config.maxAttempts} attempts: ${lastError.message}`, lastError);
}
// Calculate delay with exponential backoff
const delay = Math.min(config.baseDelay * config.factor ** (attempt - 1), config.maxDelay);
console.warn(`⚠️ Attempt ${attempt}/${config.maxAttempts} failed: ${lastError.message}`);
console.warn(` Retrying in ${delay}ms...`);
// Wait before next attempt
await new Promise(resolve => setTimeout(resolve, delay));
}
}
// This should never be reached, but TypeScript requires it
throw lastError;
}
/**
* Specialized retry for file upload operations
* Uses different retry logic optimized for large file uploads
*/
async function withFileUploadRetry(operation, _fileName) {
return withRetry(operation, {
maxAttempts: 3,
baseDelay: 2000, // Longer initial delay for file operations
maxDelay: 60000, // Allow longer delays for large files
timeout: 300000, // 5 minutes for file uploads
shouldRetry: (error) => {
// More lenient retry for file uploads
const message = error.message.toLowerCase();
// Retry on common file upload failures
if (message.includes('network'))
return true;
if (message.includes('timeout'))
return true;
if (message.includes('connection'))
return true;
if (message.includes('upload'))
return true;
if (message.includes('blob'))
return true;
if (message.includes('storage'))
return true;
// Don't retry on authentication or permission errors
if (message.includes('unauthorized') || message.includes('403'))
return false;
if (message.includes('forbidden'))
return false;
return DEFAULT_OPTIONS.shouldRetry(error);
},
});
}
/**
* Circuit breaker pattern for external service calls
* Prevents cascading failures when external services are down
*/
class CircuitBreaker {
operation;
options;
failureCount = 0;
lastFailureTime = 0;
isOpen = false;
constructor(operation, options) {
this.operation = operation;
this.options = options;
}
async execute() {
// Check if circuit should be reset
if (this.isOpen && Date.now() - this.lastFailureTime > this.options.resetTimeout) {
this.isOpen = false;
this.failureCount = 0;
this.options.monitor?.('half-open');
}
// Fail fast if circuit is open
if (this.isOpen) {
throw new Error('Circuit breaker is OPEN - service temporarily unavailable');
}
try {
const result = await this.operation();
// Reset on success
if (this.failureCount > 0) {
this.failureCount = 0;
this.options.monitor?.('closed');
}
return result;
}
catch (error) {
this.failureCount++;
this.lastFailureTime = Date.now();
// Open circuit if threshold reached
if (this.failureCount >= this.options.failureThreshold) {
this.isOpen = true;
this.options.monitor?.('open');
}
throw error;
}
}
}
exports.CircuitBreaker = CircuitBreaker;
/**
* Helper to create a circuit breaker for API calls
*/
function createApiCircuitBreaker(operation, serviceName) {
return new CircuitBreaker(operation, {
failureThreshold: 5, // Open after 5 failures
resetTimeout: 60000, // Try again after 1 minute
monitor: (state) => {
if (state === 'open') {
console.warn(`🚨 Circuit breaker OPEN for ${serviceName} - service unavailable`);
}
else if (state === 'closed') {
console.log(`✅ Circuit breaker CLOSED for ${serviceName} - service recovered`);
}
},
});
}
//# sourceMappingURL=retry.js.map