bowling-analysis-system
Version:
A comprehensive system for analyzing bowling techniques using video processing and metrics calculation
275 lines (233 loc) • 8.69 kB
JavaScript
/**
* Standard error recovery strategies
* @module ErrorStrategies
*/
/**
* Retry a failed operation with exponential backoff
* @param {Error} error - Error to recover from
* @param {Object} context - Recovery context
* @param {Function} context.operation - Operation to retry
* @param {Number} [context.maxRetries=3] - Maximum number of retries
* @param {Number} [context.initialDelay=100] - Initial delay in ms
* @param {Number} [context.maxDelay=5000] - Maximum delay in ms
* @param {Function} [context.shouldRetry] - Function to determine if retry should be attempted
* @returns {Promise<*>} - Result of operation
*/
async function retryWithBackoff(error, context) {
const operation = context.operation;
if (typeof operation !== 'function') {
throw new Error('Retry strategy requires an operation function');
}
const maxRetries = context.maxRetries || 3;
const initialDelay = context.initialDelay || 100;
const maxDelay = context.maxDelay || 5000;
const shouldRetry = context.shouldRetry || (() => true);
const logger = context.logger || console;
let attempt = 1;
let lastError = error;
while (attempt <= maxRetries) {
try {
// Wait with exponential backoff
const delay = Math.min(initialDelay * Math.pow(2, attempt - 1), maxDelay);
await new Promise(resolve => setTimeout(resolve, delay));
// Try the operation again
return await operation();
} catch (retryError) {
lastError = retryError;
// Check if we should continue retrying
if (!shouldRetry(retryError, attempt)) {
logger.info(`Retry aborted at attempt ${attempt}: ${retryError.message}`);
break;
}
logger.info(`Retry attempt ${attempt} failed: ${retryError.message}`);
attempt++;
}
}
// If we get here, all retries failed
logger.error(`All ${maxRetries} retry attempts failed`);
throw lastError;
}
/**
* Use a fallback value when an operation fails
* @param {Error} error - Error to recover from
* @param {Object} context - Recovery context
* @param {*} context.fallbackValue - Fallback value to return
* @returns {*} - Fallback value
*/
async function useFallbackValue(error, context) {
if (!('fallbackValue' in context)) {
throw new Error('Fallback strategy requires a fallbackValue');
}
const logger = context.logger || console;
logger.info(`Using fallback value due to error: ${error.message}`);
return context.fallbackValue;
}
/**
* Circuit breaker to prevent cascading failures
* @param {Error} error - Error to recover from
* @param {Object} context - Recovery context
* @param {Object} context.circuitBreaker - Circuit breaker state object
* @param {Function} context.operation - Operation to perform
* @param {Function} [context.fallback] - Fallback function
* @returns {Promise<*>} - Result of operation or fallback
*/
async function circuitBreaker(error, context) {
const circuitState = context.circuitBreaker;
if (!circuitState) {
throw new Error('Circuit breaker strategy requires a circuit state object');
}
const operation = context.operation;
const fallback = context.fallback;
const logger = context.logger || console;
// Initialize circuit state if needed
if (!circuitState.failures) {
circuitState.failures = 0;
circuitState.lastFailure = 0;
circuitState.isOpen = false;
circuitState.threshold = context.threshold || 5;
circuitState.resetTimeout = context.resetTimeout || 30000;
}
// Check if circuit is open
if (circuitState.isOpen) {
const timeSinceLastFailure = Date.now() - circuitState.lastFailure;
// Check if it's time to try again
if (timeSinceLastFailure >= circuitState.resetTimeout) {
logger.info('Circuit breaker: Testing if service is available');
circuitState.isOpen = false;
circuitState.failures = 0;
} else {
logger.info('Circuit breaker: Circuit is open, using fallback');
if (fallback) {
return await fallback(error);
}
throw new Error('Circuit is open and no fallback provided');
}
}
// Circuit is closed or half-open, try the operation
try {
return await operation();
} catch (operationError) {
// Record failure
circuitState.failures++;
circuitState.lastFailure = Date.now();
// Check if we should open the circuit
if (circuitState.failures >= circuitState.threshold) {
logger.warn(`Circuit breaker: Opening circuit after ${circuitState.failures} failures`);
circuitState.isOpen = true;
}
// Try fallback if available
if (fallback) {
logger.info('Circuit breaker: Using fallback after failure');
return await fallback(operationError);
}
throw operationError;
}
}
/**
* Queue the operation for later retry
* @param {Error} error - Error to recover from
* @param {Object} context - Recovery context
* @param {Array} context.queue - Queue to add the operation to
* @param {Function} context.operation - Operation to queue
* @param {Object} [context.metadata={}] - Metadata to store with the operation
* @returns {Promise<Object>} - Queue status
*/
async function queueForLater(error, context) {
const queue = context.queue;
const operation = context.operation;
const metadata = context.metadata || {};
const logger = context.logger || console;
if (!Array.isArray(queue)) {
throw new Error('Queue strategy requires a queue array');
}
if (typeof operation !== 'function') {
throw new Error('Queue strategy requires an operation function');
}
// Add to retry queue
const queueItem = {
operation,
error,
timestamp: Date.now(),
metadata,
retryCount: 0
};
queue.push(queueItem);
logger.info(`Queued operation for later retry. Queue size: ${queue.length}`);
return {
queued: true,
queueLength: queue.length,
item: queueItem
};
}
/**
* Rollback a transaction when an operation fails
* @param {Error} error - Error to recover from
* @param {Object} context - Recovery context
* @param {Function} context.rollback - Rollback function
* @returns {Promise<Object>} - Rollback result
*/
async function rollbackTransaction(error, context) {
const rollback = context.rollback;
const logger = context.logger || console;
if (typeof rollback !== 'function') {
throw new Error('Rollback strategy requires a rollback function');
}
logger.info(`Rolling back transaction due to error: ${error.message}`);
try {
const result = await rollback(error);
return {
rolledBack: true,
result
};
} catch (rollbackError) {
logger.error('Failed to rollback transaction:', rollbackError);
throw new Error(`Rollback failed: ${rollbackError.message}. Original error: ${error.message}`);
}
}
/**
* Use a cached result when an operation fails
* @param {Error} error - Error to recover from
* @param {Object} context - Recovery context
* @param {Object} context.cache - Cache object
* @param {String} context.key - Cache key
* @returns {Promise<*>} - Cached result
*/
async function useCache(error, context) {
const cache = context.cache;
const key = context.key;
const logger = context.logger || console;
if (!cache) {
throw new Error('Cache strategy requires a cache object');
}
if (!key) {
throw new Error('Cache strategy requires a key');
}
if (!cache.has(key)) {
throw new Error(`No cached value available for key: ${key}`);
}
logger.info(`Using cached value for key: ${key}`);
return cache.get(key);
}
module.exports = {
retryWithBackoff,
useFallbackValue,
circuitBreaker,
queueForLater,
rollbackTransaction,
useCache,
/**
* Register all strategies with an ErrorRegistry
* @param {Object} registry - ErrorRegistry instance
*/
registerAll(registry) {
if (!registry || typeof registry.registerStrategy !== 'function') {
throw new Error('Invalid ErrorRegistry provided');
}
registry.registerStrategy('retry', retryWithBackoff);
registry.registerStrategy('fallback', useFallbackValue);
registry.registerStrategy('circuitBreaker', circuitBreaker);
registry.registerStrategy('queue', queueForLater);
registry.registerStrategy('rollback', rollbackTransaction);
registry.registerStrategy('cache', useCache);
}
};