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
JavaScript
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();
}