Fix concurrency bug leading to multiple fetches of the malware database

This commit is contained in:
Sander Declerck 2026-04-21 09:26:07 +02:00
parent 3e71398430
commit 2930894624
No known key found for this signature in database
2 changed files with 51 additions and 55 deletions

View file

@ -15,8 +15,12 @@ import { getEcoSystem, ECOSYSTEM_PY } from "../config/settings.js";
* @property {function(string, string): boolean} isMalware * @property {function(string, string): boolean} isMalware
*/ */
/** @type {MalwareDatabase | null} */ // Caching the Promise (rather than the resolved database) prevents duplicate fetches. If we cached the resolved
let cachedMalwareDatabase = null; // value, multiple callers could pass the null-check before the first fetch completes (because each `await` yields
// control back to the event loop, allowing other callers to run). Since the Promise assignment is synchronous, all
// concurrent callers see it immediately and share a single fetch.
/** @type {Promise<MalwareDatabase> | null} */
let cachedMalwareDatabasePromise = null;
/** /**
* Normalize package name for comparison. * Normalize package name for comparison.
@ -34,13 +38,9 @@ function normalizePackageName(name) {
return name; return name;
} }
export async function openMalwareDatabase() { export function openMalwareDatabase() {
if (cachedMalwareDatabase) { if (!cachedMalwareDatabasePromise) {
return cachedMalwareDatabase; cachedMalwareDatabasePromise = getMalwareDatabase().then((malwareDatabase) => {
}
const malwareDatabase = await getMalwareDatabase();
/** /**
* @param {string} name * @param {string} name
* @param {string} version * @param {string} version
@ -63,16 +63,16 @@ export async function openMalwareDatabase() {
return packageData.reason; return packageData.reason;
} }
// This implicitly caches the malware database return {
// that's closed over by the getPackageStatus function
cachedMalwareDatabase = {
getPackageStatus, getPackageStatus,
isMalware: (name, version) => { isMalware: (name, version) => {
const status = getPackageStatus(name, version); const status = getPackageStatus(name, version);
return isMalwareStatus(status); return isMalwareStatus(status);
}, },
}; };
return cachedMalwareDatabase; });
}
return cachedMalwareDatabasePromise;
} }
/** /**

View file

@ -16,30 +16,26 @@ import { warnOnceAboutUnavailableDatabase } from "./newPackagesDatabaseWarnings.
*/ */
// Shared per-process cache to avoid rebuilding the same feed-backed database on each request. // Shared per-process cache to avoid rebuilding the same feed-backed database on each request.
/** @type {NewPackagesDatabase | null} */ // Caching the Promise (rather than the resolved database) prevents duplicate fetches. If we cached the resolved
let cachedNewPackagesDatabase = null; // value, multiple callers could pass the null-check before the first fetch completes (because each `await` yields
// control back to the event loop, allowing other callers to run). Since the Promise assignment is synchronous, all
// concurrent callers see it immediately and share a single fetch.
/** @type {Promise<NewPackagesDatabase> | null} */
let cachedNewPackagesDatabasePromise = null;
/** /**
* @returns {Promise<NewPackagesDatabase>} * @returns {Promise<NewPackagesDatabase>}
*/ */
export async function openNewPackagesDatabase() { export function openNewPackagesDatabase() {
if (cachedNewPackagesDatabase) { if (!cachedNewPackagesDatabasePromise) {
return cachedNewPackagesDatabase; cachedNewPackagesDatabasePromise = getNewPackagesList()
} .then((newPackagesList) => buildNewPackagesDatabase(newPackagesList))
.catch((/** @type {any} */ error) => {
/** @type {import("../api/aikido.js").NewPackageEntry[]} */
let newPackagesList;
try {
newPackagesList = await getNewPackagesList();
} catch (/** @type {any} */ error) {
warnOnceAboutUnavailableDatabase(error); warnOnceAboutUnavailableDatabase(error);
cachedNewPackagesDatabase = { isNewlyReleasedPackage: () => false }; return { isNewlyReleasedPackage: () => false };
return cachedNewPackagesDatabase; });
} }
return cachedNewPackagesDatabasePromise;
cachedNewPackagesDatabase = buildNewPackagesDatabase(newPackagesList);
return cachedNewPackagesDatabase;
} }
/** /**