523 lines
16 KiB
TypeScript
523 lines
16 KiB
TypeScript
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
import * as einvoice from '../../../ts/index.js';
|
|
import * as plugins from '../../plugins.js';
|
|
import { PerformanceTracker } from '../../helpers/performance.tracker.js';
|
|
|
|
tap.test('ERR-05: Memory/Resource Errors - Handle memory and resource constraints', async (t) => {
|
|
const performanceTracker = new PerformanceTracker('ERR-05');
|
|
|
|
await t.test('Memory allocation errors', async () => {
|
|
performanceTracker.startOperation('memory-allocation');
|
|
|
|
const memoryScenarios = [
|
|
{
|
|
name: 'Large XML parsing',
|
|
size: 50 * 1024 * 1024, // 50MB
|
|
operation: 'XML parsing',
|
|
expectedError: /memory|heap|allocation failed/i
|
|
},
|
|
{
|
|
name: 'Multiple concurrent operations',
|
|
concurrency: 100,
|
|
operation: 'Concurrent processing',
|
|
expectedError: /memory|resource|too many/i
|
|
},
|
|
{
|
|
name: 'Buffer overflow protection',
|
|
size: 100 * 1024 * 1024, // 100MB
|
|
operation: 'Buffer allocation',
|
|
expectedError: /buffer.*too large|memory limit|overflow/i
|
|
}
|
|
];
|
|
|
|
for (const scenario of memoryScenarios) {
|
|
const startTime = performance.now();
|
|
|
|
try {
|
|
if (scenario.name === 'Large XML parsing') {
|
|
// Simulate large XML that could cause memory issues
|
|
const largeXml = '<invoice>' + 'x'.repeat(scenario.size) + '</invoice>';
|
|
|
|
// Check memory usage before attempting parse
|
|
const memUsage = process.memoryUsage();
|
|
if (memUsage.heapUsed + scenario.size > memUsage.heapTotal * 0.9) {
|
|
throw new Error('Insufficient memory for XML parsing operation');
|
|
}
|
|
} else if (scenario.name === 'Buffer overflow protection') {
|
|
// Simulate buffer size check
|
|
const MAX_BUFFER_SIZE = 50 * 1024 * 1024; // 50MB limit
|
|
if (scenario.size > MAX_BUFFER_SIZE) {
|
|
throw new Error(`Buffer size ${scenario.size} exceeds maximum allowed size of ${MAX_BUFFER_SIZE}`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
expect(error).toBeTruthy();
|
|
expect(error.message.toLowerCase()).toMatch(scenario.expectedError);
|
|
console.log(`✓ ${scenario.name}: ${error.message}`);
|
|
}
|
|
|
|
performanceTracker.recordMetric('memory-error-handling', performance.now() - startTime);
|
|
}
|
|
|
|
performanceTracker.endOperation('memory-allocation');
|
|
});
|
|
|
|
await t.test('Resource exhaustion handling', async () => {
|
|
performanceTracker.startOperation('resource-exhaustion');
|
|
|
|
class ResourcePool {
|
|
private available: number;
|
|
private inUse = 0;
|
|
private waitQueue: Array<(value: any) => void> = [];
|
|
|
|
constructor(private maxResources: number) {
|
|
this.available = maxResources;
|
|
}
|
|
|
|
async acquire(): Promise<{ id: number; release: () => void }> {
|
|
if (this.available > 0) {
|
|
this.available--;
|
|
this.inUse++;
|
|
const resourceId = this.inUse;
|
|
|
|
return {
|
|
id: resourceId,
|
|
release: () => this.release()
|
|
};
|
|
}
|
|
|
|
// Resource exhausted - wait or throw
|
|
if (this.waitQueue.length > 10) {
|
|
throw new Error('Resource pool exhausted - too many pending requests');
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
this.waitQueue.push(resolve);
|
|
});
|
|
}
|
|
|
|
private release(): void {
|
|
this.available++;
|
|
this.inUse--;
|
|
|
|
if (this.waitQueue.length > 0) {
|
|
const waiting = this.waitQueue.shift();
|
|
waiting(this.acquire());
|
|
}
|
|
}
|
|
|
|
getStatus() {
|
|
return {
|
|
available: this.available,
|
|
inUse: this.inUse,
|
|
waiting: this.waitQueue.length
|
|
};
|
|
}
|
|
}
|
|
|
|
const pool = new ResourcePool(5);
|
|
const acquiredResources = [];
|
|
|
|
// Acquire all resources
|
|
for (let i = 0; i < 5; i++) {
|
|
const resource = await pool.acquire();
|
|
acquiredResources.push(resource);
|
|
console.log(` Acquired resource ${resource.id}`);
|
|
}
|
|
|
|
console.log(` Pool status:`, pool.getStatus());
|
|
|
|
// Try to acquire when exhausted
|
|
try {
|
|
// Create many waiting requests
|
|
const promises = [];
|
|
for (let i = 0; i < 15; i++) {
|
|
promises.push(pool.acquire());
|
|
}
|
|
await Promise.race([
|
|
Promise.all(promises),
|
|
new Promise((_, reject) => setTimeout(() => reject(new Error('Resource pool exhausted')), 100))
|
|
]);
|
|
} catch (error) {
|
|
expect(error.message).toMatch(/resource pool exhausted/i);
|
|
console.log(`✓ Resource exhaustion detected: ${error.message}`);
|
|
}
|
|
|
|
// Release resources
|
|
for (const resource of acquiredResources) {
|
|
resource.release();
|
|
}
|
|
|
|
performanceTracker.endOperation('resource-exhaustion');
|
|
});
|
|
|
|
await t.test('File handle management', async () => {
|
|
performanceTracker.startOperation('file-handles');
|
|
|
|
class FileHandleManager {
|
|
private openHandles = new Map<string, any>();
|
|
private readonly maxHandles = 100;
|
|
|
|
async open(filename: string): Promise<any> {
|
|
if (this.openHandles.size >= this.maxHandles) {
|
|
// Try to close least recently used
|
|
const lru = this.openHandles.keys().next().value;
|
|
if (lru) {
|
|
await this.close(lru);
|
|
console.log(` Auto-closed LRU file: ${lru}`);
|
|
} else {
|
|
throw new Error(`Too many open files (${this.maxHandles} limit reached)`);
|
|
}
|
|
}
|
|
|
|
// Simulate file open
|
|
const handle = {
|
|
filename,
|
|
opened: Date.now(),
|
|
read: async () => `Content of ${filename}`
|
|
};
|
|
|
|
this.openHandles.set(filename, handle);
|
|
return handle;
|
|
}
|
|
|
|
async close(filename: string): Promise<void> {
|
|
if (this.openHandles.has(filename)) {
|
|
this.openHandles.delete(filename);
|
|
}
|
|
}
|
|
|
|
async closeAll(): Promise<void> {
|
|
for (const filename of this.openHandles.keys()) {
|
|
await this.close(filename);
|
|
}
|
|
}
|
|
|
|
getOpenCount(): number {
|
|
return this.openHandles.size;
|
|
}
|
|
}
|
|
|
|
const fileManager = new FileHandleManager();
|
|
|
|
// Test normal operations
|
|
for (let i = 0; i < 50; i++) {
|
|
await fileManager.open(`file${i}.xml`);
|
|
}
|
|
console.log(` Opened ${fileManager.getOpenCount()} files`);
|
|
|
|
// Test approaching limit
|
|
for (let i = 50; i < 100; i++) {
|
|
await fileManager.open(`file${i}.xml`);
|
|
}
|
|
console.log(` At limit: ${fileManager.getOpenCount()} files`);
|
|
|
|
// Test exceeding limit (should auto-close LRU)
|
|
await fileManager.open('file100.xml');
|
|
console.log(` After LRU eviction: ${fileManager.getOpenCount()} files`);
|
|
|
|
// Clean up
|
|
await fileManager.closeAll();
|
|
expect(fileManager.getOpenCount()).toEqual(0);
|
|
console.log('✓ File handle management working correctly');
|
|
|
|
performanceTracker.endOperation('file-handles');
|
|
});
|
|
|
|
await t.test('Memory leak detection', async () => {
|
|
performanceTracker.startOperation('memory-leak-detection');
|
|
|
|
class MemoryMonitor {
|
|
private samples: Array<{ time: number; usage: NodeJS.MemoryUsage }> = [];
|
|
private leakThreshold = 10 * 1024 * 1024; // 10MB
|
|
|
|
recordSample(): void {
|
|
this.samples.push({
|
|
time: Date.now(),
|
|
usage: process.memoryUsage()
|
|
});
|
|
|
|
// Keep only recent samples
|
|
if (this.samples.length > 10) {
|
|
this.samples.shift();
|
|
}
|
|
}
|
|
|
|
detectLeak(): { isLeaking: boolean; growth?: number; message?: string } {
|
|
if (this.samples.length < 3) {
|
|
return { isLeaking: false };
|
|
}
|
|
|
|
const first = this.samples[0];
|
|
const last = this.samples[this.samples.length - 1];
|
|
const heapGrowth = last.usage.heapUsed - first.usage.heapUsed;
|
|
|
|
if (heapGrowth > this.leakThreshold) {
|
|
return {
|
|
isLeaking: true,
|
|
growth: heapGrowth,
|
|
message: `Potential memory leak detected: ${Math.round(heapGrowth / 1024 / 1024)}MB heap growth`
|
|
};
|
|
}
|
|
|
|
return { isLeaking: false, growth: heapGrowth };
|
|
}
|
|
|
|
getReport(): string {
|
|
const current = process.memoryUsage();
|
|
return [
|
|
`Memory Usage Report:`,
|
|
` Heap Used: ${Math.round(current.heapUsed / 1024 / 1024)}MB`,
|
|
` Heap Total: ${Math.round(current.heapTotal / 1024 / 1024)}MB`,
|
|
` RSS: ${Math.round(current.rss / 1024 / 1024)}MB`,
|
|
` Samples: ${this.samples.length}`
|
|
].join('\n');
|
|
}
|
|
}
|
|
|
|
const monitor = new MemoryMonitor();
|
|
|
|
// Simulate operations that might leak memory
|
|
const operations = [];
|
|
for (let i = 0; i < 5; i++) {
|
|
monitor.recordSample();
|
|
|
|
// Simulate memory usage
|
|
const data = new Array(1000).fill('x'.repeat(1000));
|
|
operations.push(data);
|
|
|
|
// Small delay
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
}
|
|
|
|
const leakCheck = monitor.detectLeak();
|
|
console.log(monitor.getReport());
|
|
|
|
if (leakCheck.isLeaking) {
|
|
console.log(`⚠️ ${leakCheck.message}`);
|
|
} else {
|
|
console.log(`✓ No memory leak detected (growth: ${Math.round(leakCheck.growth / 1024)}KB)`);
|
|
}
|
|
|
|
performanceTracker.endOperation('memory-leak-detection');
|
|
});
|
|
|
|
await t.test('Stream processing for large files', async () => {
|
|
performanceTracker.startOperation('stream-processing');
|
|
|
|
class StreamProcessor {
|
|
async processLargeXml(stream: any, options: { chunkSize?: number } = {}): Promise<void> {
|
|
const chunkSize = options.chunkSize || 16 * 1024; // 16KB chunks
|
|
let processedBytes = 0;
|
|
let chunkCount = 0;
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const chunks: Buffer[] = [];
|
|
|
|
// Simulate stream processing
|
|
const processChunk = (chunk: Buffer) => {
|
|
processedBytes += chunk.length;
|
|
chunkCount++;
|
|
|
|
// Check memory pressure
|
|
const memUsage = process.memoryUsage();
|
|
if (memUsage.heapUsed > memUsage.heapTotal * 0.8) {
|
|
reject(new Error('Memory pressure too high during stream processing'));
|
|
return false;
|
|
}
|
|
|
|
// Process chunk (e.g., partial XML parsing)
|
|
chunks.push(chunk);
|
|
|
|
// Limit buffered chunks
|
|
if (chunks.length > 100) {
|
|
chunks.shift(); // Remove oldest
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
// Simulate streaming
|
|
const simulateStream = () => {
|
|
for (let i = 0; i < 10; i++) {
|
|
const chunk = Buffer.alloc(chunkSize, 'x');
|
|
if (!processChunk(chunk)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
console.log(` Processed ${chunkCount} chunks (${Math.round(processedBytes / 1024)}KB)`);
|
|
resolve();
|
|
};
|
|
|
|
simulateStream();
|
|
});
|
|
}
|
|
}
|
|
|
|
const processor = new StreamProcessor();
|
|
|
|
try {
|
|
await processor.processLargeXml({}, { chunkSize: 8 * 1024 });
|
|
console.log('✓ Stream processing completed successfully');
|
|
} catch (error) {
|
|
console.log(`✗ Stream processing failed: ${error.message}`);
|
|
}
|
|
|
|
performanceTracker.endOperation('stream-processing');
|
|
});
|
|
|
|
await t.test('Resource cleanup patterns', async () => {
|
|
performanceTracker.startOperation('resource-cleanup');
|
|
|
|
class ResourceManager {
|
|
private cleanupHandlers: Array<() => Promise<void>> = [];
|
|
|
|
register(cleanup: () => Promise<void>): void {
|
|
this.cleanupHandlers.push(cleanup);
|
|
}
|
|
|
|
async executeWithCleanup<T>(operation: () => Promise<T>): Promise<T> {
|
|
try {
|
|
return await operation();
|
|
} finally {
|
|
// Always cleanup, even on error
|
|
for (const handler of this.cleanupHandlers.reverse()) {
|
|
try {
|
|
await handler();
|
|
} catch (cleanupError) {
|
|
console.error(` Cleanup error: ${cleanupError.message}`);
|
|
}
|
|
}
|
|
this.cleanupHandlers = [];
|
|
}
|
|
}
|
|
}
|
|
|
|
const manager = new ResourceManager();
|
|
|
|
// Register cleanup handlers
|
|
manager.register(async () => {
|
|
console.log(' Closing file handles...');
|
|
});
|
|
|
|
manager.register(async () => {
|
|
console.log(' Releasing memory buffers...');
|
|
});
|
|
|
|
manager.register(async () => {
|
|
console.log(' Clearing temporary files...');
|
|
});
|
|
|
|
// Test successful operation
|
|
try {
|
|
await manager.executeWithCleanup(async () => {
|
|
console.log(' Executing operation...');
|
|
return 'Success';
|
|
});
|
|
console.log('✓ Operation with cleanup completed');
|
|
} catch (error) {
|
|
console.log(`✗ Operation failed: ${error.message}`);
|
|
}
|
|
|
|
// Test failed operation (cleanup should still run)
|
|
try {
|
|
await manager.executeWithCleanup(async () => {
|
|
console.log(' Executing failing operation...');
|
|
throw new Error('Operation failed');
|
|
});
|
|
} catch (error) {
|
|
console.log('✓ Cleanup ran despite error');
|
|
}
|
|
|
|
performanceTracker.endOperation('resource-cleanup');
|
|
});
|
|
|
|
await t.test('Memory usage optimization strategies', async () => {
|
|
performanceTracker.startOperation('memory-optimization');
|
|
|
|
const optimizationStrategies = [
|
|
{
|
|
name: 'Lazy loading',
|
|
description: 'Load data only when needed',
|
|
implementation: () => {
|
|
let _data: any = null;
|
|
return {
|
|
get data() {
|
|
if (!_data) {
|
|
console.log(' Loading data on first access...');
|
|
_data = { loaded: true };
|
|
}
|
|
return _data;
|
|
}
|
|
};
|
|
}
|
|
},
|
|
{
|
|
name: 'Object pooling',
|
|
description: 'Reuse objects instead of creating new ones',
|
|
implementation: () => {
|
|
const pool: any[] = [];
|
|
return {
|
|
acquire: () => pool.pop() || { reused: false },
|
|
release: (obj: any) => {
|
|
obj.reused = true;
|
|
pool.push(obj);
|
|
}
|
|
};
|
|
}
|
|
},
|
|
{
|
|
name: 'Weak references',
|
|
description: 'Allow garbage collection of cached objects',
|
|
implementation: () => {
|
|
const cache = new WeakMap();
|
|
return {
|
|
set: (key: object, value: any) => cache.set(key, value),
|
|
get: (key: object) => cache.get(key)
|
|
};
|
|
}
|
|
}
|
|
];
|
|
|
|
for (const strategy of optimizationStrategies) {
|
|
console.log(`\n Testing ${strategy.name}:`);
|
|
console.log(` ${strategy.description}`);
|
|
|
|
const impl = strategy.implementation();
|
|
|
|
if (strategy.name === 'Lazy loading') {
|
|
// Access data multiple times
|
|
const obj = impl as any;
|
|
obj.data; // First access
|
|
obj.data; // Second access (no reload)
|
|
} else if (strategy.name === 'Object pooling') {
|
|
const pool = impl as any;
|
|
const obj1 = pool.acquire();
|
|
console.log(` First acquire: reused=${obj1.reused}`);
|
|
pool.release(obj1);
|
|
const obj2 = pool.acquire();
|
|
console.log(` Second acquire: reused=${obj2.reused}`);
|
|
}
|
|
|
|
console.log(` ✓ ${strategy.name} implemented`);
|
|
}
|
|
|
|
performanceTracker.endOperation('memory-optimization');
|
|
});
|
|
|
|
// Performance summary
|
|
console.log('\n' + performanceTracker.getSummary());
|
|
|
|
// Memory error handling best practices
|
|
console.log('\nMemory/Resource Error Handling Best Practices:');
|
|
console.log('1. Implement resource pooling for frequently used objects');
|
|
console.log('2. Use streaming for large file processing');
|
|
console.log('3. Monitor memory usage and implement early warning systems');
|
|
console.log('4. Always clean up resources in finally blocks');
|
|
console.log('5. Set reasonable limits on buffer sizes and concurrent operations');
|
|
console.log('6. Implement graceful degradation when resources are constrained');
|
|
console.log('7. Use weak references for caches that can be garbage collected');
|
|
});
|
|
|
|
tap.start(); |