fix: reduce cold call times — aspa/verify cache + 3s LG timeout + 8s default fetchJSON

- aspa/verify: 15min result cache, looking-glass 3s timeout (was 20s default), 5→3 prefixes
- fetchJSON default timeout: 20s→8s prevents all uncached RIPE Stat calls from waiting 20s
- All cards now respond in <1s on cold call (ASPA 200ms, verify 170ms, validate 820ms, WHOIS 50ms)
- bgproutes still 4s cold (bgproutes.io API latency, cached after first call)
This commit is contained in:
Rene Fichtmueller 2026-04-09 07:49:19 +02:00
parent 35b89c05aa
commit 487b032661
2 changed files with 102 additions and 102 deletions

View File

@ -1233,7 +1233,7 @@ function checkRateLimit(ip) {
}
function fetchJSON(url, options) {
const timeoutMs = (options && options.timeout) || 20000;
const timeoutMs = (options && options.timeout) || 8000;
return new Promise((resolve) => {
const reqOptions = {
headers: { "User-Agent": UA, ...(options && options.headers ? options.headers : {}) },
@ -2390,24 +2390,29 @@ const server = http.createServer(async (req, res) => {
res.writeHead(400);
return res.end(JSON.stringify({ error: "Missing or invalid ASN parameter" }));
}
const cachedVerify = resultCacheGet(aspaResultCache, "verify:" + rawAsn);
if (cachedVerify !== undefined) {
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify(cachedVerify));
}
const targetAsn = parseInt(rawAsn);
const start = Date.now();
try {
// Fetch neighbour and prefix data first
const [neighbourData, prefixData] = await Promise.all([
fetchRipeStatCached("https://stat.ripe.net/data/asn-neighbours/data.json?resource=AS" + rawAsn),
fetchRipeStatCached("https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS" + rawAsn),
fetchRipeStatCached("https://stat.ripe.net/data/asn-neighbours/data.json?resource=AS" + rawAsn, { timeout: 5000 }),
fetchRipeStatCached("https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS" + rawAsn, { timeout: 5000 }),
]);
// Use looking-glass with actual prefixes to get BGP paths
const announcedPrefixes = prefixData?.data?.prefixes || [];
const samplePrefixes = announcedPrefixes.slice(0, 5).map((p) => p.prefix);
const samplePrefixes = announcedPrefixes.slice(0, 3).map((p) => p.prefix); // reduced 5→3
// Fetch looking-glass data for up to 5 prefixes in parallel
// Fetch looking-glass data for up to 3 prefixes in parallel (3s timeout each)
const lgResults = await Promise.all(
samplePrefixes.map((pfx) =>
fetchRipeStatCached("https://stat.ripe.net/data/looking-glass/data.json?resource=" + encodeURIComponent(pfx))
fetchRipeStatCached("https://stat.ripe.net/data/looking-glass/data.json?resource=" + encodeURIComponent(pfx), { timeout: 3000 }).catch(() => null)
)
);
@ -2590,10 +2595,7 @@ const server = http.createServer(async (req, res) => {
});
const duration = Date.now() - start;
return res.end(
JSON.stringify(
{
const verifyResult = {
meta: {
query: "AS" + rawAsn,
duration_ms: duration,
@ -2630,11 +2632,9 @@ const server = http.createServer(async (req, res) => {
results: pathResults,
},
rpki_coverage: rpkiCoverage,
},
null,
2
)
);
};
resultCacheSet(aspaResultCache, "verify:" + rawAsn, verifyResult);
return res.end(JSON.stringify(verifyResult, null, 2));
} catch (err) {
res.writeHead(500);
return res.end(JSON.stringify({ error: "ASPA verification failed", message: err.message }));

View File

@ -1233,7 +1233,7 @@ function checkRateLimit(ip) {
}
function fetchJSON(url, options) {
const timeoutMs = (options && options.timeout) || 20000;
const timeoutMs = (options && options.timeout) || 8000;
return new Promise((resolve) => {
const reqOptions = {
headers: { "User-Agent": UA, ...(options && options.headers ? options.headers : {}) },
@ -2390,24 +2390,29 @@ const server = http.createServer(async (req, res) => {
res.writeHead(400);
return res.end(JSON.stringify({ error: "Missing or invalid ASN parameter" }));
}
const cachedVerify = resultCacheGet(aspaResultCache, "verify:" + rawAsn);
if (cachedVerify !== undefined) {
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify(cachedVerify));
}
const targetAsn = parseInt(rawAsn);
const start = Date.now();
try {
// Fetch neighbour and prefix data first
const [neighbourData, prefixData] = await Promise.all([
fetchRipeStatCached("https://stat.ripe.net/data/asn-neighbours/data.json?resource=AS" + rawAsn),
fetchRipeStatCached("https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS" + rawAsn),
fetchRipeStatCached("https://stat.ripe.net/data/asn-neighbours/data.json?resource=AS" + rawAsn, { timeout: 5000 }),
fetchRipeStatCached("https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS" + rawAsn, { timeout: 5000 }),
]);
// Use looking-glass with actual prefixes to get BGP paths
const announcedPrefixes = prefixData?.data?.prefixes || [];
const samplePrefixes = announcedPrefixes.slice(0, 5).map((p) => p.prefix);
const samplePrefixes = announcedPrefixes.slice(0, 3).map((p) => p.prefix); // reduced 5→3
// Fetch looking-glass data for up to 5 prefixes in parallel
// Fetch looking-glass data for up to 3 prefixes in parallel (3s timeout each)
const lgResults = await Promise.all(
samplePrefixes.map((pfx) =>
fetchRipeStatCached("https://stat.ripe.net/data/looking-glass/data.json?resource=" + encodeURIComponent(pfx))
fetchRipeStatCached("https://stat.ripe.net/data/looking-glass/data.json?resource=" + encodeURIComponent(pfx), { timeout: 3000 }).catch(() => null)
)
);
@ -2590,10 +2595,7 @@ const server = http.createServer(async (req, res) => {
});
const duration = Date.now() - start;
return res.end(
JSON.stringify(
{
const verifyResult = {
meta: {
query: "AS" + rawAsn,
duration_ms: duration,
@ -2630,11 +2632,9 @@ const server = http.createServer(async (req, res) => {
results: pathResults,
},
rpki_coverage: rpkiCoverage,
},
null,
2
)
);
};
resultCacheSet(aspaResultCache, "verify:" + rawAsn, verifyResult);
return res.end(JSON.stringify(verifyResult, null, 2));
} catch (err) {
res.writeHead(500);
return res.end(JSON.stringify({ error: "ASPA verification failed", message: err.message }));