Skip to main content
Glama
lock-file.test.js11.3 kB
/** * Integration tests for lock file management * Tests multi-instance lock handling and race condition prevention */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { createLockFileMock, createEmailFileSystemMock } from '../helpers/indexing-mocks.js' import path from 'path' describe('Lock File Management', () => { const LOCK_FILE = path.join('/Users/test', '.apple-tools-mcp', 'indexer.lock') let lockMock let fsMock beforeEach(() => { vi.clearAllMocks() lockMock = createLockFileMock() fsMock = createEmailFileSystemMock() }) afterEach(() => { vi.restoreAllMocks() lockMock.reset() }) describe('lock acquisition', () => { it('should create lock file with PID on acquire', () => { const pid = process.pid lockMock.acquire(pid) expect(lockMock.isLocked()).toBe(true) expect(lockMock.getHolder()).toBe(pid) }) it('should write current process PID to file', () => { fsMock.writeFileSync(LOCK_FILE, String(process.pid), { flag: 'wx' }) expect(fsMock.writeFileSync).toHaveBeenCalledWith( LOCK_FILE, String(process.pid), { flag: 'wx' } ) }) }) describe('re-entrant locking', () => { it('should return true if we already hold the lock', () => { const pid = process.pid // First acquire const first = lockMock.acquire(pid) expect(first).toBe(true) // Second acquire by same process const second = lockMock.acquire(pid) expect(second).toBe(true) expect(lockMock.getHolder()).toBe(pid) }) }) describe('stale lock detection', () => { it('should remove stale lock from dead process', () => { // Simulate stale lock from non-existent process const deadPid = 99999999 // Very unlikely to exist // In real implementation: // 1. Check if lock file exists // 2. Read PID from file // 3. Check if process exists with process.kill(pid, 0) // 4. If process doesn't exist (throws), remove stale lock // Simulate the behavior let lockRemoved = false const checkAndRemoveStale = (existingPid) => { try { // This would throw if process doesn't exist // process.kill(existingPid, 0) if (existingPid === deadPid) { throw new Error('ESRCH') // No such process } } catch { lockRemoved = true } } checkAndRemoveStale(deadPid) expect(lockRemoved).toBe(true) }) it('should successfully acquire after removing stale lock', () => { // Start with "stale" lock lockMock.acquire(99999) // Simulate stale lock removal lockMock.release(99999) // Now our process can acquire const acquired = lockMock.acquire(process.pid) expect(acquired).toBe(true) expect(lockMock.getHolder()).toBe(process.pid) }) }) describe('live process handling', () => { it('should fail if another process holds lock', () => { const otherPid = process.ppid || 1 // Parent PID or init // Other process holds lock lockMock.acquire(otherPid) // Our process tries to acquire const acquired = lockMock.acquire(process.pid) expect(acquired).toBe(false) expect(lockMock.getHolder()).toBe(otherPid) }) it('should not overwrite lock held by live process', () => { const otherPid = process.ppid || 1 lockMock.acquire(otherPid) lockMock.acquire(process.pid) // Should fail expect(lockMock.getHolder()).toBe(otherPid) }) }) describe('atomic file operations', () => { it('should use atomic wx flag to prevent race condition', () => { // The 'wx' flag ensures: // - File is created exclusively // - Fails with EEXIST if file already exists // - Prevents TOCTOU (time-of-check-time-of-use) race const flag = 'wx' fsMock.writeFileSync(LOCK_FILE, '12345', { flag }) expect(fsMock.writeFileSync).toHaveBeenCalledWith( LOCK_FILE, '12345', { flag: 'wx' } ) }) it('should handle EEXIST error from race condition', () => { let raceDetected = false // Simulate another process winning the race fsMock.writeFileSync.mockImplementation((path, content, options) => { if (options?.flag === 'wx') { const error = new Error('EEXIST: file already exists') error.code = 'EEXIST' throw error } }) try { fsMock.writeFileSync(LOCK_FILE, String(process.pid), { flag: 'wx' }) } catch (err) { if (err.code === 'EEXIST') { raceDetected = true } } expect(raceDetected).toBe(true) }) }) describe('lock release', () => { it('should release lock only if we own it', () => { const myPid = process.pid lockMock.acquire(myPid) expect(lockMock.isLocked()).toBe(true) const released = lockMock.release(myPid) expect(released).toBe(true) expect(lockMock.isLocked()).toBe(false) }) it('should not release lock owned by another process', () => { const otherPid = 12345 lockMock.acquire(otherPid) const released = lockMock.release(process.pid) expect(released).toBe(false) expect(lockMock.isLocked()).toBe(true) expect(lockMock.getHolder()).toBe(otherPid) }) it('should delete lock file on release', () => { fsMock.existsSync.mockReturnValue(true) fsMock.readFileSync.mockReturnValue(String(process.pid)) // Simulate release behavior const filePid = parseInt(fsMock.readFileSync(LOCK_FILE)) if (filePid === process.pid) { fsMock.unlinkSync(LOCK_FILE) } expect(fsMock.unlinkSync).toHaveBeenCalledWith(LOCK_FILE) }) }) describe('signal handlers', () => { it('should clean up lock on process exit signals', () => { const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'] signals.forEach(signal => { expect(signals).toContain(signal) }) // Lock should be released on any of these signals lockMock.acquire(process.pid) // Simulate signal handler lockMock.release(process.pid) expect(lockMock.isLocked()).toBe(false) }) it('should release lock when stdin closes', () => { // stdin.on('close') should trigger lock release lockMock.acquire(process.pid) // Simulate client disconnect lockMock.release(process.pid) expect(lockMock.isLocked()).toBe(false) }) }) describe('directory creation', () => { it('should create lock directory if not exists', () => { fsMock.existsSync.mockReturnValue(false) const lockDir = path.dirname(LOCK_FILE) if (!fsMock.existsSync(lockDir)) { fsMock.mkdirSync(lockDir, { recursive: true }) } expect(fsMock.mkdirSync).toHaveBeenCalledWith(lockDir, { recursive: true }) }) }) describe('error handling', () => { it('should fail safe on lock error', () => { fsMock.writeFileSync.mockImplementation(() => { throw new Error('Permission denied') }) let acquired = true try { fsMock.writeFileSync(LOCK_FILE, String(process.pid), { flag: 'wx' }) } catch { acquired = false } // Should fail safe - don't proceed without lock expect(acquired).toBe(false) }) }) describe('lock timeout handling', () => { const LOCK_TIMEOUT_MS = 30 * 60 * 1000 // 30 minutes it('should define lock timeout as 30 minutes', () => { expect(LOCK_TIMEOUT_MS).toBe(30 * 60 * 1000) expect(LOCK_TIMEOUT_MS).toBe(1800000) // 1,800,000 ms = 30 minutes }) it('should detect stale lock after 30 minutes', () => { const now = Date.now() const lockData = { pid: 99999, timestamp: now - (35 * 60 * 1000) // 35 minutes ago } const lockAge = now - lockData.timestamp const isStale = lockAge > LOCK_TIMEOUT_MS expect(isStale).toBe(true) expect(lockAge).toBeGreaterThan(LOCK_TIMEOUT_MS) }) it('should NOT detect fresh lock as stale', () => { const now = Date.now() const lockData = { pid: 99999, timestamp: now - (5 * 60 * 1000) // 5 minutes ago } const lockAge = now - lockData.timestamp const isStale = lockAge > LOCK_TIMEOUT_MS expect(isStale).toBe(false) expect(lockAge).toBeLessThan(LOCK_TIMEOUT_MS) }) it('should handle lock exactly at 30 minute boundary', () => { const now = Date.now() const lockData = { pid: 99999, timestamp: now - LOCK_TIMEOUT_MS // Exactly 30 minutes } const lockAge = now - lockData.timestamp const isStale = lockAge > LOCK_TIMEOUT_MS // At exactly 30 minutes, should NOT be considered stale (use > not >=) expect(isStale).toBe(false) }) it('should parse lock file with timestamp', () => { const pid = 12345 const timestamp = Date.now() - (40 * 60 * 1000) const lockContent = `${pid}:${timestamp}` const [pidStr, timestampStr] = lockContent.split(':') const parsedPid = parseInt(pidStr) const parsedTimestamp = parseInt(timestampStr) expect(parsedPid).toBe(pid) expect(parsedTimestamp).toBe(timestamp) const lockAge = Date.now() - parsedTimestamp expect(lockAge).toBeGreaterThan(LOCK_TIMEOUT_MS) }) it('should handle missing timestamp gracefully', () => { const lockContent = '12345' // Old format without timestamp const [pidStr, timestampStr] = lockContent.split(':') const pid = parseInt(pidStr) const timestamp = parseInt(timestampStr) || Date.now() expect(pid).toBe(12345) expect(timestamp).toBeGreaterThan(0) // Defaults to now }) it('should log warning for stale lock removal', () => { const now = Date.now() const stalePid = 99999 const staleTimestamp = now - (45 * 60 * 1000) // 45 minutes const lockAge = now - staleTimestamp const shouldWarn = lockAge > LOCK_TIMEOUT_MS if (shouldWarn) { const ageMinutes = Math.round(lockAge / 60000) const message = `Lock file is ${ageMinutes} minutes old. Assuming hung process (PID ${stalePid}). Removing stale lock.` expect(message).toContain('45 minutes') expect(message).toContain(String(stalePid)) } expect(shouldWarn).toBe(true) }) it('should remove stale lock even if process exists', () => { const now = Date.now() const lockData = { pid: process.pid, // Our own PID (definitely exists) timestamp: now - (35 * 60 * 1000) // But 35 minutes old } const lockAge = now - lockData.timestamp let shouldRemove = false // Check if process exists let processExists = true try { process.kill(lockData.pid, 0) // Signal 0 = check existence } catch { processExists = false } // Remove if stale, even if process exists if (lockAge > LOCK_TIMEOUT_MS) { shouldRemove = true } expect(processExists).toBe(true) // Process exists expect(shouldRemove).toBe(true) // But lock is stale }) }) })

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sfls1397/Apple-Tools-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server