UNPKG

@sailboat-computer/data-storage

Version:

Shared data storage library for sailboat computer v3

357 lines (319 loc) 11.8 kB
/** * Timeout Handling Tests * * Tests the behavior of the timeout handling in the UnifiedStorageManager. */ import * as path from 'path'; import * as os from 'os'; import * as fs from 'fs'; import { StorageTier, StorageConfig } from '../../src/types'; import { createUnifiedStorageManager } from '../../src/storage'; import { withTimeout } from '../../src/utils/errors'; describe('Timeout Handling Tests', () => { // Create a temporary directory for local storage const tempDir = path.join(os.tmpdir(), 'data-storage-tests', `test-${Date.now()}`); // Storage configuration let config: StorageConfig; beforeAll(() => { // Create temp directory fs.mkdirSync(tempDir, { recursive: true }); // Create storage configuration config = { providers: { hot: { type: 'redis' as any, config: { host: 'localhost', port: 6379 } }, batching: { timeBased: { enabled: true, intervalMs: 1000 }, sizeBased: { enabled: true, maxBatchSize: 10 }, priorityOverride: true } } }; }); afterAll(() => { // Clean up temp directory fs.rmSync(tempDir, { recursive: true, force: true }); }); it('should timeout when operation takes too long', async () => { // Arrange const storageManager = createUnifiedStorageManager(config, { localStorage: { directory: tempDir, maxSizeBytes: 10 * 1024 * 1024, // 10MB encrypt: false, compressionLevel: 0 }, resilience: { timeout: { defaultTimeoutMs: 100, // Short timeout for testing operationTimeouts: { store: 50, // Even shorter timeout for store operations retrieve: 200 } } } }); // Mock the provider factory with slow operations const mockRemoteStore = jest.fn().mockImplementation(() => { return new Promise(resolve => { // Delay longer than the timeout setTimeout(() => { resolve('remote-id'); }, 200); }); }); (storageManager as any).remoteStorageManager = { initialize: jest.fn().mockResolvedValue(undefined), store: mockRemoteStore, retrieve: jest.fn().mockResolvedValue([]), update: jest.fn().mockResolvedValue(undefined), delete: jest.fn().mockResolvedValue(undefined), storeBatch: jest.fn().mockResolvedValue([]), retrieveBatch: jest.fn().mockResolvedValue([]), cleanup: jest.fn().mockResolvedValue({ itemsRemoved: 0, bytesFreed: 0, duration: 0 }), migrate: jest.fn().mockResolvedValue(undefined), getStatus: jest.fn().mockResolvedValue({ connected: true, healthy: true, queryPerformance: { hot: { averageQueryTime: 50, queriesPerSecond: 10 }, warm: { averageQueryTime: 100, queriesPerSecond: 5 }, cold: { averageQueryTime: 200, queriesPerSecond: 2 }, overall: 'healthy', providers: { hot: 'healthy', warm: 'healthy', cold: 'healthy' } } }), close: jest.fn().mockResolvedValue(undefined) }; // Create a spy on the timeout function const timeoutSpy = jest.spyOn(global, 'setTimeout'); // Act await storageManager.initialize(); // Store attempt (should timeout and fall back to local storage) const id = await storageManager.store({ value: 1 }, 'test'); // Assert expect(mockRemoteStore).toHaveBeenCalledTimes(1); expect(id).toBeDefined(); expect(id).not.toBe('remote-id'); // Get metrics to verify timeout count const metrics = storageManager.getResilienceMetrics(); expect(Object.values(metrics.timeout.timeoutCount).reduce((sum, count) => sum + count, 0)).toBeGreaterThan(0); // Clean up timeoutSpy.mockRestore(); await storageManager.close(); }); it('should use different timeouts for different operations', async () => { // Arrange const storageManager = createUnifiedStorageManager(config, { localStorage: { directory: tempDir, maxSizeBytes: 10 * 1024 * 1024, // 10MB encrypt: false, compressionLevel: 0 }, resilience: { timeout: { defaultTimeoutMs: 100, operationTimeouts: { store: 50, // Short timeout for store retrieve: 200 // Longer timeout for retrieve } } } }); // Mock the provider factory with slow operations const mockRemoteStore = jest.fn().mockImplementation(() => { return new Promise(resolve => { // Delay longer than the store timeout but shorter than retrieve timeout setTimeout(() => { resolve('remote-id'); }, 150); }); }); const mockRemoteRetrieve = jest.fn().mockImplementation(() => { return new Promise(resolve => { // Delay longer than the store timeout but shorter than retrieve timeout setTimeout(() => { resolve([{ data: { value: 'test' }, metadata: { id: 'remote-id', category: 'test' } }]); }, 150); }); }); (storageManager as any).remoteStorageManager = { initialize: jest.fn().mockResolvedValue(undefined), store: mockRemoteStore, retrieve: mockRemoteRetrieve, update: jest.fn().mockResolvedValue(undefined), delete: jest.fn().mockResolvedValue(undefined), storeBatch: jest.fn().mockResolvedValue([]), retrieveBatch: jest.fn().mockResolvedValue([]), cleanup: jest.fn().mockResolvedValue({ itemsRemoved: 0, bytesFreed: 0, duration: 0 }), migrate: jest.fn().mockResolvedValue(undefined), getStatus: jest.fn().mockResolvedValue({ connected: true, healthy: true, queryPerformance: { hot: { averageQueryTime: 50, queriesPerSecond: 10 }, warm: { averageQueryTime: 100, queriesPerSecond: 5 }, cold: { averageQueryTime: 200, queriesPerSecond: 2 }, overall: 'healthy', providers: { hot: 'healthy', warm: 'healthy', cold: 'healthy' } } }), close: jest.fn().mockResolvedValue(undefined) }; // Act await storageManager.initialize(); // Store attempt (should timeout because 150ms > 50ms timeout) const id = await storageManager.store({ value: 1 }, 'test'); // Retrieve attempt (should succeed because 150ms < 200ms timeout) const results = await storageManager.retrieve({ category: 'test' }); // Assert expect(mockRemoteStore).toHaveBeenCalledTimes(1); expect(mockRemoteRetrieve).toHaveBeenCalledTimes(1); expect(id).toBeDefined(); expect(id).not.toBe('remote-id'); // Store should have timed out expect(results.length).toBe(1); // Retrieve should have succeeded expect(results[0].data.value).toBe('test'); // Get metrics to verify timeout count const metrics = storageManager.getResilienceMetrics(); expect(Object.values(metrics.timeout.timeoutCount).reduce((sum, count) => sum + count, 0)).toBeGreaterThan(0); // Clean up await storageManager.close(); }); it('should fall back to local storage when remote times out', async () => { // Arrange const storageManager = createUnifiedStorageManager(config, { localStorage: { directory: tempDir, maxSizeBytes: 10 * 1024 * 1024, // 10MB encrypt: false, compressionLevel: 0 }, resilience: { timeout: { defaultTimeoutMs: 100, operationTimeouts: { store: 50 } } } }); // Mock the provider factory with slow operations const mockRemoteStore = jest.fn().mockImplementation(() => { return new Promise(resolve => { // Delay longer than the timeout setTimeout(() => { resolve('remote-id'); }, 200); }); }); (storageManager as any).remoteStorageManager = { initialize: jest.fn().mockResolvedValue(undefined), store: mockRemoteStore, retrieve: jest.fn().mockResolvedValue([]), update: jest.fn().mockResolvedValue(undefined), delete: jest.fn().mockResolvedValue(undefined), storeBatch: jest.fn().mockResolvedValue([]), retrieveBatch: jest.fn().mockResolvedValue([]), cleanup: jest.fn().mockResolvedValue({ itemsRemoved: 0, bytesFreed: 0, duration: 0 }), migrate: jest.fn().mockResolvedValue(undefined), getStatus: jest.fn().mockResolvedValue({ connected: true, healthy: true, queryPerformance: { hot: { averageQueryTime: 50, queriesPerSecond: 10 }, warm: { averageQueryTime: 100, queriesPerSecond: 5 }, cold: { averageQueryTime: 200, queriesPerSecond: 2 }, overall: 'healthy', providers: { hot: 'healthy', warm: 'healthy', cold: 'healthy' } } }), close: jest.fn().mockResolvedValue(undefined) }; // Create a spy on the local storage provider const localStoreSpy = jest.fn().mockResolvedValue('local-id'); (storageManager as any).localStorageProvider = { initialize: jest.fn().mockResolvedValue(undefined), store: localStoreSpy, retrieve: jest.fn().mockResolvedValue([]), update: jest.fn().mockResolvedValue(undefined), delete: jest.fn().mockResolvedValue(undefined), storeBatch: jest.fn().mockResolvedValue([]), cleanup: jest.fn().mockResolvedValue({ itemsRemoved: 0, bytesFreed: 0, duration: 0 }), getStatus: jest.fn().mockResolvedValue({ connected: true, healthy: true, queryPerformance: { hot: { averageQueryTime: 10, queriesPerSecond: 100 }, warm: { averageQueryTime: 10, queriesPerSecond: 100 }, cold: { averageQueryTime: 10, queriesPerSecond: 100 }, overall: 'healthy', providers: { hot: 'healthy', warm: 'healthy', cold: 'healthy' } } }), close: jest.fn().mockResolvedValue(undefined), getLocalStorageStatus: jest.fn().mockResolvedValue({ available: true, healthy: true, totalSizeBytes: 1024 * 1024 * 1024, usedSizeBytes: 1024 * 1024, freeSizeBytes: 1023 * 1024 * 1024, itemCount: 10 }) }; // Act await storageManager.initialize(); // Store attempt (should timeout and fall back to local storage) const id = await storageManager.store({ value: 1 }, 'test'); // Assert expect(mockRemoteStore).toHaveBeenCalledTimes(1); expect(localStoreSpy).toHaveBeenCalledTimes(1); expect(id).toBe('local-id'); // Verify pending sync contains the stored item expect((storageManager as any).pendingSync.size).toBe(1); expect((storageManager as any).pendingSync.has(id)).toBe(true); // Clean up await storageManager.close(); }); it('should directly use withTimeout utility function', async () => { // Test the withTimeout utility function directly const fastPromise = Promise.resolve('fast result'); const slowPromise = new Promise(resolve => { setTimeout(() => { resolve('slow result'); }, 200); }); // Fast promise should resolve normally const fastResult = await withTimeout(fastPromise, 100); expect(fastResult).toBe('fast result'); // Slow promise should timeout await expect(withTimeout(slowPromise, 100)).rejects.toThrow('Operation timed out'); }); });