fix: preserve removal intent when Redis enqueue fails

This commit is contained in:
tdurieux
2026-09-06 09:16:17 +02:00
parent 12b0bb52c0
commit f5f44c2511
3 changed files with 42 additions and 9 deletions
+1 -1
View File
@@ -203,7 +203,7 @@ export async function recoverStuckRemoving() {
...serializeError(e), ...serializeError(e),
repoId: doc.repoId, repoId: doc.repoId,
}); });
await markErrorIfRemoving(doc.repoId, "removal_interrupted"); // Keep REMOVING so another recovery pass can retry the enqueue.
} }
} }
} catch (e) { } catch (e) {
+3 -8
View File
@@ -241,14 +241,9 @@ router.delete(
const user = await getUser(req); const user = await getUser(req);
isOwnerOrAdmin([repo.owner.id], user); isOwnerOrAdmin([repo.owner.id], user);
await repo.updateStatus(RepositoryStatus.REMOVING); await repo.updateStatus(RepositoryStatus.REMOVING);
try { // Keep removal intent durable if Redis is unavailable. Recovery can
await addRemovalJob(repo.repoId); // enqueue it later, and public requests must remain blocked meanwhile.
} catch (error) { await addRemovalJob(repo.repoId);
const message =
error instanceof Error ? error.message : "removal_enqueue_failed";
await repo.updateStatus(RepositoryStatus.ERROR, message);
throw error;
}
return res.json({ status: repo.status }); return res.json({ status: repo.status });
} catch (error) { } catch (error) {
handleError(error, res, req); handleError(error, res, req);
+38
View File
@@ -110,6 +110,44 @@ describe("removal recovery", function () {
expect(added[1]).to.deep.equal({ repoId: repo.repoId }); expect(added[1]).to.deep.equal({ repoId: repo.repoId });
}); });
} }
it("preserves deletion across a failed request enqueue and recovery enqueue", async function () {
const repo = new Repository(new AnonymizedRepositoryModel({
repoId: "repo-1", status: "ready",
owner: "507f1f77bcf86cd799439011",
options: { expirationMode: "never" }, source: {},
}));
routeUtils.getRepo = async () => repo;
routeUtils.getUser = async () => ({ model: { id: "admin" }, isAdmin: true });
let responseError;
routeUtils.handleError = (error) => { responseError = error; };
const failure = new Error("Redis unavailable");
queueModule.removeQueue = {
getJobs: async () => [],
getJob: async () => undefined,
add: async () => { throw failure; },
};
const router = require("../src/server/routes/repository-private").default;
const handler = router.stack.find((layer) =>
layer.route?.path === "/:repoId/" && layer.route.methods.delete
).route.stack[0].handle;
await handler({ params: { repoId: repo.repoId } }, {});
expect(responseError).to.equal(failure);
expect(repo.status).to.equal("removing");
let publicError;
try { await repo.check(); } catch (error) { publicError = error; }
expect(publicError.message).to.equal("repository_expired");
AnonymizedRepositoryModel.find = (filter) => ({
lean: async () => repo.status === filter.status ? [repo.model] : [],
});
await queueModule.recoverStuckRemoving();
expect(repo.status).to.equal("removing");
let added;
queueModule.removeQueue.add = async (...args) => { added = args; };
await queueModule.recoverStuckRemoving();
expect(added[1]).to.deep.equal({ repoId: repo.repoId });
});
}); });
describe("conference edits", function () { describe("conference edits", function () {