fix: fence shared transformations and cancel abandoned cache work

This commit is contained in:
Thomas Durieux committed 2026-11-02 13:58:25 +00:00
1 parent d851fdb381
commit 074a9af315
6 files changed
+118 -10

No files matched your search

+19
View File
@@ -262,6 +262,25 @@ describe("shared content-addressed blob pool", function () {
assert.equal(cache.disk.prepare("SELECT COUNT(*) n FROM reservations").get().n, 0);
} finally { workers.forEach(w => w.kill()); }
});
it("commits completed evictions when pinned readers still prevent admission", async () => {
for (let i = 0; i < 11; i++) await read(await cache.getOrFill("evict-" + i, async () => Readable.from("content-" + i)));
const pinned = await Promise.all(Array.from({ length: 10 }, (_, i) => cache.lookup("evict-" + i)));
let release, started;
const began = new Promise(resolve => { started = resolve; });
const pending = cache.getOrFill("pending", async () => {
started(); await new Promise(resolve => { release = resolve; }); return Readable.from("pending content");
});
try {
await began;
await assert.rejects(cache.getOrFill("denied", async () => Readable.from("new")),
error => error instanceof CacheAdmissionError && error.message === "cache_budget_busy");
assert.equal(cache.stats().blobs, 10); assert.equal(cache.stats().allocatedBytes, 10 * 4096);
assert.equal(cache.db.prepare("SELECT 1 FROM aliases WHERE key='evict-10'").get(), undefined);
for (const row of cache.db.prepare("SELECT * FROM blobs").all())
await fs.access(join(root, "blobs", row.digest.split("-")[1].slice(0, 2), row.digest, row.file));
release(); await read(await pending);
} finally { release?.(); await pending.catch(() => {}); pinned.forEach(stream => stream.destroy()); }
});
it("does not publish failed or hash-invalid fills and releases reservations", async () => {
await assert.rejects(cache.getOrFill("invalid", async () => Readable.from("bad"), async () => { throw Error("invalid_hash"); }), /invalid_hash/);
assert.equal(cache.stats().blobs, 0); assert.equal(cache.stats().reservedBytes, 0);
+58 -1
View File
@@ -3,6 +3,7 @@ const { promises: fs } = require("node:fs");
const { tmpdir } = require("node:os");
const { join } = require("node:path");
const http = require("node:http");
const { setTimeout } = require("node:timers");
const { Readable } = require("node:stream");
const { createHash } = require("node:crypto");
const { gunzipSync } = require("node:zlib");
@@ -47,7 +48,7 @@ describe("shared cache HTTP and telemetry integration", function () {
const handled = await sendSharedContent(input, res, { filePath, repoId: req.params.project,
terms: req.params.mode === "changed" ? ["Public=>Private"] : ["Alice"] });
if (!handled) res.send(await consume(input));
} catch (e) { if (!res.headersSent) res.status(500).end(e.message); else res.destroy(e); }
} catch (e) { if (!res.headersSent) res.status(e.httpStatus || 500).end(e.message); else res.destroy(e); }
});
await new Promise(resolve => { server = app.listen(0, "127.0.0.1", resolve); }); port = server.address().port;
});
@@ -85,6 +86,62 @@ describe("shared cache HTTP and telemetry integration", function () {
assert.equal(calls, 1);
} finally { GitHubStream.prototype.downloadWithFallback = original; }
});
for (const change of ["expired", "removed", "revision"]) it("rejects a large rewrite retired during worker execution: " + change, async () => {
const db = require("../src/server/database"), model = require("../src/core/model/anonymizedRepositories/anonymizedRepositories.model").default;
const pool = require("../src/core/anonymization-pool"), worker = pool.anonymizeOnWorker, find = model.findOne, connected = db.isConnected;
const current = { status: "ready" }; let release, started;
const began = new Promise(resolve => { started = resolve; }), held = new Promise(resolve => { release = resolve; });
body = Buffer.from("Public documentation.\n".repeat(20000));
db.isConnected = true;
model.findOne = () => ({ select() { return this; }, lean() { return this; }, exec: async () => current });
pool.anonymizeOnWorker = async message => {
started(); await held;
await fs.writeFile(message.output, (await fs.readFile(message.input, "utf8")).replaceAll("Public", "Private"));
return true;
};
let pending;
try {
pending = request(port, "/a/changed", "identity", headers); await began;
if (change === "revision") current.contentCacheRevision = "new"; else current.status = change;
release(); const r = await pending;
assert.equal(r.status, 409); assert.equal(r.body.toString(), "repository_changed");
assert.equal(resultBlobCache().stats().blobs, 0); assert.equal(resultBlobCache().stats().decisions, 0);
} finally { release(); await pending?.catch(() => {}); pool.anonymizeOnWorker = worker; model.findOne = find; db.isConnected = connected; }
});
it("rechecks repository lifecycle on a decision hit before sending bytes", async () => {
await request(port, "/a/changed", "identity", headers);
const db = require("../src/server/database"), model = require("../src/core/model/anonymizedRepositories/anonymizedRepositories.model").default;
const results = resultBlobCache(), open = results.open.bind(results), find = model.findOne, connected = db.isConnected;
const current = { status: "ready" }; db.isConnected = true;
model.findOne = () => ({ select() { return this; }, lean() { return this; }, exec: async () => current });
results.open = async (...args) => { const stream = await open(...args); current.status = "removed"; return stream; };
try {
const r = await request(port, "/a/changed", "identity", headers);
assert.equal(r.status, 409); assert.equal(r.body.toString(), "repository_changed");
} finally { results.open = open; model.findOne = find; db.isConnected = connected; }
});
it("cancels an active worker and releases spools when the client disconnects", async () => {
const pool = require("../src/core/anonymization-pool"), worker = pool.anonymizeOnWorker;
const baseline = pool.getAnonymizationPoolStats().spoolBytes;
let started, aborted, rejectWork, client;
const began = new Promise(resolve => { started = resolve; }), cancelled = new Promise(resolve => { aborted = resolve; });
body = Buffer.from("Public documentation.\n".repeat(20000));
pool.anonymizeOnWorker = (_message, _size, signal) => new Promise((_resolve, reject) => {
rejectWork = reject; started();
signal.addEventListener("abort", () => { aborted(); reject(Error("anonymization_cancelled")); }, { once: true });
});
try {
client = http.get({ host: "127.0.0.1", port, path: "/a/changed", headers }); client.on("error", () => {});
await began; assert.ok(pool.getAnonymizationPoolStats().spoolBytes > baseline);
client.destroy();
await Promise.race([cancelled, new Promise((_, reject) => setTimeout(() => reject(Error("worker was not cancelled")), 2000))]);
for (let i = 0; i < 100 && (pool.getAnonymizationPoolStats().spoolBytes !== baseline || resultBlobCache().stats().reservedBytes); i++)
await new Promise(resolve => setTimeout(resolve, 10));
assert.equal(pool.getAnonymizationPoolStats().spoolBytes, baseline);
assert.equal(resultBlobCache().stats().reservedBytes, 0);
assert.equal(resultBlobCache().stats().blobs, 0);
} finally { client?.destroy(); rejectWork?.(Error("test cleanup")); pool.anonymizeOnWorker = worker; }
});
for (const changed of [false, true]) it("preserves sniffed text MIME on initial and cached responses: " + changed, async () => {
for (const encoding of ["gzip", "identity"]) {
const r = await request(port, "/a/" + (changed ? "changed" : "plain") + "?name=data.unknownext", encoding, headers);