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 {
|
function writeLockFile(lockPath: string): void {
|
||||||
const fd = fs.openSync(lockPath, 'wx');
|
const fd = fs.openSync(lockPath, 'wx');
|
||||||
fs.writeSync(fd, `${process.pid}\n${Date.now()}\n`);
|
let closeError: unknown = null;
|
||||||
fs.closeSync(fd);
|
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 {
|
function tryAcquire(lockPath: string, options: Required<FileLockOptions>): boolean {
|
||||||
|
|
@ -85,19 +100,27 @@ function tryAcquire(lockPath: string, options: Required<FileLockOptions>): boole
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const code = (err as NodeJS.ErrnoException).code;
|
const code = (err as NodeJS.ErrnoException).code;
|
||||||
if (code === 'ENOENT') {
|
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.
|
// first-acquire latency in a fresh dir is unchanged.
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
||||||
writeLockFile(lockPath);
|
writeLockFile(lockPath);
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch (retryError) {
|
||||||
// Lost a race (another process created the dir/lock) or still failing —
|
const retryCode = (retryError as NodeJS.ErrnoException).code;
|
||||||
// fall through to a normal retry on the next loop iteration.
|
if (retryCode === 'ENOENT') {
|
||||||
return false;
|
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)) {
|
if (shouldBreakExistingLock(lockPath, options.staleTimeoutMs)) {
|
||||||
removeLockPath(lockPath);
|
removeLockPath(lockPath);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
|
import { withFileLock } from '@main/services/team/fileLock';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
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', () => {
|
describe('withFileLock', () => {
|
||||||
let tmpDir: string;
|
let tmpDir: string;
|
||||||
|
|
@ -111,4 +112,24 @@ describe('withFileLock', () => {
|
||||||
expect(result).toBe('created');
|
expect(result).toBe('created');
|
||||||
expect(fs.existsSync(`${nested}.lock`)).toBe(false);
|
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