Files
compiler-explorer/test/compilation-queue-tests.ts
Matt Godbolt (bot acct) 19ae5abcf3 Count queue jobs as completed when they settle, not when dequeued (#8821)
The last loose end from the incident issue —
`ce_compilation_queue_completed_total` incremented in a `finally` that
ran as soon as the job *returned its promise*, so it counted dequeues,
in lockstep with `ce_compilation_queue_dequeued_total`, and
`status().running` was almost always 0.

Completion is now counted when the job settles (fulfilled or rejected),
via a settlement callback rather than by awaiting in the wrapper. The
non-awaiting detail matters: my first attempt awaited `job()` so the
`finally` ran at settlement — and the existing "times out a job that
never settles" test immediately caught that this keeps `_running`
populated forever for a wedged job, reintroducing the exact
`busy`-forever wedge #8813 fixed. (A nice demonstration of that test
paying for itself.) With the callback approach, a never-settling job
correctly never counts as completed, so `dequeued − completed` now
exposes wedged/in-flight jobs — which would have made the original
incident visible directly in Grafana.

New test pins the semantics: counter unchanged while a job is running,
+1 once it settles.

Per discussion: no temp-dir sweeps of any kind (instances are replaced,
never restarted; and multiple CE processes may share a machine), so the
orphaned-dirs observation in #8811 is closed as won't-fix.

Closes #8811.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: mattgodbolt-molty <mattgodbolt-molty@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 23:02:35 +01:00

88 lines
4.0 KiB
TypeScript

// Copyright (c) 2026, Compiler Explorer Authors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
import {TimeoutError} from 'p-queue';
import PromClient from 'prom-client';
import {describe, expect, it} from 'vitest';
import {CompilationQueue} from '../lib/compilation-queue.js';
async function completedCount(): Promise<number> {
const counter = PromClient.register.getSingleMetric('ce_compilation_queue_completed_total');
const metric = await (counter as PromClient.Counter).get();
return metric.values[0].value;
}
describe('CompilationQueue', () => {
it('runs an enqueued job and returns its result', async () => {
const queue = new CompilationQueue(1, 1000, 1000);
await expect(queue.enqueue(async () => 42)).resolves.toEqual(42);
expect(queue.status().busy).toBe(false);
});
it('does not deadlock when a job enqueues another job', async () => {
const queue = new CompilationQueue(1, 1000, 1000);
const result = await queue.enqueue(() => {
return queue.enqueue(async () => 42);
});
expect(result).toEqual(42);
});
it('times out a job that never settles, freeing the queue', async () => {
// A job whose promise never settles (e.g. a download severed mid-stream, see #8811)
// must not occupy a queue slot forever: the queue timeout has to reject it so the
// queue can go back to being non-busy (which gates temp dir cleanup).
const queue = new CompilationQueue(1, 100, 1000);
const wedged = queue.enqueue(() => new Promise(() => {}));
await expect(wedged).rejects.toThrow(TimeoutError);
await expect(queue.enqueue(async () => 'still works')).resolves.toEqual('still works');
expect(queue.status().busy).toBe(false);
// Generous test budget: the 100ms queue timeout can fire very late when vitest workers
// saturate the machine (e.g. during pre-commit runs).
}, 15_000);
it('counts a job as completed when it settles, not when it starts', async () => {
const queue = new CompilationQueue(1, 1000, 1000);
const before = await completedCount();
let release: () => void = () => {};
const gate = new Promise<void>(resolve => {
release = resolve;
});
let started: () => void = () => {};
const startedGate = new Promise<void>(resolve => {
started = resolve;
});
const jobPromise = queue.enqueue(() => {
started();
return gate;
});
await startedGate;
expect(await completedCount()).toEqual(before);
release();
await jobPromise;
expect(await completedCount()).toEqual(before + 1);
});
});