UNPKG

lightweight-browser-load-tester

Version:

A lightweight load testing tool using real browsers for streaming applications with DRM support

488 lines (379 loc) 17.2 kB
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { ErrorRecoveryManager, CircuitBreakerState } from './error-recovery'; describe('ErrorRecoveryManager', () => { let errorRecovery: ErrorRecoveryManager; let mockDate: Date; beforeEach(() => { mockDate = new Date('2023-01-01T00:00:00Z'); vi.useFakeTimers(); vi.setSystemTime(mockDate); errorRecovery = new ErrorRecoveryManager({ failureThreshold: 3, recoveryTimeout: 30000, successThreshold: 2, monitoringWindow: 300000 }); }); afterEach(() => { errorRecovery.shutdown(); vi.useRealTimers(); }); describe('failure recording', () => { it('should record failures for browser instances', () => { const instanceId = 'test-instance-1'; const error = new Error('Test error'); errorRecovery.recordFailure(instanceId, error); const stats = errorRecovery.getInstanceStats(instanceId); expect(stats).toBeDefined(); expect(stats!.failureCount).toBe(1); expect(stats!.consecutiveFailures).toBe(1); expect(stats!.instanceId).toBe(instanceId); }); it('should increment failure counts on multiple failures', () => { const instanceId = 'test-instance-1'; const error = new Error('Test error'); errorRecovery.recordFailure(instanceId, error); errorRecovery.recordFailure(instanceId, error); errorRecovery.recordFailure(instanceId, error); const stats = errorRecovery.getInstanceStats(instanceId); expect(stats!.failureCount).toBe(3); expect(stats!.consecutiveFailures).toBe(3); }); it('should emit failure-recorded event', () => { const instanceId = 'test-instance-1'; const error = new Error('Test error'); const eventSpy = vi.fn(); errorRecovery.on('failure-recorded', eventSpy); errorRecovery.recordFailure(instanceId, error); expect(eventSpy).toHaveBeenCalledWith({ instanceId, error, failureInfo: expect.objectContaining({ instanceId, failureCount: 1, consecutiveFailures: 1 }) }); }); }); describe('success recording', () => { it('should reset consecutive failures on success', () => { const instanceId = 'test-instance-1'; const error = new Error('Test error'); // Record some failures errorRecovery.recordFailure(instanceId, error); errorRecovery.recordFailure(instanceId, error); // Record success errorRecovery.recordSuccess(instanceId); const stats = errorRecovery.getInstanceStats(instanceId); expect(stats!.failureCount).toBe(2); // Total failures remain expect(stats!.consecutiveFailures).toBe(0); // Consecutive failures reset }); it('should emit success-recorded event', () => { const instanceId = 'test-instance-1'; const eventSpy = vi.fn(); errorRecovery.on('success-recorded', eventSpy); errorRecovery.recordSuccess(instanceId); expect(eventSpy).toHaveBeenCalledWith({ instanceId }); }); }); describe('circuit breaker functionality', () => { it('should open circuit breaker after threshold failures', () => { const instanceId = 'test-instance-1'; const error = new Error('Test error'); const eventSpy = vi.fn(); errorRecovery.on('circuit-breaker-opened', eventSpy); // Record failures up to threshold errorRecovery.recordFailure(instanceId, error); errorRecovery.recordFailure(instanceId, error); errorRecovery.recordFailure(instanceId, error); expect(errorRecovery.getCircuitBreakerState(instanceId)).toBe(CircuitBreakerState.OPEN); expect(eventSpy).toHaveBeenCalledWith({ instanceId, failureInfo: expect.objectContaining({ consecutiveFailures: 3 }) }); }); it('should transition to half-open after recovery timeout', () => { const instanceId = 'test-instance-1'; const error = new Error('Test error'); // Open circuit breaker errorRecovery.recordFailure(instanceId, error); errorRecovery.recordFailure(instanceId, error); errorRecovery.recordFailure(instanceId, error); expect(errorRecovery.getCircuitBreakerState(instanceId)).toBe(CircuitBreakerState.OPEN); expect(errorRecovery.canUseInstance(instanceId)).toBe(false); // Advance time past recovery timeout vi.advanceTimersByTime(31000); expect(errorRecovery.canUseInstance(instanceId)).toBe(true); expect(errorRecovery.getCircuitBreakerState(instanceId)).toBe(CircuitBreakerState.HALF_OPEN); }); it('should close circuit breaker after successful operations in half-open state', () => { const instanceId = 'test-instance-1'; const error = new Error('Test error'); const eventSpy = vi.fn(); errorRecovery.on('circuit-breaker-closed', eventSpy); // Open circuit breaker errorRecovery.recordFailure(instanceId, error); errorRecovery.recordFailure(instanceId, error); errorRecovery.recordFailure(instanceId, error); // Advance time to half-open vi.advanceTimersByTime(31000); errorRecovery.canUseInstance(instanceId); // Triggers half-open // Record successful operations errorRecovery.recordSuccess(instanceId); errorRecovery.recordSuccess(instanceId); // Need to record recovery attempts to track successes in half-open state errorRecovery.recordRestartAttempt(instanceId, true); errorRecovery.recordRestartAttempt(instanceId, true); expect(errorRecovery.getCircuitBreakerState(instanceId)).toBe(CircuitBreakerState.CLOSED); expect(eventSpy).toHaveBeenCalledWith({ instanceId }); }); }); describe('instance restart logic', () => { it('should recommend restart for instances with failures', () => { const instanceId = 'test-instance-1'; const error = new Error('Test error'); errorRecovery.recordFailure(instanceId, error); expect(errorRecovery.shouldRestartInstance(instanceId)).toBe(true); }); it('should not recommend restart for blacklisted instances', () => { const instanceId = 'test-instance-1'; const error = new Error('Test error'); // Generate enough failures to trigger blacklisting for (let i = 0; i < 7; i++) { errorRecovery.recordFailure(instanceId, error); } expect(errorRecovery.shouldRestartInstance(instanceId)).toBe(false); const stats = errorRecovery.getInstanceStats(instanceId); expect(stats!.isBlacklisted).toBe(true); }); it('should record restart attempts', () => { const instanceId = 'test-instance-1'; const _error = new Error('Test error'); const eventSpy = vi.fn(); errorRecovery.on('restart-attempted', eventSpy); errorRecovery.recordRestartAttempt(instanceId, true); const attempts = errorRecovery.getRecoveryAttempts(instanceId); expect(attempts).toHaveLength(1); expect(attempts[0].success).toBe(true); expect(attempts[0].instanceId).toBe(instanceId); expect(eventSpy).toHaveBeenCalledWith({ instanceId, success: true, error: undefined, attempt: expect.objectContaining({ success: true, attemptNumber: 1 }) }); }); it('should track total restarts in failure info', () => { const instanceId = 'test-instance-1'; const error = new Error('Test error'); // First record a failure to create the failure info errorRecovery.recordFailure(instanceId, error); errorRecovery.recordRestartAttempt(instanceId, true); errorRecovery.recordRestartAttempt(instanceId, false, new Error('Restart failed')); errorRecovery.recordRestartAttempt(instanceId, true); const stats = errorRecovery.getInstanceStats(instanceId); expect(stats!.totalRestarts).toBe(3); }); it('should blacklist instances with too many restart attempts', () => { const instanceId = 'test-instance-1'; const error = new Error('Test error'); const eventSpy = vi.fn(); errorRecovery.on('instance-blacklisted', eventSpy); // Record initial failure errorRecovery.recordFailure(instanceId, error); // Simulate many failed restart attempts for (let i = 0; i < 11; i++) { errorRecovery.recordRestartAttempt(instanceId, false, new Error('Restart failed')); } expect(errorRecovery.shouldRestartInstance(instanceId)).toBe(false); const stats = errorRecovery.getInstanceStats(instanceId); expect(stats!.isBlacklisted).toBe(true); expect(eventSpy).toHaveBeenCalledWith({ instanceId, reason: 'Maximum restart attempts exceeded', failureInfo: expect.objectContaining({ isBlacklisted: true, totalRestarts: 11 }) }); }); }); describe('blacklisting functionality', () => { it('should blacklist instances with too many consecutive failures', () => { const instanceId = 'test-instance-1'; const error = new Error('Test error'); const eventSpy = vi.fn(); errorRecovery.on('instance-blacklisted', eventSpy); // Generate enough consecutive failures to trigger blacklisting (threshold * 2) for (let i = 0; i < 6; i++) { errorRecovery.recordFailure(instanceId, error); } expect(errorRecovery.shouldRestartInstance(instanceId)).toBe(false); expect(errorRecovery.canUseInstance(instanceId)).toBe(false); const stats = errorRecovery.getInstanceStats(instanceId); expect(stats!.isBlacklisted).toBe(true); expect(eventSpy).toHaveBeenCalledWith({ instanceId, reason: 'Too many consecutive failures', failureInfo: expect.objectContaining({ isBlacklisted: true }) }); }); it('should automatically unblacklist instances after timeout', () => { const instanceId = 'test-instance-1'; const error = new Error('Test error'); const eventSpy = vi.fn(); errorRecovery.on('instance-unblacklisted', eventSpy); // Blacklist instance for (let i = 0; i < 6; i++) { errorRecovery.recordFailure(instanceId, error); } expect(errorRecovery.canUseInstance(instanceId)).toBe(false); // Advance time past blacklist timeout (3x recovery timeout) vi.advanceTimersByTime(91000); // Call canUseInstance to trigger the unblacklisting check const canUse = errorRecovery.canUseInstance(instanceId); expect(canUse).toBe(true); expect(eventSpy).toHaveBeenCalledWith({ instanceId }); const stats = errorRecovery.getInstanceStats(instanceId); expect(stats!.isBlacklisted).toBe(false); }); }); describe('statistics and monitoring', () => { it('should provide comprehensive recovery statistics', () => { const instance1 = 'test-instance-1'; const instance2 = 'test-instance-2'; const error = new Error('Test error'); // Create different failure scenarios errorRecovery.recordFailure(instance1, error); errorRecovery.recordFailure(instance1, error); errorRecovery.recordFailure(instance2, error); errorRecovery.recordFailure(instance2, error); errorRecovery.recordFailure(instance2, error); // Opens circuit breaker errorRecovery.recordRestartAttempt(instance1, true); errorRecovery.recordRestartAttempt(instance2, false, new Error('Restart failed')); const stats = errorRecovery.getRecoveryStats(); expect(stats.totalInstances).toBe(2); expect(stats.failingInstances).toBe(1); // instance2 still has consecutive failures expect(stats.openCircuitBreakers).toBe(1); expect(stats.totalFailures).toBe(5); expect(stats.totalRestarts).toBe(2); }); it('should reset instance tracking', () => { const instanceId = 'test-instance-1'; const error = new Error('Test error'); const eventSpy = vi.fn(); errorRecovery.on('instance-reset', eventSpy); // Create some tracking data errorRecovery.recordFailure(instanceId, error); errorRecovery.recordRestartAttempt(instanceId, true); expect(errorRecovery.getInstanceStats(instanceId)).toBeDefined(); expect(errorRecovery.getRecoveryAttempts(instanceId)).toHaveLength(1); // Reset instance errorRecovery.resetInstance(instanceId); expect(errorRecovery.getInstanceStats(instanceId)).toBeNull(); expect(errorRecovery.getRecoveryAttempts(instanceId)).toHaveLength(0); expect(errorRecovery.getCircuitBreakerState(instanceId)).toBe(CircuitBreakerState.CLOSED); expect(eventSpy).toHaveBeenCalledWith({ instanceId }); }); }); describe('cleanup functionality', () => { it('should clean up old tracking data', () => { const instanceId = 'test-instance-1'; const error = new Error('Test error'); const eventSpy = vi.fn(); errorRecovery.on('cleanup-completed', eventSpy); // Create tracking data errorRecovery.recordFailure(instanceId, error); errorRecovery.recordSuccess(instanceId); // Reset consecutive failures errorRecovery.recordRestartAttempt(instanceId, true); // Advance time past monitoring window vi.advanceTimersByTime(301000); // Trigger cleanup errorRecovery.cleanup(); expect(errorRecovery.getInstanceStats(instanceId)).toBeNull(); expect(errorRecovery.getRecoveryAttempts(instanceId)).toHaveLength(0); expect(eventSpy).toHaveBeenCalledWith({ remainingInstances: 0, remainingAttempts: 0 }); }); it('should not clean up blacklisted instances', () => { const instanceId = 'test-instance-1'; const error = new Error('Test error'); // Blacklist instance for (let i = 0; i < 6; i++) { errorRecovery.recordFailure(instanceId, error); } // Verify instance is blacklisted const statsBeforeCleanup = errorRecovery.getInstanceStats(instanceId); expect(statsBeforeCleanup!.isBlacklisted).toBe(true); // Advance time past monitoring window vi.advanceTimersByTime(301000); // Trigger cleanup errorRecovery.cleanup(); // Blacklisted instance should not be cleaned up const statsAfterCleanup = errorRecovery.getInstanceStats(instanceId); expect(statsAfterCleanup).toBeDefined(); expect(statsAfterCleanup!.isBlacklisted).toBe(true); }); it('should automatically run cleanup periodically', () => { const instanceId = 'test-instance-1'; const error = new Error('Test error'); // Create tracking data that will be cleaned up errorRecovery.recordFailure(instanceId, error); errorRecovery.recordSuccess(instanceId); // Advance time past monitoring window and cleanup interval vi.advanceTimersByTime(301000); vi.advanceTimersByTime(60000); // Cleanup runs every minute expect(errorRecovery.getInstanceStats(instanceId)).toBeNull(); }); }); describe('error logging', () => { it('should emit error-logged events with comprehensive context', () => { const instanceId = 'test-instance-1'; const error = new Error('Test error'); const eventSpy = vi.fn(); errorRecovery.on('error-logged', eventSpy); errorRecovery.recordFailure(instanceId, error, { customContext: 'test' }); expect(eventSpy).toHaveBeenCalledWith({ timestamp: expect.any(Date), level: 'error', message: 'Browser instance failure recorded', stack: error.stack, context: { component: 'ErrorRecoveryManager', instanceId, failureCount: 1, consecutiveFailures: 1, customContext: 'test' } }); }); }); describe('edge cases', () => { it('should handle recording success for non-existent instance', () => { const instanceId = 'non-existent-instance'; expect(() => { errorRecovery.recordSuccess(instanceId); }).not.toThrow(); expect(errorRecovery.getInstanceStats(instanceId)).toBeNull(); }); it('should handle canUseInstance for non-existent instance', () => { const instanceId = 'non-existent-instance'; expect(errorRecovery.canUseInstance(instanceId)).toBe(true); expect(errorRecovery.getCircuitBreakerState(instanceId)).toBe(CircuitBreakerState.CLOSED); }); it('should handle shouldRestartInstance for non-existent instance', () => { const instanceId = 'non-existent-instance'; expect(errorRecovery.shouldRestartInstance(instanceId)).toBe(false); }); }); });