Initial commit: Inventory Barcode System
This commit is contained in:
262
utils/retry.js
Normal file
262
utils/retry.js
Normal file
@ -0,0 +1,262 @@
|
||||
const logger = require('./logger');
|
||||
|
||||
/**
|
||||
* Retry configuration options
|
||||
*/
|
||||
const DEFAULT_RETRY_OPTIONS = {
|
||||
maxAttempts: 3,
|
||||
baseDelay: 1000, // 1 second
|
||||
maxDelay: 10000, // 10 seconds
|
||||
backoffFactor: 2,
|
||||
jitter: true,
|
||||
retryCondition: (error) => {
|
||||
// Default retry condition - retry on network errors and 5xx responses
|
||||
const retryableCodes = ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'ECONNREFUSED'];
|
||||
const retryableStatusCodes = [500, 502, 503, 504];
|
||||
|
||||
return retryableCodes.includes(error.code) ||
|
||||
retryableStatusCodes.includes(error.statusCode) ||
|
||||
(error.status && retryableStatusCodes.includes(error.status));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate delay with exponential backoff and optional jitter
|
||||
*/
|
||||
const calculateDelay = (attempt, options) => {
|
||||
const { baseDelay, maxDelay, backoffFactor, jitter } = options;
|
||||
|
||||
let delay = baseDelay * Math.pow(backoffFactor, attempt - 1);
|
||||
|
||||
// Apply maximum delay limit
|
||||
delay = Math.min(delay, maxDelay);
|
||||
|
||||
// Add jitter to prevent thundering herd
|
||||
if (jitter) {
|
||||
delay = delay * (0.5 + Math.random() * 0.5);
|
||||
}
|
||||
|
||||
return Math.floor(delay);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sleep utility function
|
||||
*/
|
||||
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
/**
|
||||
* Retry wrapper for async operations
|
||||
*/
|
||||
const withRetry = async (operation, options = {}) => {
|
||||
const config = { ...DEFAULT_RETRY_OPTIONS, ...options };
|
||||
const { maxAttempts, retryCondition } = config;
|
||||
|
||||
let lastError;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
logger.debug('Retry Attempt', {
|
||||
operation: operation.name || 'anonymous',
|
||||
attempt,
|
||||
maxAttempts
|
||||
});
|
||||
|
||||
const result = await operation();
|
||||
|
||||
if (attempt > 1) {
|
||||
logger.info('Retry Successful', {
|
||||
operation: operation.name || 'anonymous',
|
||||
attempt,
|
||||
totalAttempts: attempt
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
|
||||
logger.warn('Retry Attempt Failed', {
|
||||
operation: operation.name || 'anonymous',
|
||||
attempt,
|
||||
maxAttempts,
|
||||
error: error.message,
|
||||
errorCode: error.code,
|
||||
statusCode: error.statusCode
|
||||
});
|
||||
|
||||
// Check if we should retry
|
||||
if (attempt === maxAttempts || !retryCondition(error)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Calculate and apply delay
|
||||
const delay = calculateDelay(attempt, config);
|
||||
|
||||
logger.debug('Retry Delay', {
|
||||
operation: operation.name || 'anonymous',
|
||||
attempt,
|
||||
delay: `${delay}ms`
|
||||
});
|
||||
|
||||
await sleep(delay);
|
||||
}
|
||||
}
|
||||
|
||||
// All attempts failed
|
||||
logger.error('Retry Exhausted', {
|
||||
operation: operation.name || 'anonymous',
|
||||
totalAttempts: maxAttempts,
|
||||
finalError: lastError.message
|
||||
});
|
||||
|
||||
throw lastError;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retry wrapper for database operations
|
||||
*/
|
||||
const withDatabaseRetry = async (operation, options = {}) => {
|
||||
const dbRetryOptions = {
|
||||
maxAttempts: 3,
|
||||
baseDelay: 500,
|
||||
maxDelay: 5000,
|
||||
retryCondition: (error) => {
|
||||
// Retry on database connection errors and lock timeouts
|
||||
const retryableCodes = [
|
||||
'SQLITE_BUSY',
|
||||
'SQLITE_LOCKED',
|
||||
'SQLITE_PROTOCOL',
|
||||
'ECONNRESET',
|
||||
'ETIMEDOUT'
|
||||
];
|
||||
|
||||
return retryableCodes.includes(error.code) ||
|
||||
error.message.includes('database is locked') ||
|
||||
error.message.includes('connection') ||
|
||||
error.message.includes('timeout');
|
||||
},
|
||||
...options
|
||||
};
|
||||
|
||||
return withRetry(operation, dbRetryOptions);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retry wrapper for file operations
|
||||
*/
|
||||
const withFileRetry = async (operation, options = {}) => {
|
||||
const fileRetryOptions = {
|
||||
maxAttempts: 2,
|
||||
baseDelay: 100,
|
||||
maxDelay: 1000,
|
||||
retryCondition: (error) => {
|
||||
// Retry on temporary file system errors
|
||||
const retryableCodes = [
|
||||
'EBUSY',
|
||||
'EMFILE',
|
||||
'ENFILE',
|
||||
'ENOENT'
|
||||
];
|
||||
|
||||
return retryableCodes.includes(error.code);
|
||||
},
|
||||
...options
|
||||
};
|
||||
|
||||
return withRetry(operation, fileRetryOptions);
|
||||
};
|
||||
|
||||
/**
|
||||
* Circuit breaker pattern implementation
|
||||
*/
|
||||
class CircuitBreaker {
|
||||
constructor(options = {}) {
|
||||
this.failureThreshold = options.failureThreshold || 5;
|
||||
this.resetTimeout = options.resetTimeout || 60000; // 1 minute
|
||||
this.monitoringPeriod = options.monitoringPeriod || 10000; // 10 seconds
|
||||
|
||||
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
|
||||
this.failureCount = 0;
|
||||
this.lastFailureTime = null;
|
||||
this.successCount = 0;
|
||||
}
|
||||
|
||||
async execute(operation) {
|
||||
if (this.state === 'OPEN') {
|
||||
if (Date.now() - this.lastFailureTime >= this.resetTimeout) {
|
||||
this.state = 'HALF_OPEN';
|
||||
this.successCount = 0;
|
||||
logger.info('Circuit Breaker Half-Open', {
|
||||
operation: operation.name || 'anonymous'
|
||||
});
|
||||
} else {
|
||||
const error = new Error('Circuit breaker is OPEN');
|
||||
error.code = 'CIRCUIT_BREAKER_OPEN';
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await operation();
|
||||
|
||||
if (this.state === 'HALF_OPEN') {
|
||||
this.successCount++;
|
||||
if (this.successCount >= 3) {
|
||||
this.reset();
|
||||
logger.info('Circuit Breaker Closed', {
|
||||
operation: operation.name || 'anonymous'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.recordFailure();
|
||||
|
||||
if (this.failureCount >= this.failureThreshold) {
|
||||
this.state = 'OPEN';
|
||||
this.lastFailureTime = Date.now();
|
||||
|
||||
logger.error('Circuit Breaker Opened', {
|
||||
operation: operation.name || 'anonymous',
|
||||
failureCount: this.failureCount,
|
||||
threshold: this.failureThreshold
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.state = 'CLOSED';
|
||||
this.failureCount = 0;
|
||||
this.lastFailureTime = null;
|
||||
this.successCount = 0;
|
||||
}
|
||||
|
||||
recordFailure() {
|
||||
this.failureCount++;
|
||||
}
|
||||
|
||||
getState() {
|
||||
return {
|
||||
state: this.state,
|
||||
failureCount: this.failureCount,
|
||||
lastFailureTime: this.lastFailureTime,
|
||||
successCount: this.successCount
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
withRetry,
|
||||
withDatabaseRetry,
|
||||
withFileRetry,
|
||||
CircuitBreaker,
|
||||
calculateDelay,
|
||||
sleep,
|
||||
DEFAULT_RETRY_OPTIONS
|
||||
};
|
||||
Reference in New Issue
Block a user