mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-01 02:01:24 +00:00
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
* perf(lock): probe this process's own start time once `acquireFileLock` stamps the owner file with the acquiring process's start time so a later reclaimer can tell a live owner from pid reuse. That value cannot change while we are running, but it was re-probed on every acquisition — and on Windows the probe is a `powershell.exe` spawn plus a `Get-CimInstance Win32_Process` WMI query, which is the single most expensive step in taking an uncontended lock. Add `readProcessStartTimeCached` and make it the default reader in `acquireFileLock` and `resolveWatchDeps`. Only this process's own pid is cached: - A foreign pid is always re-probed. That process can exit and its pid be reused, which is precisely what the stamp exists to detect. - A failed probe is not cached. `acquireFileLock` throws when the start time is empty, so caching one transient failure would leave the process unable to take a lock for the rest of its life. `readProcessStartTime` itself is unchanged and still probes every call, so the existing timezone-pinning regression test keeps exercising the real `ps` invocation instead of passing off a cached value. Behavior is otherwise identical: same probe, same string, same stamp. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(autofix): apply prettier + eslint fixes via /autofix command --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
87 lines
3.4 KiB
TypeScript
87 lines
3.4 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { isProcessAlive, readProcessStartTime } from '../../src/utils/process-identity.js';
|
|
|
|
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');
|
|
kill.mockImplementationOnce(() => {
|
|
throw Object.assign(new Error('missing'), { code: 'ESRCH' });
|
|
});
|
|
kill.mockImplementationOnce(() => {
|
|
throw Object.assign(new Error('not permitted'), { code: 'EPERM' });
|
|
});
|
|
|
|
expect(isProcessAlive(111)).toBe(false);
|
|
expect(isProcessAlive(222)).toBe(true);
|
|
});
|
|
|
|
it.skipIf(process.platform === 'win32')(
|
|
'renders the same start time regardless of the ambient timezone',
|
|
() => {
|
|
const original = process.env.TZ;
|
|
try {
|
|
process.env.TZ = 'UTC';
|
|
const utc = readProcessStartTime(process.pid);
|
|
process.env.TZ = 'Asia/Tokyo';
|
|
const tokyo = readProcessStartTime(process.pid);
|
|
|
|
expect(utc).toBeTruthy();
|
|
// A locale/timezone-dependent identity makes a live lock look reused.
|
|
expect(tokyo).toBe(utc);
|
|
} finally {
|
|
if (original === undefined) delete process.env.TZ;
|
|
else process.env.TZ = original;
|
|
}
|
|
},
|
|
);
|
|
|
|
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);
|
|
});
|
|
});
|