MCP GitHub Issue Server

/** * WAL (Write-Ahead Logging) management */ import { Database } from 'sqlite'; import { Logger } from '../../../logging/index.js'; import { ErrorCodes, createError } from '../../../errors/index.js'; import { EventManager } from '../../../events/event-manager.js'; import { EventTypes } from '../../../types/events.js'; import { isTransientError } from '../../../utils/error-utils.js'; import { WALConfig, WALMetrics, WALState, DEFAULT_WAL_CONFIG } from './types.js'; import { CheckpointManager } from './checkpoint-manager.js'; import { MetricsCollector } from './metrics-collector.js'; import { FileHandler } from './file-handler.js'; import { BackupManager } from '../backup/backup-manager.js'; export class WALManager { private static instance: WALManager; private readonly logger: Logger; private readonly eventManager: EventManager; private readonly checkpointManager: CheckpointManager; private readonly metricsCollector: MetricsCollector; private readonly fileHandler: FileHandler; private readonly backupManager: BackupManager; private readonly config: Required<WALConfig>; private initializationPromise: Promise<void> | null = null; private state: WALState = { isEnabled: false, lastCheckpoint: 0, checkpointCount: 0, totalCheckpointTime: 0, maxWalSizeReached: 0, }; private constructor(dbPath: string, config?: Partial<WALConfig>) { this.config = { ...DEFAULT_WAL_CONFIG, dbPath, ...config, }; this.logger = Logger.getInstance().child({ component: 'WALManager', context: { dbPath: this.config.dbPath, maxWalSize: this.config.maxWalSize, checkpointInterval: this.config.checkpointInterval, }, }); this.eventManager = EventManager.getInstance(); this.checkpointManager = new CheckpointManager(this.config.dbPath); this.metricsCollector = new MetricsCollector(this.config.dbPath, this.state); this.fileHandler = new FileHandler(this.config.dbPath); this.backupManager = new BackupManager(this.config.dbPath); this.logger.info('WAL manager created', { config: this.config, context: { operation: 'create', timestamp: Date.now(), }, }); } static getInstance(dbPath?: string): WALManager { if (!WALManager.instance) { if (!dbPath) { throw new Error('dbPath required for WALManager initialization'); } WALManager.instance = new WALManager(dbPath); } return WALManager.instance; } async enableWAL(db: Database): Promise<void> { // Check for existing initialization if (this.initializationPromise) { try { await this.initializationPromise; return; } catch (error) { // If previous initialization failed, we need to verify database integrity this.logger.warn('Previous initialization failed, checking database integrity', { error, context: { operation: 'enableWAL', timestamp: Date.now(), }, }); // Check if database files exist and are potentially corrupted const walInfo = await this.fileHandler.getWALInfo(); if (walInfo.walSize > 0) { this.logger.error('Found existing WAL file during initialization retry', { context: { operation: 'enableWAL', walInfo, timestamp: Date.now(), }, }); throw createError( ErrorCodes.STORAGE_INIT, 'Database recovery required', 'enableWAL', 'Found existing WAL file during initialization retry. Manual recovery required to prevent data loss.' ); } } } const enableStart = Date.now(); this.initializationPromise = (async () => { try { // Initialize directory first await this.fileHandler.initializeDirectory(); // Check WAL support if (!(await this.fileHandler.checkWALSupport())) { throw createError( ErrorCodes.STORAGE_INIT, 'WAL mode not supported on this system', 'enableWAL', 'The system does not support WAL mode' ); } // Verify no existing WAL files before proceeding const walInfo = await this.fileHandler.getWALInfo(); if (walInfo.walSize > 0) { this.logger.error('Found existing WAL file before initialization', { context: { operation: 'enableWAL', walInfo, timestamp: Date.now(), }, }); throw createError( ErrorCodes.STORAGE_INIT, 'Database recovery required', 'enableWAL', 'Found existing WAL file. Manual recovery required to prevent data loss.' ); } let retryCount = 0; const maxRetries = this.config.retryOptions?.maxAttempts || 5; let lastError: Error | undefined; while (retryCount < maxRetries) { try { // Check current mode first without lock const currentMode = await db.get<{ journal_mode: string }>('PRAGMA journal_mode'); // If WAL is already enabled, just verify and configure if (currentMode?.journal_mode === 'wal') { this.logger.debug('WAL mode already enabled', { context: { operation: 'enableWAL', timestamp: enableStart, }, }); // Set state before configuration this.state.isEnabled = true; // Configure WAL settings without transaction await this.configureWALSafe(db); // Start monitoring this.metricsCollector.startCollecting(); const duration = Date.now() - enableStart; this.logger.info('WAL configuration verified', { duration, context: { operation: 'enableWAL', timestamp: Date.now(), }, }); return; } try { // Enable WAL mode without exclusive lock first await db.exec('PRAGMA journal_mode = WAL'); // Verify WAL mode const mode = await db.get<{ journal_mode: string }>('PRAGMA journal_mode'); if (mode?.journal_mode !== 'wal') { // If failed, try with exclusive lock await db.exec('PRAGMA locking_mode = EXCLUSIVE'); await db.exec('PRAGMA journal_mode = WAL'); // Verify again const modeWithLock = await db.get<{ journal_mode: string }>('PRAGMA journal_mode'); if (modeWithLock?.journal_mode !== 'wal') { throw createError( ErrorCodes.STORAGE_INIT, 'Failed to enable WAL mode', 'enableWAL', `Expected 'wal', got '${modeWithLock?.journal_mode}'` ); } } try { // Set state before configuration this.state.isEnabled = true; // Configure WAL settings await this.configureWAL(db); // Start monitoring this.metricsCollector.startCollecting(); const operationDuration = Date.now() - enableStart; this.logger.info('WAL mode enabled successfully', { duration: operationDuration, retryCount, context: { operation: 'enableWAL', timestamp: Date.now(), }, }); // Emit WAL enabled event this.eventManager.emitSystemEvent({ type: EventTypes.STORAGE_WAL_ENABLED, timestamp: Date.now(), metadata: { dbPath: this.config.dbPath, duration: operationDuration, metrics: { connections: { total: 1, active: 1, idle: 0, errors: retryCount, avgResponseTime: operationDuration, }, cache: { hits: 0, misses: 0, size: 0, maxSize: 0, hitRate: 0, evictions: 0, memoryUsage: 0, }, queries: { total: 1, errors: retryCount, avgExecutionTime: operationDuration, slowQueries: 0, }, timestamp: Date.now(), }, }, }); } catch (configError) { // Reset state if configuration fails this.state.isEnabled = false; throw configError; } return; } finally { // Always try to release exclusive lock try { await db.exec('PRAGMA locking_mode = NORMAL'); } catch (unlockError) { this.logger.warn('Failed to release exclusive lock', { error: unlockError, context: { operation: 'enableWAL', timestamp: Date.now(), }, }); // Don't throw - we don't want to mask the original error if there was one } } } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); // Only retry on transient errors if (!isTransientError(error)) { throw error; } retryCount++; if (retryCount < maxRetries) { const delay = Math.min( (this.config.retryOptions?.initialDelay || 100) * Math.pow(2, retryCount), this.config.retryOptions?.maxDelay || 2000 ); this.logger.debug('Retrying WAL initialization', { attempt: retryCount, maxRetries, delay, error: lastError, context: { operation: 'enableWAL', timestamp: Date.now(), }, }); await new Promise(resolve => setTimeout(resolve, delay)); } } } // If we get here, all retries failed throw createError( ErrorCodes.STORAGE_INIT, 'Failed to enable WAL mode after retries', 'enableWAL', lastError?.message || 'Maximum retry attempts reached', { retryCount, maxRetries, lastError: lastError ? { name: lastError.name, message: lastError.message, stack: lastError.stack, } : undefined, } ); } catch (error) { const duration = Date.now() - enableStart; this.logger.error('Failed to enable WAL mode', { error, duration, context: { operation: 'enableWAL', timestamp: Date.now(), }, }); // Reset state this.state.isEnabled = false; throw error; } finally { this.initializationPromise = null; } })(); await this.initializationPromise; } /** * Configure WAL settings with transaction */ private async configureWAL(db: Database): Promise<void> { try { // Configure WAL behavior one setting at a time await db.exec('PRAGMA synchronous = NORMAL'); await db.exec('PRAGMA wal_autocheckpoint = 1000'); await db.exec(`PRAGMA journal_size_limit = ${this.config.maxWalSize}`); await db.exec('PRAGMA mmap_size = 67108864'); // 64MB memory mapping await db.exec('PRAGMA page_size = 4096'); } catch (error) { this.logger.error('Failed to configure WAL settings', { error, context: { operation: 'configureWAL', timestamp: Date.now(), }, }); throw error; } } /** * Configure WAL settings without transaction for existing WAL mode */ private async configureWALSafe(db: Database): Promise<void> { try { // Set synchronous mode await db.exec('PRAGMA synchronous = NORMAL'); // Configure WAL behavior one at a time await db.exec('PRAGMA wal_autocheckpoint = 1000'); await db.exec(`PRAGMA journal_size_limit = ${this.config.maxWalSize}`); await db.exec('PRAGMA mmap_size = 67108864'); // 64MB memory mapping await db.exec('PRAGMA page_size = 4096'); } catch (error) { // Log error but don't fail - these are optional optimizations this.logger.warn('Failed to configure WAL settings', { error, context: { operation: 'configureWALSafe', timestamp: Date.now(), }, }); } } async checkpoint(db: Database): Promise<void> { if (!this.state.isEnabled) { throw createError( ErrorCodes.STORAGE_ERROR, 'WAL mode not enabled', 'checkpoint', 'Cannot checkpoint when WAL mode is not enabled' ); } // Get current WAL info before checkpoint const walInfo = await this.fileHandler.getWALInfo(); // If WAL file is getting large, create automatic backup if (walInfo.walSize > this.config.maxWalSize * 0.8) { this.logger.warn('WAL file size approaching limit - creating backup', { context: { operation: 'checkpoint', walSize: walInfo.walSize, maxSize: this.config.maxWalSize, timestamp: Date.now(), }, }); try { // Create backup using backup manager await this.backupManager.createBackup(); } catch (backupError) { this.logger.error('Failed to create backup before checkpoint', { error: backupError, context: { operation: 'checkpoint.backup', timestamp: Date.now(), }, }); // Continue with checkpoint even if backup fails } } try { const result = await this.checkpointManager.executeCheckpoint(db, this.config.retryOptions); // Update state this.state.lastCheckpoint = Date.now(); this.state.checkpointCount++; this.state.totalCheckpointTime += result.duration; // Track max WAL size if (walInfo.walSize > this.state.maxWalSizeReached) { this.state.maxWalSizeReached = walInfo.walSize; } // Emit checkpoint event with detailed metrics this.eventManager.emitSystemEvent({ type: EventTypes.STORAGE_WAL_CHECKPOINT, timestamp: Date.now(), metadata: { dbPath: this.config.dbPath, checkpointCount: this.state.checkpointCount, metrics: { storage: { size: walInfo.walSize, // Total size is WAL size for checkpoint metrics walSize: walInfo.walSize, pageSize: 4096, // Standard SQLite page size pageCount: Math.ceil(walInfo.walSize / 4096), journalMode: 'wal', }, performance: { queryTime: 0, transactionTime: 0, walCheckpointTime: this.state.totalCheckpointTime / this.state.checkpointCount, cacheHitRate: 0, indexHitRate: 0, }, connections: { total: 1, active: 1, idle: 0, errors: 0, avgResponseTime: result.duration, }, queries: { total: 1, errors: 0, slowQueries: 0, avgExecutionTime: result.duration, }, cache: { hits: 0, misses: 0, size: 0, maxSize: 0, hitRate: 0, evictions: 0, memoryUsage: 0, }, timestamp: Date.now(), }, }, }); // Log detailed checkpoint metrics this.logger.info('Checkpoint completed successfully', { context: { operation: 'checkpoint', walSizeBefore: walInfo.walSize, duration: result.duration, checkpointCount: this.state.checkpointCount, timestamp: Date.now(), }, }); } catch (error) { // Enhanced error reporting for checkpoint failures this.logger.error('Checkpoint failed', { error, context: { operation: 'checkpoint', walInfo, state: this.state, timestamp: Date.now(), }, }); throw createError( ErrorCodes.STORAGE_ERROR, 'Checkpoint failed', 'checkpoint', error instanceof Error ? error.message : 'Unknown error during checkpoint', { walSize: walInfo.walSize, lastCheckpoint: this.state.lastCheckpoint, checkpointCount: this.state.checkpointCount, } ); } } async getMetrics(): Promise<WALMetrics> { return this.metricsCollector.getMetrics(); } async verifyIntegrity(): Promise<boolean> { return this.fileHandler.verifyIntegrity(); } async close(): Promise<void> { const closeStart = Date.now(); try { this.logger.info('Closing WAL manager', { metrics: await this.getMetrics(), context: { operation: 'close', timestamp: closeStart, }, }); // Stop metrics collection this.metricsCollector.stopCollecting(); // Clean up files await this.fileHandler.cleanup(); await this.backupManager.cleanup(); // Reset state this.state = { isEnabled: false, lastCheckpoint: 0, checkpointCount: 0, totalCheckpointTime: 0, maxWalSizeReached: 0, }; this.logger.info('WAL manager closed successfully', { duration: Date.now() - closeStart, context: { operation: 'close', timestamp: Date.now(), }, }); } catch (error) { this.logger.error('Error closing WAL manager', { error, duration: Date.now() - closeStart, context: { operation: 'close', timestamp: Date.now(), }, }); throw error; } } }