timeout-manager.ts•13.1 kB
/**
* Timeout Manager - Centralized timeout and retry management using configurable values
* Replaces hardcoded timeout values throughout the codebase
*/
import { VibeTaskManagerConfig } from './config-loader.js';
import logger from '../../../logger.js';
/**
* Timeout operation types
*/
export type TimeoutOperation =
| 'taskExecution'
| 'taskDecomposition'
| 'recursiveTaskDecomposition'
| 'taskRefinement'
| 'agentCommunication'
| 'llmRequest'
| 'fileOperations'
| 'databaseOperations'
| 'networkOperations';
export type TaskComplexity = 'simple' | 'moderate' | 'complex' | 'critical';
export interface ComplexityTimeoutConfig {
simple: number; // 1.0x multiplier
moderate: number; // 1.5x multiplier
complex: number; // 2.0x multiplier
critical: number; // 3.0x multiplier
}
/**
* Timeout result interface
*/
export interface TimeoutResult<T> {
success: boolean;
data?: T;
error?: string;
timedOut: boolean;
duration: number;
retryCount: number;
}
/**
* Retry configuration for specific operation
*/
export interface RetryConfig {
maxRetries: number;
backoffMultiplier: number;
initialDelayMs: number;
maxDelayMs: number;
enableExponentialBackoff: boolean;
}
/**
* Centralized timeout and retry manager
*/
export class TimeoutManager {
private static instance: TimeoutManager | null = null;
private config: VibeTaskManagerConfig['taskManager'] | null = null;
private constructor() {}
static getInstance(): TimeoutManager {
if (!TimeoutManager.instance) {
TimeoutManager.instance = new TimeoutManager();
}
return TimeoutManager.instance;
}
/**
* Check if instance exists and is initialized (without creating instance)
*/
static isInstanceInitialized(): boolean {
return TimeoutManager.instance !== null && TimeoutManager.instance.isInitialized();
}
/**
* Initialize with configuration
*/
initialize(config: VibeTaskManagerConfig['taskManager']): void {
this.config = config;
logger.debug('TimeoutManager initialized with configuration');
}
/**
* Check if timeout manager has been initialized with configuration
*/
isInitialized(): boolean {
return this.config !== null;
}
/**
* Get timeout value for specific operation
*/
getTimeout(operation: TimeoutOperation): number {
if (!this.config) {
// Fallback values matching test expectations
const fallbacks: Record<TimeoutOperation, number> = {
taskExecution: 300000, // 5 minutes (test expectation)
taskDecomposition: 600000, // 10 minutes
recursiveTaskDecomposition: 720000, // 12 minutes
taskRefinement: 180000, // 3 minutes
agentCommunication: 30000, // 30 seconds
llmRequest: 60000, // 1 minute (test expectation)
fileOperations: 10000, // 10 seconds
databaseOperations: 15000, // 15 seconds
networkOperations: 20000 // 20 seconds
};
logger.warn({ operation }, 'Using fallback timeout value - config not initialized');
return fallbacks[operation];
}
return this.config.timeouts[operation];
}
/**
* Get timeout value adjusted for task complexity
*/
getComplexityAdjustedTimeout(
operation: TimeoutOperation,
complexity: TaskComplexity,
estimatedHours?: number
): number {
const baseTimeout = this.getTimeout(operation);
// Complexity multipliers
const complexityMultipliers: ComplexityTimeoutConfig = {
simple: 1.0,
moderate: 1.5,
complex: 2.0,
critical: 3.0
};
let adjustedTimeout = baseTimeout * complexityMultipliers[complexity];
// Additional adjustment based on estimated hours for task execution
if (operation === 'taskExecution' && estimatedHours) {
const hourMultiplier = Math.max(1.0, estimatedHours / 2); // Scale with estimated time
adjustedTimeout = Math.max(adjustedTimeout, baseTimeout * hourMultiplier);
}
// Cap maximum timeout to prevent runaway operations
const maxTimeout = operation === 'taskExecution' ? 14400000 : baseTimeout * 5; // 4 hours max for tasks
return Math.min(adjustedTimeout, maxTimeout);
}
/**
* Get retry configuration
*/
getRetryConfig(): RetryConfig {
if (!this.config) {
logger.warn('Using fallback retry config - config not initialized');
return {
maxRetries: 3, // Test expectation
backoffMultiplier: 2.0, // Test expectation
initialDelayMs: 1000,
maxDelayMs: 30000, // Test expectation
enableExponentialBackoff: true
};
}
return this.config.retryPolicy;
}
/**
* Get retry configuration adjusted for operation complexity
*/
getComplexityAdjustedRetryConfig(complexity: TaskComplexity): RetryConfig {
const baseConfig = this.getRetryConfig();
// Adjust retry parameters based on complexity
const complexityAdjustments = {
simple: { maxRetries: baseConfig.maxRetries, backoffMultiplier: baseConfig.backoffMultiplier },
moderate: { maxRetries: baseConfig.maxRetries + 1, backoffMultiplier: baseConfig.backoffMultiplier * 0.9 },
complex: { maxRetries: baseConfig.maxRetries + 2, backoffMultiplier: baseConfig.backoffMultiplier * 0.8 },
critical: { maxRetries: baseConfig.maxRetries + 3, backoffMultiplier: baseConfig.backoffMultiplier * 0.7 }
};
const adjustment = complexityAdjustments[complexity];
return {
...baseConfig,
maxRetries: Math.min(adjustment.maxRetries, 10), // Cap at 10 retries
backoffMultiplier: Math.max(adjustment.backoffMultiplier, 1.2), // Minimum 1.2x backoff
maxDelayMs: Math.min(baseConfig.maxDelayMs * 2, 120000) // Cap at 2 minutes
};
}
/**
* Execute operation with timeout and retry logic
*/
async executeWithTimeout<T>(
operation: TimeoutOperation,
operationFn: () => Promise<T>,
customTimeout?: number,
customRetryConfig?: Partial<RetryConfig>
): Promise<TimeoutResult<T>> {
const timeout = customTimeout || this.getTimeout(operation);
const retryConfig = { ...this.getRetryConfig(), ...customRetryConfig };
let retryCount = 0;
let lastError: string | undefined;
const startTime = Date.now();
while (retryCount <= retryConfig.maxRetries) {
try {
const result = await this.executeWithTimeoutOnce(operationFn, timeout);
if (result.success) {
const duration = Date.now() - startTime;
logger.debug({
operation,
duration,
retryCount,
timeout
}, 'Operation completed successfully');
return {
success: true,
data: result.data,
timedOut: false,
duration,
retryCount
};
}
lastError = result.error;
if (result.timedOut && retryCount < retryConfig.maxRetries) {
const delay = this.calculateDelay(retryCount, retryConfig);
logger.warn({
operation,
retryCount: retryCount + 1,
delay,
timeout,
error: lastError
}, 'Operation timed out, retrying');
await this.delay(delay);
}
} catch (error) {
lastError = error instanceof Error ? error.message : 'Unknown error';
if (retryCount < retryConfig.maxRetries) {
const delay = this.calculateDelay(retryCount, retryConfig);
logger.warn({
operation,
retryCount: retryCount + 1,
delay,
error: lastError
}, 'Operation failed, retrying');
await this.delay(delay);
}
}
retryCount++;
}
const duration = Date.now() - startTime;
logger.error({
operation,
retryCount,
duration,
timeout,
error: lastError
}, 'Operation failed after all retries');
return {
success: false,
error: lastError || 'Operation failed after maximum retries',
timedOut: true,
duration,
retryCount
};
}
/**
* Execute operation with timeout (single attempt)
*/
private async executeWithTimeoutOnce<T>(
operationFn: () => Promise<T>,
timeout: number
): Promise<{ success: boolean; data?: T; error?: string; timedOut: boolean }> {
return new Promise((resolve) => {
let completed = false;
// Set up timeout
const timeoutHandle = setTimeout(() => {
if (!completed) {
completed = true;
resolve({
success: false,
error: `Operation timed out after ${timeout}ms`,
timedOut: true
});
}
}, timeout);
// Execute operation
operationFn()
.then((result) => {
if (!completed) {
completed = true;
clearTimeout(timeoutHandle);
resolve({
success: true,
data: result,
timedOut: false
});
}
})
.catch((error) => {
if (!completed) {
completed = true;
clearTimeout(timeoutHandle);
resolve({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
timedOut: false
});
}
});
});
}
/**
* Calculate delay for retry with exponential backoff
*/
private calculateDelay(retryCount: number, config: RetryConfig): number {
if (!config.enableExponentialBackoff) {
return config.initialDelayMs;
}
const delay = config.initialDelayMs * Math.pow(config.backoffMultiplier, retryCount);
return Math.min(delay, config.maxDelayMs);
}
/**
* Delay utility function
*/
delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Create a timeout promise for manual timeout handling
*/
createTimeoutPromise(operation: TimeoutOperation, customTimeout?: number): Promise<never> {
const timeout = customTimeout || this.getTimeout(operation);
return new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`${operation} operation timed out after ${timeout}ms`));
}, timeout);
});
}
/**
* Race operation against timeout
*/
async raceWithTimeout<T>(
operation: TimeoutOperation,
operationPromise: Promise<T>,
customTimeout?: number
): Promise<T> {
const timeoutPromise = this.createTimeoutPromise(operation, customTimeout);
return Promise.race([operationPromise, timeoutPromise]);
}
/**
* Get timeout configuration summary
*/
getTimeoutSummary(): Record<TimeoutOperation, number> {
const operations: TimeoutOperation[] = [
'taskExecution',
'taskDecomposition',
'recursiveTaskDecomposition',
'taskRefinement',
'agentCommunication',
'llmRequest',
'fileOperations',
'databaseOperations',
'networkOperations'
];
const summary: Record<TimeoutOperation, number> = {} as Record<TimeoutOperation, number>;
for (const operation of operations) {
summary[operation] = this.getTimeout(operation);
}
return summary;
}
/**
* Validate timeout configuration
*/
validateTimeouts(): { valid: boolean; issues: string[] } {
const issues: string[] = [];
if (!this.config) {
issues.push('Timeout configuration not initialized');
return { valid: false, issues };
}
// Check for reasonable timeout values
const timeouts = this.config.timeouts;
if (timeouts.taskExecution < 10000) {
issues.push('Task execution timeout is too low (< 10 seconds)');
}
if (timeouts.taskExecution > 3600000) {
issues.push('Task execution timeout is too high (> 1 hour)');
}
if (timeouts.llmRequest < 5000) {
issues.push('LLM request timeout is too low (< 5 seconds)');
}
if (timeouts.fileOperations < 1000) {
issues.push('File operations timeout is too low (< 1 second)');
}
// Check retry configuration
const retry = this.config.retryPolicy;
if (retry.maxRetries < 0 || retry.maxRetries > 10) {
issues.push('Max retries should be between 0 and 10');
}
if (retry.backoffMultiplier < 1.0 || retry.backoffMultiplier > 5.0) {
issues.push('Backoff multiplier should be between 1.0 and 5.0');
}
if (retry.initialDelayMs < 100 || retry.initialDelayMs > 10000) {
issues.push('Initial delay should be between 100ms and 10 seconds');
}
return {
valid: issues.length === 0,
issues
};
}
}
/**
* Check if timeout manager has been created and initialized
*/
export function isTimeoutManagerInitialized(): boolean {
return TimeoutManager.isInstanceInitialized();
}
/**
* Convenience function to get timeout manager instance
*/
export function getTimeoutManager(): TimeoutManager {
return TimeoutManager.getInstance();
}