feat: schedule pricing job retention cleanup
Quality / test (push) Canceled after 0s

Schedule configurable completed pricing-job cleanup in the worker, broaden repository cleanup across sellers, add unit and integration coverage, and update pricing operational documentation.
This commit is contained in:
2026-09-09 22:32:04 -04:00
parent 660690bf34
commit 76e40d7289
8 changed files with 126 additions and 14 deletions
+17
View File
@@ -1,3 +1,20 @@
export function getPricingJobRetentionCutoff({ now = new Date(), retentionDays }) {
if (!Number.isInteger(retentionDays) || retentionDays < 1)
throw new Error("Pricing job retention days must be a positive integer.");
const cutoff = new Date(now);
cutoff.setUTCDate(cutoff.getUTCDate() - retentionDays);
return cutoff;
}
export async function purgeCompletedPricingRefreshJobs({
jobRepository,
now,
retentionDays,
}) {
const cutoff = getPricingJobRetentionCutoff({ now, retentionDays });
return jobRepository.purgeCompletedBefore(cutoff);
}
export async function processNextPersistentPricingRefreshJob({
jobRepository,
pricingRefreshJob,
+24
View File
@@ -22,6 +22,7 @@ import { createHttpPricingProvider } from "../../api/src/modules/pricing/provide
import { processNextPersistentImageDerivativeJob } from "./image-derivative-worker.js";
import {
processNextPersistentPricingRefreshJob,
purgeCompletedPricingRefreshJobs,
runPricingRefreshJob,
} from "./pricing-refresh-worker.js";
@@ -29,6 +30,14 @@ const pollIntervalMs = Number.parseInt(
process.env.WORKER_POLL_INTERVAL_MS ?? "2000",
10,
);
const pricingJobRetentionDays = Number.parseInt(
process.env.PRICING_JOB_RETENTION_DAYS ?? "30",
10,
);
const pricingJobCleanupIntervalMs = Number.parseInt(
process.env.PRICING_JOB_CLEANUP_INTERVAL_MS ?? "3600000",
10,
);
async function runWorker() {
const imageStorageRoot = resolveProjectImageStorageRoot(
@@ -63,6 +72,15 @@ async function runWorker() {
renderer: createSharpImageRenderer(),
});
async function cleanupPricingJobs() {
const deletedCount = await purgeCompletedPricingRefreshJobs({
jobRepository: pricingRefreshJobRepository,
retentionDays: pricingJobRetentionDays,
});
if (deletedCount > 0)
console.log(`Purged ${deletedCount} completed pricing refresh job(s).`);
}
async function processOne() {
const result = await processNextPersistentImageDerivativeJob({
jobRepository,
@@ -94,13 +112,19 @@ async function runWorker() {
}
await processOne();
await cleanupPricingJobs();
const timer = setInterval(
() => processOne().catch((error) => console.error(error)),
pollIntervalMs,
);
const cleanupTimer = setInterval(
() => cleanupPricingJobs().catch((error) => console.error(error)),
pricingJobCleanupIntervalMs,
);
const shutdown = async () => {
clearInterval(timer);
clearInterval(cleanupTimer);
await database.close();
};
process.once("SIGINT", shutdown);
@@ -6,10 +6,40 @@ import {
createPricingProviderRegistry,
} from "../../api/src/modules/pricing/providers/pricing-provider.js";
import {
getPricingJobRetentionCutoff,
processNextPersistentPricingRefreshJob,
purgeCompletedPricingRefreshJobs,
runPricingRefreshJob,
} from "../src/pricing-refresh-worker.js";
test("calculates a UTC retention cutoff and purges completed jobs before it", async () => {
const now = new Date("2026-09-09T12:00:00.000Z");
const cutoff = getPricingJobRetentionCutoff({ now, retentionDays: 30 });
assert.equal(cutoff.toISOString(), "2026-08-10T12:00:00.000Z");
let receivedCutoff;
const deleted = await purgeCompletedPricingRefreshJobs({
jobRepository: {
async purgeCompletedBefore(value) {
receivedCutoff = value;
return 3;
},
},
now,
retentionDays: 30,
});
assert.equal(deleted, 3);
assert.equal(receivedCutoff.toISOString(), cutoff.toISOString());
});
test("rejects invalid pricing job retention days", () => {
assert.throws(
() => getPricingJobRetentionCutoff({ retentionDays: 0 }),
/positive integer/,
);
});
test("completes a pricing refresh through the provider and pricing service boundaries", async () => {
const providerRegistry = createPricingProviderRegistry([
createPricingProvider({