mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2026-07-16 17:26:58 -04:00
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>
88 lines
4.0 KiB
TypeScript
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);
|
|
});
|
|
});
|