graceful-degradation.ts•11.1 kB
/**
* Graceful degradation service for handling API failures
*/
import { GoogleDriveMCPError, ErrorCategory } from '../types/errors.js';
import { CacheManager } from '../types/cache.js';
import { Logger } from '../logging/logger.js';
/**
* Degradation strategy options
*/
export interface DegradationStrategy {
useCachedData: boolean;
usePartialData: boolean;
useDefaultValues: boolean;
skipNonEssentialFeatures: boolean;
enableOfflineMode: boolean;
}
/**
* Degradation context for decision making
*/
export interface DegradationContext {
operation: string;
error: GoogleDriveMCPError;
hasCache: boolean;
isEssential: boolean;
userContext?: Record<string, any>;
}
/**
* Degraded response wrapper
*/
export interface DegradedResponse<T> {
data: T;
degraded: boolean;
degradationReason: string;
limitations: string[];
suggestedActions: string[];
}
/**
* Default degradation strategy
*/
export const DEFAULT_DEGRADATION_STRATEGY: DegradationStrategy = {
useCachedData: true,
usePartialData: true,
useDefaultValues: false,
skipNonEssentialFeatures: true,
enableOfflineMode: true
};
/**
* Graceful degradation service
*/
export class GracefulDegradationService {
private strategy: DegradationStrategy;
private cacheManager: CacheManager | undefined;
private logger: Logger;
constructor(
strategy: Partial<DegradationStrategy> = {},
cacheManager?: CacheManager,
logger?: Logger
) {
this.strategy = { ...DEFAULT_DEGRADATION_STRATEGY, ...strategy };
this.cacheManager = cacheManager;
this.logger = logger || new Logger();
}
/**
* Attempt graceful degradation for failed operation
*/
async handleFailure<T>(
context: DegradationContext,
fallbackOperations: Array<() => Promise<T | null>>
): Promise<DegradedResponse<T> | null> {
this.logger.warn('Attempting graceful degradation', {
operation: context.operation,
error: context.error.toJSON(),
strategy: this.strategy
});
// Try fallback operations in order
for (let i = 0; i < fallbackOperations.length; i++) {
try {
const result = await fallbackOperations[i]();
if (result !== null) {
const degradationInfo = this.createDegradationInfo(context, i);
this.logger.info('Graceful degradation successful', {
operation: context.operation,
fallbackIndex: i,
degradationInfo
});
return {
data: result,
degraded: true,
degradationReason: degradationInfo.reason,
limitations: degradationInfo.limitations,
suggestedActions: degradationInfo.suggestedActions
};
}
} catch (fallbackError) {
this.logger.warn(`Fallback operation ${i} failed`, {
operation: context.operation,
fallbackIndex: i,
error: fallbackError instanceof Error ? fallbackError.message : String(fallbackError)
});
continue;
}
}
this.logger.error('All graceful degradation attempts failed', context.error, {
operation: context.operation
});
return null;
}
/**
* Create fallback operations for file retrieval
*/
createFileRetrievalFallbacks(fileId: string): Array<() => Promise<any | null>> {
const fallbacks: Array<() => Promise<any | null>> = [];
// 1. Try cached file content
if (this.strategy.useCachedData && this.cacheManager) {
fallbacks.push(async () => {
try {
const cached = await this.cacheManager!.get(`file_content_${fileId}`);
return cached ? {
content: cached.content,
source: 'cache'
} : null;
} catch {
return null;
}
});
}
// 2. Try cached metadata only
if (this.strategy.usePartialData && this.cacheManager) {
fallbacks.push(async () => {
try {
const cached = await this.cacheManager!.get(`file_metadata_${fileId}`);
return cached ? {
metadata: cached,
content: null,
source: 'cache_metadata_only'
} : null;
} catch {
return null;
}
});
}
// 3. Return minimal default response
if (this.strategy.useDefaultValues) {
fallbacks.push(async () => ({
metadata: {
id: fileId,
name: 'Unknown File',
mimeType: 'application/octet-stream',
size: 0,
modifiedTime: new Date(),
error: 'File details unavailable due to API failure'
},
content: null,
source: 'default'
}));
}
return fallbacks;
}
/**
* Create fallback operations for search
*/
createSearchFallbacks(query: any): Array<() => Promise<any | null>> {
const fallbacks: Array<() => Promise<any | null>> = [];
// 1. Try cached search results
if (this.strategy.useCachedData && this.cacheManager) {
fallbacks.push(async () => {
try {
const cacheKey = `search_${JSON.stringify(query)}`;
const cached = await this.cacheManager!.get(cacheKey);
return cached ? {
results: cached.content,
source: 'cache',
stale: true
} : null;
} catch {
return null;
}
});
}
// 2. Return recent files from cache
if (this.strategy.usePartialData && this.cacheManager) {
fallbacks.push(async () => {
try {
const recentFiles = await this.cacheManager!.get('recent_files');
return recentFiles ? {
results: Array.isArray(recentFiles.content) ? recentFiles.content.slice(0, 10) : [], // Limit to 10 recent files
source: 'cache_recent',
partial: true
} : null;
} catch {
return null;
}
});
}
// 3. Return empty results with explanation
if (this.strategy.useDefaultValues) {
fallbacks.push(async () => ({
results: [],
source: 'default',
message: 'Search unavailable due to API failure. Please try again later.'
}));
}
return fallbacks;
}
/**
* Create fallback operations for folder listing
*/
createFolderListingFallbacks(folderId: string): Array<() => Promise<any | null>> {
const fallbacks: Array<() => Promise<any | null>> = [];
// 1. Try cached folder contents
if (this.strategy.useCachedData && this.cacheManager) {
fallbacks.push(async () => {
try {
const cached = await this.cacheManager!.get(`folder_${folderId}`);
return cached ? {
files: cached.content,
source: 'cache'
} : null;
} catch {
return null;
}
});
}
// 2. Return empty folder with explanation
if (this.strategy.useDefaultValues) {
fallbacks.push(async () => ({
files: [],
source: 'default',
message: 'Folder contents unavailable due to API failure. Please try again later.'
}));
}
return fallbacks;
}
/**
* Determine if operation should use degradation
*/
shouldDegrade(error: GoogleDriveMCPError, isEssential: boolean): boolean {
// Don't degrade for authentication errors - user needs to fix these
if (error.category === ErrorCategory.AUTHENTICATION ||
error.category === ErrorCategory.AUTHORIZATION) {
return false;
}
// Don't degrade for validation errors - these need to be fixed
if (error.category === ErrorCategory.VALIDATION) {
return false;
}
// Always try to degrade for non-essential operations
if (!isEssential) {
return true;
}
// Degrade for retryable errors that have exhausted retries
if (error.retryable) {
return true;
}
// Degrade for network and API errors
return [
ErrorCategory.NETWORK,
ErrorCategory.API_ERROR,
ErrorCategory.RATE_LIMIT,
ErrorCategory.QUOTA
].includes(error.category);
}
/**
* Create degradation information
*/
private createDegradationInfo(context: DegradationContext, fallbackIndex: number): {
reason: string;
limitations: string[];
suggestedActions: string[];
} {
const baseReason = `${context.operation} failed: ${context.error.message}`;
let reason: string;
let limitations: string[] = [];
let suggestedActions: string[] = [];
switch (fallbackIndex) {
case 0: // Cached data
reason = `${baseReason}. Using cached data.`;
limitations = [
'Data may be outdated',
'Some recent changes may not be reflected'
];
suggestedActions = [
'Try again later when the API is available',
'Check your internet connection'
];
break;
case 1: // Partial data
reason = `${baseReason}. Using partial cached data.`;
limitations = [
'Only metadata available, content may be missing',
'Data may be incomplete or outdated'
];
suggestedActions = [
'Try again later for complete data',
'Use cached content if available'
];
break;
case 2: // Default values
reason = `${baseReason}. Using default values.`;
limitations = [
'Minimal information available',
'Most features may be unavailable'
];
suggestedActions = [
'Check your internet connection',
'Verify Google Drive API status',
'Try again later'
];
break;
default:
reason = `${baseReason}. Using fallback data.`;
limitations = ['Limited functionality available'];
suggestedActions = ['Try again later'];
}
// Add error-specific suggestions
suggestedActions.push(...this.getErrorSpecificActions(context.error));
return { reason, limitations, suggestedActions };
}
/**
* Get error-specific suggested actions
*/
private getErrorSpecificActions(error: GoogleDriveMCPError): string[] {
switch (error.category) {
case ErrorCategory.NETWORK:
return ['Check your internet connection', 'Try using a different network'];
case ErrorCategory.RATE_LIMIT:
return ['Wait a few minutes before trying again', 'Reduce the frequency of requests'];
case ErrorCategory.QUOTA:
return ['Wait for quota to reset', 'Contact your administrator about quota limits'];
case ErrorCategory.API_ERROR:
return ['Check Google Drive API status', 'Verify your API credentials'];
default:
return [];
}
}
/**
* Update degradation strategy
*/
updateStrategy(newStrategy: Partial<DegradationStrategy>): void {
this.strategy = { ...this.strategy, ...newStrategy };
this.logger.info('Degradation strategy updated', { strategy: this.strategy });
}
/**
* Get current strategy
*/
getStrategy(): DegradationStrategy {
return { ...this.strategy };
}
}