fix: preserve file lock acquire errors
This commit is contained in:
parent
a18009cc0f
commit
3b7b5dfd75
2 changed files with 53 additions and 9 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue