fix: preserve file lock acquire errors

This commit is contained in:
777genius 2026-05-30 17:08:29 +03:00
parent a18009cc0f
commit 3b7b5dfd75
2 changed files with 53 additions and 9 deletions

View file

@ -71,8 +71,23 @@ function removeLockPath(lockPath: string): void {
function writeLockFile(lockPath: string): void {
const fd = fs.openSync(lockPath, 'wx');
fs.writeSync(fd, `${process.pid}\n${Date.now()}\n`);
fs.closeSync(fd);
let closeError: unknown = null;
try {
fs.writeSync(fd, `${process.pid}\n${Date.now()}\n`);
} finally {
try {
fs.closeSync(fd);
} catch (err) {
closeError = err;
}
}
if (closeError) {
throw closeError;
}
}
function isExistingLockError(code: string | undefined): boolean {
return code === 'EEXIST' || code === 'EISDIR';
}
function tryAcquire(lockPath: string, options: Required<FileLockOptions>): boolean {
@ -85,19 +100,27 @@ function tryAcquire(lockPath: string, options: Required<FileLockOptions>): boole
} catch (err) {
const code = (err as NodeJS.ErrnoException).code;
if (code === 'ENOENT') {
// Lock directory missing create it lazily and acquire in the same call, so
// Lock directory missing - create it lazily and acquire in the same call, so
// first-acquire latency in a fresh dir is unchanged.
try {
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
writeLockFile(lockPath);
return true;
} catch {
// Lost a race (another process created the dir/lock) or still failing —
// fall through to a normal retry on the next loop iteration.
return false;
} catch (retryError) {
const retryCode = (retryError as NodeJS.ErrnoException).code;
if (retryCode === 'ENOENT') {
return false;
}
if (isExistingLockError(retryCode)) {
if (shouldBreakExistingLock(lockPath, options.staleTimeoutMs)) {
removeLockPath(lockPath);
}
return false;
}
throw retryError;
}
}
if (code === 'EEXIST' || code === 'EISDIR') {
if (isExistingLockError(code)) {
if (shouldBreakExistingLock(lockPath, options.staleTimeoutMs)) {
removeLockPath(lockPath);
}

View file

@ -1,9 +1,10 @@
import { withFileLock } from '@main/services/team/fileLock';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { withFileLock } from '@main/services/team/fileLock';
const canAssertPosixPermissions = process.platform !== 'win32' && process.getuid?.() !== 0;
describe('withFileLock', () => {
let tmpDir: string;
@ -111,4 +112,24 @@ describe('withFileLock', () => {
expect(result).toBe('created');
expect(fs.existsSync(`${nested}.lock`)).toBe(false);
});
it.skipIf(!canAssertPosixPermissions)(
'rethrows fatal errors while creating missing lock directory',
async () => {
const readonlyDir = path.join(tmpDir, 'readonly');
fs.mkdirSync(readonlyDir, 0o555);
const nested = path.join(readonlyDir, 'missing', 'test.json');
try {
await expect(
withFileLock(nested, async () => 'ok', {
acquireTimeoutMs: 25,
retryIntervalMs: 1,
})
).rejects.toMatchObject({ code: 'EACCES' });
} finally {
fs.chmodSync(readonlyDir, 0o755);
}
}
);
});