@sailboat-computer/data-storage
Version:
Shared data storage library for sailboat computer v3
357 lines (319 loc) • 11.8 kB
text/typescript
/**
* 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');
});
});