UNPKG

vibe-coder-mcp

Version:

Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.

522 lines (521 loc) 20 kB
import fs from 'fs-extra'; import path from 'path'; import os from 'os'; import { getTimeoutManager } from '../utils/timeout-manager.js'; import { AppError } from '../../../utils/errors.js'; import logger from '../../../logger.js'; export class ConcurrentAccessManager { static instance = null; config = null; userConfig; initialized = false; activeLocks = new Map(); lockWaiters = new Map(); auditEvents = []; deadlockDetectionTimer = null; cleanupTimer = null; lockCounter = 0; auditCounter = 0; constructor(config) { this.userConfig = config; logger.info('Concurrent Access Manager initialized (background tasks deferred)'); } getConfig() { if (!this.config) { const isTestEnv = process.env.NODE_ENV === 'test'; const timeoutManager = getTimeoutManager(); const retryConfig = timeoutManager.getRetryConfig(); this.config = { lockDirectory: isTestEnv ? path.join(process.cwd(), 'tmp', 'test-locks') : this.getOSAwareLockDirectory(), defaultLockTimeout: isTestEnv ? 5000 : timeoutManager.getTimeout('databaseOperations'), maxLockTimeout: isTestEnv ? 10000 : timeoutManager.getTimeout('taskExecution'), deadlockDetectionInterval: isTestEnv ? 1000 : 10000, lockCleanupInterval: isTestEnv ? 2000 : 60000, maxRetryAttempts: retryConfig.maxRetries, retryDelayMs: isTestEnv ? 100 : retryConfig.initialDelayMs, enableDeadlockDetection: !isTestEnv, enableLockAuditTrail: true, ...this.userConfig }; logger.debug('Concurrent Access Manager configuration initialized with timeout values'); this.ensureInitialized(); } return this.config; } async ensureInitialized() { if (!this.initialized) { this.initialized = true; await this.initializeLockDirectory(); this.startBackgroundTasks(); logger.debug('Concurrent Access Manager background tasks initialized'); } } static getInstance(config) { if (!ConcurrentAccessManager.instance) { ConcurrentAccessManager.instance = new ConcurrentAccessManager(config); } return ConcurrentAccessManager.instance; } static resetInstance() { if (ConcurrentAccessManager.instance) { ConcurrentAccessManager.instance.dispose(); ConcurrentAccessManager.instance = null; } } static hasInstance() { return ConcurrentAccessManager.instance !== null; } async acquireLock(resource, owner, operation = 'write', options) { const startTime = Date.now(); const timeout = Math.min(options?.timeout || this.getConfig().defaultLockTimeout, this.getConfig().maxLockTimeout); const lockId = `lock_${++this.lockCounter}_${Date.now()}`; try { const existingLock = this.findConflictingLock(resource, operation); if (existingLock) { if (options?.waitForRelease) { return await this.waitForLockRelease(resource, owner, operation, lockId, timeout, options); } else { this.logAuditEvent('conflict', lockId, resource, owner, options?.sessionId, 0, { conflictingLock: existingLock.id, operation }); return { success: false, error: 'Resource is locked', conflictingLock: existingLock, waitTime: Date.now() - startTime }; } } const lock = { id: lockId, resource, owner, sessionId: options?.sessionId, operation, acquiredAt: new Date(), expiresAt: new Date(Date.now() + timeout), metadata: options?.metadata }; await this.atomicLockAcquisition(lock); this.activeLocks.set(lockId, lock); if (this.getConfig().enableLockAuditTrail) { this.logAuditEvent('acquire', lockId, resource, owner, options?.sessionId, Date.now() - startTime, { operation, timeout }); } logger.debug({ lockId, resource, owner, operation, timeout }, 'Lock acquired successfully'); return { success: true, lock, waitTime: Date.now() - startTime }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logAuditEvent('conflict', lockId, resource, owner, options?.sessionId, Date.now() - startTime, { error: errorMessage }); logger.error({ err: error, lockId, resource, owner }, 'Failed to acquire lock'); return { success: false, error: errorMessage, waitTime: Date.now() - startTime }; } } async releaseLock(lockId) { try { const lock = this.activeLocks.get(lockId); if (!lock) { logger.warn({ lockId }, 'Attempted to release non-existent lock'); return false; } this.activeLocks.delete(lockId); await this.removeFileLock(lock); this.notifyWaiters(lock.resource); const duration = Date.now() - lock.acquiredAt.getTime(); if (this.getConfig().enableLockAuditTrail) { this.logAuditEvent('release', lockId, lock.resource, lock.owner, lock.sessionId, duration); } logger.debug({ lockId, resource: lock.resource, owner: lock.owner, duration: `${duration}ms` }, 'Lock released successfully'); return true; } catch (error) { logger.error({ err: error, lockId }, 'Failed to release lock'); return false; } } async atomicLockAcquisition(lock) { const lockFilePath = path.join(this.getConfig().lockDirectory, `${this.sanitizeResourceName(lock.resource)}.lock`); try { await fs.ensureDir(this.getConfig().lockDirectory); const lockData = JSON.stringify(lock, null, 2); await fs.writeFile(lockFilePath, lockData, { flag: 'wx' }); } catch (error) { if (error instanceof Error && 'code' in error && error.code === 'EEXIST') { const existingLock = await this.readLockFile(lockFilePath); if (existingLock && this.isLockExpired(existingLock)) { await fs.remove(lockFilePath); await fs.writeFile(lockFilePath, JSON.stringify(lock, null, 2), { flag: 'wx' }); } else { throw new AppError('Resource is already locked'); } } else if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { logger.warn('Lock directory not accessible, using in-memory locking only'); return; } else { throw error; } } } async waitForLockRelease(resource, owner, operation, lockId, timeout, options) { return new Promise((resolve, reject) => { const timeoutHandle = setTimeout(() => { this.removeWaiter(resource, resolve); this.logAuditEvent('timeout', lockId, resource, owner, options?.sessionId, timeout); resolve({ success: false, error: 'Lock acquisition timeout', waitTime: timeout }); }, timeout); const waiter = { resolve: async () => { clearTimeout(timeoutHandle); this.removeWaiter(resource, resolve); const result = await this.acquireLock(resource, owner, operation, { ...options, waitForRelease: false }); resolve(result); }, reject, timeout: timeoutHandle }; if (!this.lockWaiters.has(resource)) { this.lockWaiters.set(resource, []); } this.lockWaiters.get(resource).push(waiter); }); } findConflictingLock(resource, operation) { for (const lock of this.activeLocks.values()) { if (lock.resource === resource) { if (operation === 'write' || lock.operation === 'write') { if (!this.isLockExpired(lock)) { return lock; } } } } return null; } isLockExpired(lock) { return Date.now() > lock.expiresAt.getTime(); } notifyWaiters(resource) { const waiters = this.lockWaiters.get(resource); if (waiters && waiters.length > 0) { const waiter = waiters.shift(); waiter.resolve({ success: true }); if (waiters.length === 0) { this.lockWaiters.delete(resource); } } } removeWaiter(resource, resolveFunc) { const waiters = this.lockWaiters.get(resource); if (waiters) { const index = waiters.findIndex(w => w.resolve === resolveFunc); if (index !== -1) { clearTimeout(waiters[index].timeout); waiters.splice(index, 1); if (waiters.length === 0) { this.lockWaiters.delete(resource); } } } } getOSAwareLockDirectory() { const envLockDir = process.env.VIBE_LOCK_DIR; if (envLockDir) { return envLockDir; } try { const tempDir = os.tmpdir(); return path.join(tempDir, 'vibe-locks'); } catch (error) { logger.warn({ error }, 'Failed to get OS temp directory, using project fallback'); return path.join(process.cwd(), 'tmp', 'vibe-locks'); } } async initializeLockDirectory() { try { await fs.ensureDir(this.getConfig().lockDirectory); await this.cleanupStaleLocks(); } catch (error) { logger.warn({ err: error }, 'Failed to initialize lock directory, continuing without file-based locking'); } } startBackgroundTasks() { if (this.getConfig().enableDeadlockDetection) { this.deadlockDetectionTimer = setInterval(() => { this.detectDeadlocks(); }, this.getConfig().deadlockDetectionInterval); } this.cleanupTimer = setInterval(() => { this.cleanupExpiredLocks(); }, this.getConfig().lockCleanupInterval); } async detectDeadlocks() { const waitGraph = new Map(); for (const [resource, waiters] of this.lockWaiters) { const lockHolder = this.findLockHolder(resource); if (lockHolder) { for (let i = 0; i < waiters.length; i++) { const waiterId = 'waiter'; if (!waitGraph.has(waiterId)) { waitGraph.set(waiterId, []); } waitGraph.get(waiterId).push(lockHolder.owner); } } } const hasDeadlock = this.hasCycle(waitGraph); if (hasDeadlock) { this.logAuditEvent('deadlock', 'system', 'system', 'system', undefined, 0, { activeLocks: this.activeLocks.size, waitingRequests: Array.from(this.lockWaiters.values()).reduce((sum, waiters) => sum + waiters.length, 0) }); logger.warn({ activeLocks: this.activeLocks.size, waitingRequests: Array.from(this.lockWaiters.values()).reduce((sum, waiters) => sum + waiters.length, 0) }, 'Deadlock detected'); } return { hasDeadlock, resolutionStrategy: hasDeadlock ? 'timeout' : undefined }; } hasCycle(graph) { const visited = new Set(); const recursionStack = new Set(); const dfs = (node) => { visited.add(node); recursionStack.add(node); const neighbors = graph.get(node) || []; for (const neighbor of neighbors) { if (!visited.has(neighbor)) { if (dfs(neighbor)) return true; } else if (recursionStack.has(neighbor)) { return true; } } recursionStack.delete(node); return false; }; for (const node of graph.keys()) { if (!visited.has(node)) { if (dfs(node)) return true; } } return false; } findLockHolder(resource) { for (const lock of this.activeLocks.values()) { if (lock.resource === resource && !this.isLockExpired(lock)) { return lock; } } return null; } async cleanupExpiredLocks() { const expiredLocks = []; for (const [lockId, lock] of this.activeLocks) { if (this.isLockExpired(lock)) { expiredLocks.push(lockId); } } for (const lockId of expiredLocks) { await this.releaseLock(lockId); this.logAuditEvent('timeout', lockId, 'expired', 'system', undefined, 0); } if (expiredLocks.length > 0) { logger.info({ expiredLocks: expiredLocks.length }, 'Cleaned up expired locks'); } } async cleanupStaleLocks() { try { const lockFiles = await fs.readdir(this.getConfig().lockDirectory); for (const file of lockFiles) { if (file.endsWith('.lock')) { const lockFilePath = path.join(this.getConfig().lockDirectory, file); const lock = await this.readLockFile(lockFilePath); if (lock && this.isLockExpired(lock)) { await fs.remove(lockFilePath); logger.debug({ lockFile: file }, 'Removed stale lock file'); } } } } catch (error) { logger.warn({ err: error }, 'Failed to cleanup stale locks'); } } async readLockFile(lockFilePath) { try { const lockData = await fs.readFile(lockFilePath, 'utf-8'); const parsed = JSON.parse(lockData); if (parsed.acquiredAt && typeof parsed.acquiredAt === 'string') { parsed.acquiredAt = new Date(parsed.acquiredAt); } if (parsed.expiresAt && typeof parsed.expiresAt === 'string') { parsed.expiresAt = new Date(parsed.expiresAt); } return parsed; } catch { return null; } } async removeFileLock(lock) { const lockFilePath = path.join(this.getConfig().lockDirectory, `${this.sanitizeResourceName(lock.resource)}.lock`); try { await fs.remove(lockFilePath); } catch (error) { logger.warn({ err: error, lockFilePath }, 'Failed to remove lock file'); } } sanitizeResourceName(resource) { return resource.replace(/[^a-zA-Z0-9_-]/g, '_'); } logAuditEvent(type, lockId, resource, owner, sessionId, duration, metadata) { if (!this.getConfig().enableLockAuditTrail) return; const auditEvent = { id: `lock_audit_${++this.auditCounter}_${Date.now()}`, type, lockId, resource, owner, sessionId, timestamp: new Date(), duration, metadata }; this.auditEvents.push(auditEvent); if (this.auditEvents.length > 1000) { this.auditEvents = this.auditEvents.slice(-1000); } logger.debug({ auditEvent }, `Lock ${type} event`); } getActiveLocks() { return Array.from(this.activeLocks.values()) .filter(lock => !this.isLockExpired(lock)); } getLockStatistics() { const active = this.getActiveLocks().length; const expired = this.activeLocks.size - active; const waiting = Array.from(this.lockWaiters.values()).reduce((sum, waiters) => sum + waiters.length, 0); const acquisitions = this.auditEvents.filter(e => e.type === 'acquire').length; const releases = this.auditEvents.filter(e => e.type === 'release').length; const timeouts = this.auditEvents.filter(e => e.type === 'timeout').length; const deadlocks = this.auditEvents.filter(e => e.type === 'deadlock').length; const releasedEvents = this.auditEvents.filter(e => e.type === 'release' && e.duration); const avgDuration = releasedEvents.length > 0 ? releasedEvents.reduce((sum, e) => sum + (e.duration || 0), 0) / releasedEvents.length : 0; return { activeLocks: active, expiredLocks: expired, waitingRequests: waiting, totalAcquisitions: acquisitions, totalReleases: releases, totalTimeouts: timeouts, totalDeadlocks: deadlocks, averageLockDuration: avgDuration }; } async clearAllLocks() { const lockIds = Array.from(this.activeLocks.keys()); let clearedCount = 0; for (const lockId of lockIds) { try { await this.releaseLock(lockId); clearedCount++; } catch (error) { logger.warn({ lockId, error }, 'Failed to clear lock'); } } for (const waiters of this.lockWaiters.values()) { for (const waiter of waiters) { clearTimeout(waiter.timeout); waiter.reject(new Error('All locks cleared')); } } this.lockWaiters.clear(); logger.debug({ clearedCount }, 'All locks cleared'); } async dispose() { await this.shutdown(); } async shutdown() { if (this.deadlockDetectionTimer) { clearInterval(this.deadlockDetectionTimer); } if (this.cleanupTimer) { clearInterval(this.cleanupTimer); } const lockIds = Array.from(this.activeLocks.keys()); for (const lockId of lockIds) { await this.releaseLock(lockId); } for (const waiters of this.lockWaiters.values()) { for (const waiter of waiters) { clearTimeout(waiter.timeout); waiter.reject(new Error('Concurrent access manager shutdown')); } } this.lockWaiters.clear(); this.auditEvents = []; logger.info('Concurrent Access Manager shutdown'); } } export function getConcurrentAccessManager() { return ConcurrentAccessManager.getInstance(); }