Files
paseo/packages/server/src/utils/git-process-scheduler.test.ts
T
Mohamed Boudra 7d093967ef fix(server): pick the git job when its start token fires
Background jobs that already reserved a start-rate token no longer start
ahead of normal work submitted after them.
2026-11-10 02:58:52 +01:00

230 lines
7.6 KiB
TypeScript

import { setImmediate as scheduleImmediate } from "node:timers";
import { afterEach, describe, expect, test, vi } from "vitest";
import { GitProcessScheduler, resolveGitProcessPolicy } from "./git-process-scheduler.js";
function createConcurrencyTask(
index: number,
started: number[],
releases: Array<() => void>,
): () => { result: Promise<void>; exited: Promise<void> } {
return () => {
started.push(index);
if (index >= 2) {
const completed = Promise.resolve();
return { result: completed, exited: completed };
}
const completed = new Promise<void>((resolve) => {
releases.push(resolve);
});
return { result: completed, exited: completed };
};
}
describe("GitProcessScheduler", () => {
afterEach(() => {
vi.useRealTimers();
});
test("limits unresolved processes independently of their start rate", async () => {
const scheduler = new GitProcessScheduler({
maxProcessesPerSecond: 100,
maxProcessConcurrency: 2,
});
const releases: Array<() => void> = [];
const started: number[] = [];
const tasks = Array.from({ length: 4 }, (_, index) =>
scheduler.run(createConcurrencyTask(index, started, releases)),
);
await vi.waitFor(() => expect(started).toEqual([0, 1]));
releases.shift()?.();
await vi.waitFor(() => expect(started).toContain(2));
releases.shift()?.();
await Promise.all(tasks);
expect(scheduler.activeCount).toBe(0);
expect(scheduler.pendingCount).toBe(0);
});
test("admits a high-priority user operation before queued observation work", async () => {
const scheduler = new GitProcessScheduler({
maxProcessesPerSecond: 100,
maxProcessConcurrency: 1,
});
const releases: Array<() => void> = [];
const started: number[] = [];
const activeObservation = scheduler.run(createConcurrencyTask(0, started, releases));
await vi.waitFor(() => expect(started).toEqual([0]));
const queuedObservation = scheduler.run(createConcurrencyTask(2, started, releases));
const userOperation = scheduler.run(createConcurrencyTask(3, started, releases), {
priority: "high",
});
releases.shift()?.();
await Promise.all([activeObservation, queuedObservation, userOperation]);
expect(started).toEqual([0, 3, 2]);
});
test("admits normal work before a queued background burst", async () => {
const scheduler = new GitProcessScheduler({
maxProcessesPerSecond: 100,
maxProcessConcurrency: 1,
});
const releases: Array<() => void> = [];
const started: number[] = [];
const activeBackground = scheduler.run(createConcurrencyTask(0, started, releases), {
priority: "background",
});
await vi.waitFor(() => expect(started).toEqual([0]));
const queuedBackground = [2, 3, 4].map((index) =>
scheduler.run(createConcurrencyTask(index, started, releases), { priority: "background" }),
);
const normalWork = scheduler.run(createConcurrencyTask(5, started, releases));
const highWork = scheduler.run(createConcurrencyTask(6, started, releases), {
priority: "high",
});
releases.shift()?.();
await Promise.all([activeBackground, ...queuedBackground, normalWork, highWork]);
expect(started).toEqual([0, 6, 5, 2, 3, 4]);
});
test("starts normal work at the next start slot when background work saturates the rate", async () => {
vi.useFakeTimers();
const scheduler = new GitProcessScheduler({
maxProcessesPerSecond: 2,
maxProcessConcurrency: 8,
});
const started: string[] = [];
const task = (name: string) => () => {
started.push(name);
const completed = Promise.resolve();
return { result: completed, exited: completed };
};
const background = Array.from({ length: 8 }, (_, index) =>
scheduler.run(task(`background-${index}`), { priority: "background" }),
);
// Admission yields a real event-loop turn per process; let every
// background job reach the start-rate limiter.
for (let turn = 0; turn < 10; turn += 1) {
await new Promise<void>((resolve) => scheduleImmediate(resolve));
}
expect(started).toEqual(["background-0", "background-1"]);
const normal = scheduler.run(task("normal"));
while (started.length < 3) {
await vi.advanceTimersToNextTimerAsync();
}
expect(started[2]).toBe("normal");
await vi.runAllTimersAsync();
await Promise.all([...background, normal]);
});
test("yields to daemon liveness work between process starts", async () => {
const scheduler = new GitProcessScheduler({
maxProcessesPerSecond: 100,
maxProcessConcurrency: 8,
});
const started: number[] = [];
const start = () => {
started.push(started.length);
const completed = Promise.resolve();
return { result: completed, exited: completed };
};
const livenessTurn = new Promise<void>((resolve) => setImmediate(resolve));
const tasks = Array.from({ length: 8 }, () => scheduler.run(start));
await livenessTurn;
expect(started).toHaveLength(1);
await Promise.all(tasks);
});
test("starts an isolated process without an extra event-loop turn", async () => {
const scheduler = new GitProcessScheduler({
maxProcessesPerSecond: 100,
maxProcessConcurrency: 8,
});
let started = false;
const result = scheduler.run(() => {
started = true;
const completed = Promise.resolve();
return { result: completed, exited: completed };
});
expect(started).toBe(true);
await result;
});
test("queued processes keep moving while application timers are faked", async () => {
vi.useFakeTimers();
const scheduler = new GitProcessScheduler({
maxProcessesPerSecond: 100,
maxProcessConcurrency: 1,
});
const releases: Array<() => void> = [];
const started: number[] = [];
const first = scheduler.run(createConcurrencyTask(0, started, releases));
const second = scheduler.run(createConcurrencyTask(2, started, releases));
expect(started).toEqual([0]);
releases.shift()?.();
await Promise.resolve();
await new Promise<void>((resolve) => scheduleImmediate(resolve));
expect(started).toEqual([0, 2]);
await Promise.all([first, second]);
});
test("limits process starts in each strict one-second interval", async () => {
vi.useFakeTimers();
const scheduler = new GitProcessScheduler({
maxProcessesPerSecond: 2,
maxProcessConcurrency: 4,
});
const startedAt: number[] = [];
const recordStart = () => {
startedAt.push(Date.now());
const completed = Promise.resolve();
return { result: completed, exited: completed };
};
const tasks = Array.from({ length: 4 }, () => scheduler.run(recordStart));
await vi.advanceTimersByTimeAsync(999);
expect(startedAt.length).toBeLessThanOrEqual(2);
while (startedAt.length < 3) {
await vi.advanceTimersToNextTimerAsync();
}
while (startedAt.length < 4) {
await vi.advanceTimersToNextTimerAsync();
}
await Promise.all(tasks);
expect(startedAt).toHaveLength(4);
expect(startedAt[2]! - startedAt[0]!).toBeGreaterThanOrEqual(1_000);
});
});
describe("resolveGitProcessPolicy", () => {
test("prefers renamed environment variables over legacy and persisted values", () => {
expect(
resolveGitProcessPolicy({
env: {
PASEO_GIT_MAX_PROCESSES_PER_SECOND: "12",
PASEO_GIT_MAX_PROCESS_CONCURRENCY: "7",
PASEO_GIT_CONCURRENCY: "3",
},
persisted: { maxProcessesPerSecond: 5, maxProcessConcurrency: 4 },
}),
).toEqual({ maxProcessesPerSecond: 12, maxProcessConcurrency: 7 });
});
});