Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
This commit is contained in:
235
dashcaddy-api/port-lock-manager.js
Normal file
235
dashcaddy-api/port-lock-manager.js
Normal file
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* Port Lock Manager
|
||||
* Provides atomic port allocation using file-based locks to prevent race conditions
|
||||
* during concurrent container deployments
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const lockfile = require('proper-lockfile');
|
||||
|
||||
const LOCK_DIR = path.join(__dirname, '.port-locks');
|
||||
const LOCK_TIMEOUT = 120000; // 2 minutes
|
||||
const LOCK_STALE_THRESHOLD = 120000; // 2 minutes
|
||||
const LOCK_RETRY_OPTIONS = {
|
||||
retries: {
|
||||
retries: 10,
|
||||
minTimeout: 100,
|
||||
maxTimeout: 1000,
|
||||
randomize: true
|
||||
},
|
||||
stale: LOCK_STALE_THRESHOLD,
|
||||
realpath: false
|
||||
};
|
||||
|
||||
class PortLockManager {
|
||||
constructor() {
|
||||
this.activeLocks = new Map(); // Map of lockId -> { ports: [], release: fn }
|
||||
this.ensureLockDirectory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure lock directory exists
|
||||
*/
|
||||
ensureLockDirectory() {
|
||||
if (!fs.existsSync(LOCK_DIR)) {
|
||||
fs.mkdirSync(LOCK_DIR, { recursive: true });
|
||||
console.log('[PortLockManager] Created lock directory:', LOCK_DIR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get lock file path for a port
|
||||
*/
|
||||
getLockFilePath(port) {
|
||||
return path.join(LOCK_DIR, `port-${port}.lock`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire locks for multiple ports atomically
|
||||
* Ports are sorted to prevent deadlocks
|
||||
* @param {string[]} ports - Array of port numbers as strings
|
||||
* @returns {Promise<string>} Lock ID for releasing locks later
|
||||
*/
|
||||
async acquirePorts(ports) {
|
||||
if (!Array.isArray(ports) || ports.length === 0) {
|
||||
throw new Error('Ports must be a non-empty array');
|
||||
}
|
||||
|
||||
const lockId = `lock-${Date.now()}-${Math.random().toString(36).substring(7)}`;
|
||||
const sortedPorts = [...new Set(ports)].sort((a, b) => parseInt(a) - parseInt(b));
|
||||
const acquiredLocks = [];
|
||||
const releaseFunctions = [];
|
||||
|
||||
try {
|
||||
console.log(`[PortLockManager] Acquiring locks for ports: ${sortedPorts.join(', ')}`);
|
||||
|
||||
// Acquire locks in sorted order to prevent deadlocks
|
||||
for (const port of sortedPorts) {
|
||||
const lockFilePath = this.getLockFilePath(port);
|
||||
|
||||
// Create lock file if it doesn't exist
|
||||
if (!fs.existsSync(lockFilePath)) {
|
||||
fs.writeFileSync(lockFilePath, JSON.stringify({
|
||||
created: new Date().toISOString(),
|
||||
port
|
||||
}));
|
||||
}
|
||||
|
||||
// Acquire lock with retry
|
||||
const release = await lockfile.lock(lockFilePath, LOCK_RETRY_OPTIONS);
|
||||
|
||||
acquiredLocks.push(port);
|
||||
releaseFunctions.push(release);
|
||||
|
||||
console.log(`[PortLockManager] Locked port ${port}`);
|
||||
}
|
||||
|
||||
// Store lock information
|
||||
this.activeLocks.set(lockId, {
|
||||
ports: sortedPorts,
|
||||
releases: releaseFunctions,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
console.log(`[PortLockManager] Successfully acquired all locks (ID: ${lockId})`);
|
||||
return lockId;
|
||||
|
||||
} catch (error) {
|
||||
// Release any locks we managed to acquire
|
||||
console.error(`[PortLockManager] Failed to acquire all locks:`, error.message);
|
||||
|
||||
for (const release of releaseFunctions) {
|
||||
try {
|
||||
await release();
|
||||
} catch (releaseError) {
|
||||
console.error(`[PortLockManager] Error releasing lock during cleanup:`, releaseError.message);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Failed to acquire port locks: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Release locks for a lock ID
|
||||
* @param {string} lockId - Lock ID returned from acquirePorts
|
||||
*/
|
||||
async releasePorts(lockId) {
|
||||
const lockInfo = this.activeLocks.get(lockId);
|
||||
|
||||
if (!lockInfo) {
|
||||
console.warn(`[PortLockManager] Lock ID ${lockId} not found (may have been released already)`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[PortLockManager] Releasing locks for ports: ${lockInfo.ports.join(', ')}`);
|
||||
|
||||
const errors = [];
|
||||
|
||||
for (const release of lockInfo.releases) {
|
||||
try {
|
||||
await release();
|
||||
} catch (error) {
|
||||
errors.push(error.message);
|
||||
console.error(`[PortLockManager] Error releasing lock:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
this.activeLocks.delete(lockId);
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.warn(`[PortLockManager] Released locks with ${errors.length} errors`);
|
||||
} else {
|
||||
console.log(`[PortLockManager] Successfully released all locks (ID: ${lockId})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up stale lock files
|
||||
* Removes locks older than LOCK_STALE_THRESHOLD
|
||||
*/
|
||||
async cleanupStaleLocks() {
|
||||
console.log('[PortLockManager] Cleaning up stale locks...');
|
||||
|
||||
this.ensureLockDirectory();
|
||||
|
||||
let cleaned = 0;
|
||||
let errors = 0;
|
||||
|
||||
try {
|
||||
const files = fs.readdirSync(LOCK_DIR);
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.endsWith('.lock')) continue;
|
||||
|
||||
const lockFilePath = path.join(LOCK_DIR, file);
|
||||
|
||||
try {
|
||||
// Check if lock is stale using proper-lockfile's built-in check
|
||||
const isLocked = await lockfile.check(lockFilePath, { realpath: false, stale: LOCK_STALE_THRESHOLD });
|
||||
|
||||
if (!isLocked) {
|
||||
// Lock is stale or not locked, safe to remove
|
||||
fs.unlinkSync(lockFilePath);
|
||||
cleaned++;
|
||||
console.log(`[PortLockManager] Removed stale lock: ${file}`);
|
||||
}
|
||||
} catch (error) {
|
||||
// File might not exist or might have been removed by another process
|
||||
if (error.code !== 'ENOENT') {
|
||||
errors++;
|
||||
console.warn(`[PortLockManager] Error checking lock ${file}:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[PortLockManager] Cleanup complete: ${cleaned} stale locks removed, ${errors} errors`);
|
||||
} catch (error) {
|
||||
console.error('[PortLockManager] Error during cleanup:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current lock status
|
||||
*/
|
||||
getStatus() {
|
||||
const activeLocks = Array.from(this.activeLocks.entries()).map(([lockId, info]) => ({
|
||||
lockId,
|
||||
ports: info.ports,
|
||||
age: Date.now() - info.timestamp,
|
||||
timestamp: new Date(info.timestamp).toISOString()
|
||||
}));
|
||||
|
||||
return {
|
||||
activeLocks: activeLocks.length,
|
||||
locks: activeLocks,
|
||||
lockDirectory: LOCK_DIR
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a port is currently locked
|
||||
* @param {string} port - Port number as string
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async isPortLocked(port) {
|
||||
const lockFilePath = this.getLockFilePath(port);
|
||||
|
||||
if (!fs.existsSync(lockFilePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return await lockfile.check(lockFilePath, { realpath: false, stale: LOCK_STALE_THRESHOLD });
|
||||
} catch (error) {
|
||||
// If we can't check, assume it's not locked
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
const portLockManager = new PortLockManager();
|
||||
|
||||
module.exports = portLockManager;
|
||||
Reference in New Issue
Block a user