glassbox-ai
Version:
Enterprise-grade AI testing framework with reliability, observability, and comprehensive validation
297 lines (259 loc) • 9.5 kB
JavaScript
/**
* Fallback Manager for handling service failures
* Provides multiple fallback mechanisms when primary services fail
*/
export class FallbackManager {
constructor(options = {}) {
this.fallbacks = new Map();
this.primaryServices = new Map();
this.fallbackStrategies = new Map();
this.defaultStrategy = options.defaultStrategy || 'sequential';
this.maxFallbackDepth = options.maxFallbackDepth || 3;
}
/**
* Register a primary service with its fallbacks
* @param {string} serviceName - Name of the service
* @param {Function} primaryFn - Primary service function
* @param {Array<Function>} fallbacks - Array of fallback functions
* @param {object} options - Additional options
*/
registerService(serviceName, primaryFn, fallbacks = [], options = {}) {
this.primaryServices.set(serviceName, {
fn: primaryFn,
options: options
});
this.fallbacks.set(serviceName, fallbacks);
this.fallbackStrategies.set(serviceName, options.strategy || this.defaultStrategy);
}
/**
* Execute a service with fallback mechanisms
* @param {string} serviceName - Name of the service to execute
* @param {Array} args - Arguments to pass to the service
* @param {object} options - Additional options
* @returns {Promise<any>} Result from primary or fallback service
*/
async execute(serviceName, args = [], options = {}) {
const primaryService = this.primaryServices.get(serviceName);
const fallbackList = this.fallbacks.get(serviceName) || [];
const strategy = this.fallbackStrategies.get(serviceName) || this.defaultStrategy;
if (!primaryService) {
throw new Error(`Service '${serviceName}' not registered`);
}
const executionOptions = {
...primaryService.options,
...options
};
switch (strategy) {
case 'sequential':
return await this.executeSequential(serviceName, primaryService.fn, fallbackList, args, executionOptions);
case 'parallel':
return await this.executeParallel(serviceName, primaryService.fn, fallbackList, args, executionOptions);
case 'fastest':
return await this.executeFastest(serviceName, primaryService.fn, fallbackList, args, executionOptions);
default:
return await this.executeSequential(serviceName, primaryService.fn, fallbackList, args, executionOptions);
}
}
/**
* Execute services sequentially (primary first, then fallbacks)
* @param {string} serviceName - Service name
* @param {Function} primaryFn - Primary function
* @param {Array<Function>} fallbacks - Fallback functions
* @param {Array} args - Arguments
* @param {object} options - Options
* @returns {Promise<any>}
*/
async executeSequential(serviceName, primaryFn, fallbacks, args, options) {
const allServices = [primaryFn, ...fallbacks];
let lastError = null;
let serviceUsed = 'primary';
for (let i = 0; i < allServices.length; i++) {
const service = allServices[i];
const isPrimary = i === 0;
try {
const result = await service(...args);
if (!isPrimary) {
console.warn(`Fallback used for ${serviceName}: ${serviceUsed}`);
}
return {
result,
serviceUsed: isPrimary ? 'primary' : `fallback_${i}`,
fallbackUsed: !isPrimary,
error: null
};
} catch (error) {
lastError = error;
serviceUsed = isPrimary ? 'primary' : `fallback_${i}`;
console.warn(`Service ${serviceName} failed (${serviceUsed}): ${error.message}`);
// Continue to next fallback
continue;
}
}
// All services failed
throw new Error(`All services for '${serviceName}' failed. Last error: ${lastError?.message}`);
}
/**
* Execute services in parallel and return the first successful result
* @param {string} serviceName - Service name
* @param {Function} primaryFn - Primary function
* @param {Array<Function>} fallbacks - Fallback functions
* @param {Array} args - Arguments
* @param {object} options - Options
* @returns {Promise<any>}
*/
async executeParallel(serviceName, primaryFn, fallbacks, args, options) {
const allServices = [primaryFn, ...fallbacks];
const timeout = options.timeout || 30000;
const promises = allServices.map(async (service, index) => {
const isPrimary = index === 0;
const serviceName = isPrimary ? 'primary' : `fallback_${index}`;
try {
const result = await Promise.race([
service(...args),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeout)
)
]);
return {
result,
serviceUsed: serviceName,
fallbackUsed: !isPrimary,
error: null
};
} catch (error) {
return {
result: null,
serviceUsed: serviceName,
fallbackUsed: !isPrimary,
error: error
};
}
});
const results = await Promise.all(promises);
const successfulResult = results.find(r => r.error === null);
if (successfulResult) {
if (successfulResult.fallbackUsed) {
console.warn(`Fallback used for ${serviceName}: ${successfulResult.serviceUsed}`);
}
return successfulResult;
}
// All services failed
const errors = results.map(r => r.error.message).join(', ');
throw new Error(`All services for '${serviceName}' failed: ${errors}`);
}
/**
* Execute services and return the fastest successful result
* @param {string} serviceName - Service name
* @param {Function} primaryFn - Primary function
* @param {Array<Function>} fallbacks - Fallback functions
* @param {Array} args - Arguments
* @param {object} options - Options
* @returns {Promise<any>}
*/
async executeFastest(serviceName, primaryFn, fallbacks, args, options) {
const allServices = [primaryFn, ...fallbacks];
const timeout = options.timeout || 30000;
const promises = allServices.map(async (service, index) => {
const isPrimary = index === 0;
const serviceName = isPrimary ? 'primary' : `fallback_${index}`;
const startTime = Date.now();
try {
const result = await Promise.race([
service(...args),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeout)
)
]);
const duration = Date.now() - startTime;
return {
result,
serviceUsed: serviceName,
fallbackUsed: !isPrimary,
duration,
error: null
};
} catch (error) {
const duration = Date.now() - startTime;
return {
result: null,
serviceUsed: serviceName,
fallbackUsed: !isPrimary,
duration,
error: error
};
}
});
const results = await Promise.all(promises);
const successfulResults = results.filter(r => r.error === null);
if (successfulResults.length === 0) {
const errors = results.map(r => r.error.message).join(', ');
throw new Error(`All services for '${serviceName}' failed: ${errors}`);
}
// Return the fastest successful result
const fastestResult = successfulResults.reduce((fastest, current) =>
current.duration < fastest.duration ? current : fastest
);
if (fastestResult.fallbackUsed) {
console.warn(`Fastest fallback used for ${serviceName}: ${fastestResult.serviceUsed} (${fastestResult.duration}ms)`);
}
return fastestResult;
}
/**
* Add a fallback to an existing service
* @param {string} serviceName - Service name
* @param {Function} fallbackFn - Fallback function
*/
addFallback(serviceName, fallbackFn) {
const fallbacks = this.fallbacks.get(serviceName) || [];
fallbacks.push(fallbackFn);
this.fallbacks.set(serviceName, fallbacks);
}
/**
* Remove a fallback from a service
* @param {string} serviceName - Service name
* @param {number} index - Index of fallback to remove
*/
removeFallback(serviceName, index) {
const fallbacks = this.fallbacks.get(serviceName) || [];
if (index >= 0 && index < fallbacks.length) {
fallbacks.splice(index, 1);
this.fallbacks.set(serviceName, fallbacks);
}
}
/**
* Get service information
* @param {string} serviceName - Service name
* @returns {object} Service information
*/
getServiceInfo(serviceName) {
const primaryService = this.primaryServices.get(serviceName);
const fallbacks = this.fallbacks.get(serviceName) || [];
const strategy = this.fallbackStrategies.get(serviceName) || this.defaultStrategy;
return {
name: serviceName,
hasPrimary: !!primaryService,
fallbackCount: fallbacks.length,
strategy,
options: primaryService?.options || {}
};
}
/**
* Get all registered services
* @returns {Array<object>} Array of service information
*/
getAllServices() {
const services = [];
for (const [name] of this.primaryServices) {
services.push(this.getServiceInfo(name));
}
return services;
}
/**
* Clear all services and fallbacks
*/
clear() {
this.primaryServices.clear();
this.fallbacks.clear();
this.fallbackStrategies.clear();
}
}