mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-05 02:43:32 +00:00
Merge remote-tracking branch 'origin/main' into feat/mock-provider
This commit is contained in:
commit
4315658427
5 changed files with 74 additions and 8 deletions
6
gitnexus-web/package-lock.json
generated
6
gitnexus-web/package-lock.json
generated
|
|
@ -5077,9 +5077,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/joi": {
|
||||
"version": "18.2.3",
|
||||
"resolved": "https://registry.npmjs.org/joi/-/joi-18.2.3.tgz",
|
||||
"integrity": "sha512-N5A3KTWQpPWT4ExxxPlUx7WmykGXRzhNidWhV41d6Abu9YfI2NyWCJuxdPnslJCPWtbRpSVOWSnSS6GakLM/Rg==",
|
||||
"version": "18.2.8",
|
||||
"resolved": "https://registry.npmjs.org/joi/-/joi-18.2.8.tgz",
|
||||
"integrity": "sha512-G2TX62h58ZHuwqetJgP2F4ualakqAmZtBYe3jWen7gxQRw5xApX6crnFtuB91WC0c3ESBnva+kGSnb3+6pIQDQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import path from 'node:path';
|
|||
import { execFileSync } from 'node:child_process';
|
||||
import { acquireFileLock, FileLockBusyError } from '../../storage/file-lock.js';
|
||||
import { getGlobalDir } from '../../storage/repo-manager.js';
|
||||
import { isProcessAlive, readProcessStartTime } from '../../utils/process-identity.js';
|
||||
import { isProcessAlive, readProcessStartTimeCached } from '../../utils/process-identity.js';
|
||||
import { loadAutoSyncConfig } from './config.js';
|
||||
import { runAutoSyncOnce } from './runner.js';
|
||||
import { getAutoSyncMutexPath, getAutoSyncWatchDir } from './state.js';
|
||||
|
|
@ -632,7 +632,7 @@ function resolveWatchDeps(deps: Partial<AutoSyncWatchControlDeps> = {}): AutoSyn
|
|||
return undefined;
|
||||
}
|
||||
}),
|
||||
readProcessStartTime: deps.readProcessStartTime ?? readProcessStartTime,
|
||||
readProcessStartTime: deps.readProcessStartTime ?? readProcessStartTimeCached,
|
||||
sleep:
|
||||
deps.sleep ??
|
||||
((ms) =>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import fs from 'node:fs/promises';
|
|||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { setTimeout as sleep } from 'node:timers/promises';
|
||||
import { isProcessAlive, readProcessStartTime } from '../utils/process-identity.js';
|
||||
import { isProcessAlive, readProcessStartTimeCached } from '../utils/process-identity.js';
|
||||
|
||||
const HOSTNAME = os.hostname();
|
||||
|
||||
|
|
@ -46,7 +46,9 @@ export async function acquireFileLock(
|
|||
pid,
|
||||
ownerId: crypto.randomUUID(),
|
||||
processStartTime:
|
||||
options.processStartTime ?? (options.readProcessStartTime ?? readProcessStartTime)(pid) ?? '',
|
||||
options.processStartTime ??
|
||||
(options.readProcessStartTime ?? readProcessStartTimeCached)(pid) ??
|
||||
'',
|
||||
hostname: options.hostname ?? HOSTNAME,
|
||||
};
|
||||
if (!owner.processStartTime) {
|
||||
|
|
@ -69,7 +71,7 @@ export async function acquireFileLock(
|
|||
resolvedPath,
|
||||
owner,
|
||||
options.isProcessAlive ?? isProcessAlive,
|
||||
options.readProcessStartTime ?? readProcessStartTime,
|
||||
options.readProcessStartTime ?? readProcessStartTimeCached,
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -38,3 +38,22 @@ export function readProcessStartTime(pid: number): string | undefined {
|
|||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
let ownStartTime: string | undefined;
|
||||
|
||||
/**
|
||||
* `readProcessStartTime`, except this process's own start time is probed once.
|
||||
* It cannot change while we are running, and every `acquireFileLock` — plus
|
||||
* each retry attempt and each stale-lock reclaim guard — stamps the owner file
|
||||
* with it. On Windows that probe is a `powershell.exe` spawn and a WMI query,
|
||||
* so a process taking several locks pays it several times for one constant.
|
||||
*
|
||||
* A foreign pid is never cached: that process can exit and its pid can be
|
||||
* reused, which is the very thing the stamp exists to detect. A failed probe
|
||||
* is not cached either — one transient failure would otherwise leave the
|
||||
* process unable to take a lock for its whole lifetime.
|
||||
*/
|
||||
export function readProcessStartTimeCached(pid: number): string | undefined {
|
||||
if (pid !== process.pid) return readProcessStartTime(pid);
|
||||
return (ownStartTime ??= readProcessStartTime(pid));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,24 @@ import { isProcessAlive, readProcessStartTime } from '../../src/utils/process-id
|
|||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.doUnmock('node:child_process');
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
/**
|
||||
* Loads a fresh copy of the module (fresh memo) over a counted `execFileSync`,
|
||||
* so "how many times did we actually shell out" is observable. `doMock` is not
|
||||
* hoisted, so the statically imported functions used by the other tests keep
|
||||
* the real implementation.
|
||||
*/
|
||||
async function withCountedProbe(probe: () => string) {
|
||||
const execFileSync = vi.fn(probe);
|
||||
vi.doMock('node:child_process', () => ({ execFileSync }));
|
||||
vi.resetModules();
|
||||
const identity = await import('../../src/utils/process-identity.js');
|
||||
return { execFileSync, readProcessStartTimeCached: identity.readProcessStartTimeCached };
|
||||
}
|
||||
|
||||
describe('process identity', () => {
|
||||
it('treats only ESRCH as a dead process', () => {
|
||||
const kill = vi.spyOn(process, 'kill');
|
||||
|
|
@ -39,4 +55,33 @@ describe('process identity', () => {
|
|||
}
|
||||
},
|
||||
);
|
||||
|
||||
it('probes this process once and re-probes a foreign pid every time', async () => {
|
||||
const { execFileSync, readProcessStartTimeCached } = await withCountedProbe(() => 'STAMP\n');
|
||||
|
||||
expect(readProcessStartTimeCached(process.pid)).toBe('STAMP');
|
||||
expect(readProcessStartTimeCached(process.pid)).toBe('STAMP');
|
||||
// On Windows each probe is a powershell.exe spawn plus a WMI query.
|
||||
expect(execFileSync).toHaveBeenCalledTimes(1);
|
||||
|
||||
// A foreign process can exit and its pid be reused — caching that stamp
|
||||
// would blind the reuse check the stamp exists for.
|
||||
expect(readProcessStartTimeCached(process.pid + 1)).toBe('STAMP');
|
||||
expect(readProcessStartTimeCached(process.pid + 1)).toBe('STAMP');
|
||||
expect(execFileSync).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('retries after a failed self probe instead of caching the failure', async () => {
|
||||
const { execFileSync, readProcessStartTimeCached } = await withCountedProbe(() => 'STAMP\n');
|
||||
execFileSync.mockImplementationOnce(() => {
|
||||
throw new Error('probe unavailable');
|
||||
});
|
||||
|
||||
// A cached failure would leave acquireFileLock throwing "Unable to
|
||||
// determine process start time" for the rest of the process's life.
|
||||
expect(readProcessStartTimeCached(process.pid)).toBeUndefined();
|
||||
expect(readProcessStartTimeCached(process.pid)).toBe('STAMP');
|
||||
expect(readProcessStartTimeCached(process.pid)).toBe('STAMP');
|
||||
expect(execFileSync).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue