UNPKG

claude-flow-novice

Version:

Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.

578 lines (577 loc) 20.4 kB
/** * File Lock Manager * * Centralized file locking system with queuing, renewal, and monitoring. * Part of Task 4.2: Centralized File Locking & Atomic Operations * * Features: * - Lock acquisition with configurable timeout (300s default) * - Waiting queue for blocked processes * - Lock renewal for long-running operations * - Force release capability for stuck locks * - Owner tracking (process ID + agent ID) * - Stale lock detection and cleanup * - Performance monitoring (<100ms acquisition target) * - Automatic lock release on process exit * * Usage: * const manager = new FileLockManager(); * const lock = await manager.acquireLock('/path/to/file.txt', { * agentId: 'backend-dev-001', * timeout: 30000 * }); * try { * // Perform file operations * } finally { * await manager.releaseLock(lock.id); * } */ import * as fs from 'fs'; import * as path from 'path'; import { promisify } from 'util'; import { randomUUID } from 'crypto'; import { createLogger } from './logging.js'; import { createError, ErrorCode, createTimeoutError } from './errors.js'; const logger = createLogger('file-lock-manager'); const fsWriteFile = promisify(fs.writeFile); const fsReadFile = promisify(fs.readFile); const fsUnlink = promisify(fs.unlink); const fsStat = promisify(fs.stat); const fsMkdir = promisify(fs.mkdir); const fsAccess = promisify(fs.access); /** * Lock directory (defaults to /tmp/cfn-locks/) */ const LOCK_DIR = process.env.CFN_LOCK_DIR || '/tmp/cfn-locks'; /** * File Lock Manager * * Manages file locks with queuing, renewal, and monitoring capabilities. */ export class FileLockManager { activeLocks = new Map(); lockQueues = new Map(); metrics = { acquisitions: 0, releases: 0, activeLocks: 0, timeouts: 0, staleLocksRemoved: 0, avgAcquisitionTimeMs: 0, totalAcquisitionTimeMs: 0, renewals: 0, forceReleases: 0 }; cleanupInterval = null; constructor(){ this.ensureLockDirectory(); this.setupCleanupInterval(); this.setupProcessExitHandlers(); } /** * Acquire a file lock * * @param filePath - Path to file to lock * @param options - Lock acquisition options * @returns Promise<FileLock> - Lock instance * @throws LOCK_TIMEOUT if acquisition times out */ async acquireLock(filePath, options = {}) { const startTime = Date.now(); const opts = { timeout: options.timeout || 300000, retryInterval: options.retryInterval || 100, agentId: options.agentId, waitInQueue: options.waitInQueue !== false, staleTimeout: options.staleTimeout || 300000 }; const absolutePath = path.resolve(filePath); const lockPath = this.getLockPath(absolutePath); const lockId = randomUUID(); logger.debug('Attempting lock acquisition', { filePath: absolutePath, lockId, agentId: opts.agentId }); // Try immediate acquisition const immediateLock = await this.tryAcquireLock(absolutePath, lockPath, lockId, opts); if (immediateLock) { const acquisitionTime = Date.now() - startTime; this.recordAcquisition(acquisitionTime); logger.info('Lock acquired immediately', { filePath: absolutePath, lockId, acquisitionTimeMs: acquisitionTime }); return immediateLock; } // If immediate acquisition failed and queuing is enabled, wait in queue if (opts.waitInQueue) { return this.waitInQueue(absolutePath, lockPath, lockId, opts, startTime); } throw createTimeoutError(`acquire lock on ${absolutePath}`, 0); } /** * Try to acquire lock immediately */ async tryAcquireLock(filePath, lockPath, lockId, options) { try { // Check if lock file exists const exists = await this.fileExists(lockPath); if (exists) { // Check if lock is stale const isStale = await this.isLockStale(lockPath, options.staleTimeout); if (isStale) { logger.warn('Removing stale lock', { lockPath }); await this.forceReleaseLock(lockPath); this.metrics.staleLocksRemoved++; } else { return null; // Lock is held by another process } } // Create lock const owner = { pid: process.pid, agentId: options.agentId, hostname: require('os').hostname() }; const expiresAt = new Date(Date.now() + options.timeout); const metadata = { lockId, filePath, owner, acquiredAt: new Date().toISOString(), expiresAt: expiresAt.toISOString(), timeoutMs: options.timeout, renewalCount: 0 }; // Write lock file atomically await this.writeLockFile(lockPath, metadata); // Verify we won the race const verify = await this.readLockFile(lockPath); if (verify.lockId !== lockId) { logger.debug('Lost lock race', { lockId, winnerId: verify.lockId }); return null; } // Create and store lock instance const lock = { id: lockId, filePath, lockPath, owner, acquiredAt: new Date(), expiresAt, timeoutMs: options.timeout, renewalCount: 0 }; this.activeLocks.set(lockId, lock); this.metrics.activeLocks++; return lock; } catch (error) { logger.error('Error during lock acquisition attempt', error instanceof Error ? error : undefined, { filePath, lockId }); return null; } } /** * Wait in queue for lock availability */ async waitInQueue(filePath, lockPath, lockId, options, startTime) { return new Promise((resolve, reject)=>{ const queueEntry = { id: randomUUID(), filePath, agentId: options.agentId, pid: process.pid, queuedAt: new Date(), resolve, reject }; // Set timeout queueEntry.timeoutTimer = setTimeout(()=>{ this.removeFromQueue(filePath, queueEntry.id); this.metrics.timeouts++; reject(createTimeoutError(`acquire lock on ${filePath}`, options.timeout)); }, options.timeout); // Add to queue if (!this.lockQueues.has(filePath)) { this.lockQueues.set(filePath, []); } this.lockQueues.get(filePath).push(queueEntry); logger.debug('Added to lock queue', { filePath, queuePosition: this.lockQueues.get(filePath).length, agentId: options.agentId }); // Start polling for lock availability this.pollForLock(filePath, lockPath, lockId, options, queueEntry, startTime); }); } /** * Poll for lock availability */ async pollForLock(filePath, lockPath, lockId, options, queueEntry, startTime) { const pollInterval = setInterval(async ()=>{ try { const lock = await this.tryAcquireLock(filePath, lockPath, lockId, options); if (lock) { clearInterval(pollInterval); if (queueEntry.timeoutTimer) { clearTimeout(queueEntry.timeoutTimer); } this.removeFromQueue(filePath, queueEntry.id); const acquisitionTime = Date.now() - startTime; this.recordAcquisition(acquisitionTime); logger.info('Lock acquired from queue', { filePath, lockId, waitTimeMs: acquisitionTime }); queueEntry.resolve(lock); } } catch (error) { clearInterval(pollInterval); if (queueEntry.timeoutTimer) { clearTimeout(queueEntry.timeoutTimer); } this.removeFromQueue(filePath, queueEntry.id); queueEntry.reject(error instanceof Error ? error : new Error(`Lock acquisition failed: ${String(error)}`)); } }, options.retryInterval); } /** * Release a lock * * @param lockId - Lock ID to release * @returns Promise that resolves when lock is released */ async releaseLock(lockId) { const lock = this.activeLocks.get(lockId); if (!lock) { logger.warn('Attempted to release non-existent lock', { lockId }); return; } try { // Remove lock file await fsUnlink(lock.lockPath); // Remove from active locks this.activeLocks.delete(lockId); this.metrics.activeLocks--; this.metrics.releases++; logger.info('Lock released', { lockId, filePath: lock.filePath, heldDurationMs: Date.now() - lock.acquiredAt.getTime() }); } catch (error) { logger.error('Error releasing lock', error instanceof Error ? error : undefined, { lockId, filePath: lock.filePath }); throw createError(ErrorCode.LOCK_RELEASE_FAILED, `Failed to release lock ${lockId}`, { lockId, filePath: lock.filePath }, error instanceof Error ? error : undefined); } } /** * Renew a lock (extend expiration time) * * @param lockId - Lock ID to renew * @param extensionMs - Extension time in milliseconds * @returns Promise that resolves when lock is renewed */ async renewLock(lockId, extensionMs = 300000) { const lock = this.activeLocks.get(lockId); if (!lock) { throw createError(ErrorCode.LOCK_NOT_FOUND, `Lock not found: ${lockId}`, { lockId }); } try { // Read current metadata const metadata = await this.readLockFile(lock.lockPath); // Verify ownership if (metadata.lockId !== lockId) { throw createError(ErrorCode.LOCK_OWNERSHIP_MISMATCH, 'Lock ownership mismatch during renewal', { lockId, actualOwnerId: metadata.lockId }); } // Update expiration const newExpiresAt = new Date(Date.now() + extensionMs); metadata.expiresAt = newExpiresAt.toISOString(); metadata.lastRenewedAt = new Date().toISOString(); metadata.renewalCount++; // Write updated metadata await this.writeLockFile(lock.lockPath, metadata); // Update in-memory lock lock.expiresAt = newExpiresAt; lock.renewalCount = metadata.renewalCount; this.metrics.renewals++; logger.info('Lock renewed', { lockId, filePath: lock.filePath, newExpiresAt: newExpiresAt.toISOString(), renewalCount: metadata.renewalCount }); } catch (error) { logger.error('Error renewing lock', error instanceof Error ? error : undefined, { lockId, filePath: lock.filePath }); throw error; } } /** * Force release a lock (for stuck locks) * * @param lockPath - Path to lock file * @returns Promise that resolves when lock is force-released */ async forceReleaseLock(lockPath) { try { const exists = await this.fileExists(lockPath); if (exists) { const metadata = await this.readLockFile(lockPath); await fsUnlink(lockPath); // Remove from active locks if present if (this.activeLocks.has(metadata.lockId)) { this.activeLocks.delete(metadata.lockId); this.metrics.activeLocks--; } this.metrics.forceReleases++; logger.warn('Lock force-released', { lockPath, lockId: metadata.lockId, owner: metadata.owner }); } } catch (error) { logger.error('Error force-releasing lock', error instanceof Error ? error : undefined, { lockPath }); throw error; } } /** * Get lock metrics */ getMetrics() { return { ...this.metrics }; } /** * Get queue status for a file */ getQueueStatus(filePath) { const absolutePath = path.resolve(filePath); const queue = this.lockQueues.get(absolutePath); if (!queue || queue.length === 0) { return null; } return { position: 1, total: queue.length }; } /** * Helper: Get lock file path */ getLockPath(filePath) { const hash = this.hashFilePath(filePath); return path.join(LOCK_DIR, `${hash}.lock`); } /** * Helper: Hash file path for lock file naming */ hashFilePath(filePath) { const crypto = require('crypto'); return crypto.createHash('sha256').update(filePath).digest('hex').substring(0, 16); } /** * Helper: Check if file exists */ async fileExists(filePath) { try { await fsAccess(filePath, fs.constants.F_OK); return true; } catch { return false; } } /** * Helper: Check if lock is stale */ async isLockStale(lockPath, staleTimeout) { try { const metadata = await this.readLockFile(lockPath); const expiresAt = new Date(metadata.expiresAt); const now = new Date(); return now > expiresAt; } catch { // If we can't read the lock file, consider it stale return true; } } /** * Helper: Write lock file */ async writeLockFile(lockPath, metadata) { await fsWriteFile(lockPath, JSON.stringify(metadata, null, 2), 'utf8'); } /** * Helper: Read lock file */ async readLockFile(lockPath) { const content = await fsReadFile(lockPath, 'utf8'); return JSON.parse(content); } /** * Helper: Remove entry from queue */ removeFromQueue(filePath, entryId) { const queue = this.lockQueues.get(filePath); if (queue) { const index = queue.findIndex((e)=>e.id === entryId); if (index !== -1) { queue.splice(index, 1); } if (queue.length === 0) { this.lockQueues.delete(filePath); } } } /** * Helper: Record acquisition metrics */ recordAcquisition(acquisitionTimeMs) { this.metrics.acquisitions++; this.metrics.totalAcquisitionTimeMs += acquisitionTimeMs; this.metrics.avgAcquisitionTimeMs = this.metrics.totalAcquisitionTimeMs / this.metrics.acquisitions; } /** * Ensure lock directory exists */ ensureLockDirectory() { try { if (!fs.existsSync(LOCK_DIR)) { fs.mkdirSync(LOCK_DIR, { recursive: true, mode: 0o755 }); logger.info('Created lock directory', { directory: LOCK_DIR }); } } catch (error) { logger.error('Failed to create lock directory', error instanceof Error ? error : undefined, { directory: LOCK_DIR }); } } /** * Setup periodic cleanup of stale locks */ setupCleanupInterval() { // Clean up stale locks every 60 seconds this.cleanupInterval = setInterval(async ()=>{ await this.cleanupStaleLocks(); }, 60000); } /** * Cleanup stale locks */ async cleanupStaleLocks() { try { const files = fs.readdirSync(LOCK_DIR); for (const file of files){ if (file.endsWith('.lock')) { const lockPath = path.join(LOCK_DIR, file); const isStale = await this.isLockStale(lockPath, 300000); if (isStale) { await this.forceReleaseLock(lockPath); } } } } catch (error) { logger.error('Error during stale lock cleanup', error instanceof Error ? error : undefined); } } /** * Setup process exit handlers to release locks */ setupProcessExitHandlers() { const cleanup = async ()=>{ logger.info('Process exiting, releasing all locks', { activeLocksCount: this.activeLocks.size }); for (const [lockId, lock] of this.activeLocks.entries()){ try { await this.releaseLock(lockId); } catch (error) { logger.error('Error releasing lock on exit', error instanceof Error ? error : undefined, { lockId, filePath: lock.filePath }); } } if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } }; process.on('exit', ()=>{ // Synchronous cleanup only for (const [, lock] of this.activeLocks.entries()){ try { fs.unlinkSync(lock.lockPath); } catch {} } }); process.on('SIGINT', async ()=>{ await cleanup(); process.exit(0); }); process.on('SIGTERM', async ()=>{ await cleanup(); process.exit(0); }); } /** * Shutdown the lock manager */ async shutdown() { logger.info('Shutting down file lock manager'); // Clear cleanup interval if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } // Release all active locks for (const lockId of this.activeLocks.keys()){ await this.releaseLock(lockId); } // Clear all queues for (const [, queue] of this.lockQueues.entries()){ for (const entry of queue){ if (entry.timeoutTimer) { clearTimeout(entry.timeoutTimer); } entry.reject(new Error('Lock manager shutting down')); } } this.lockQueues.clear(); } } /** * Singleton instance */ let defaultManager = null; /** * Get the default file lock manager instance */ export function getFileLockManager() { if (!defaultManager) { defaultManager = new FileLockManager(); } return defaultManager; } /** * Execute a function with file lock * * @param filePath - File to lock * @param fn - Function to execute * @param options - Lock options * @returns Promise that resolves with function result */ export async function withFileLock(filePath, fn, options = {}) { const manager = getFileLockManager(); const lock = await manager.acquireLock(filePath, options); try { return await fn(); } finally{ await manager.releaseLock(lock.id); } } //# sourceMappingURL=file-lock-manager.js.map