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:
parent
35b89c05aa
commit
487b032661
102
deploy/server.js
102
deploy/server.js
@ -1233,7 +1233,7 @@ function checkRateLimit(ip) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fetchJSON(url, options) {
|
function fetchJSON(url, options) {
|
||||||
const timeoutMs = (options && options.timeout) || 20000;
|
const timeoutMs = (options && options.timeout) || 8000;
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const reqOptions = {
|
const reqOptions = {
|
||||||
headers: { "User-Agent": UA, ...(options && options.headers ? options.headers : {}) },
|
headers: { "User-Agent": UA, ...(options && options.headers ? options.headers : {}) },
|
||||||
@ -2390,24 +2390,29 @@ const server = http.createServer(async (req, res) => {
|
|||||||
res.writeHead(400);
|
res.writeHead(400);
|
||||||
return res.end(JSON.stringify({ error: "Missing or invalid ASN parameter" }));
|
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 targetAsn = parseInt(rawAsn);
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch neighbour and prefix data first
|
// Fetch neighbour and prefix data first
|
||||||
const [neighbourData, prefixData] = await Promise.all([
|
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/asn-neighbours/data.json?resource=AS" + rawAsn, { timeout: 5000 }),
|
||||||
fetchRipeStatCached("https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS" + rawAsn),
|
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
|
// Use looking-glass with actual prefixes to get BGP paths
|
||||||
const announcedPrefixes = prefixData?.data?.prefixes || [];
|
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(
|
const lgResults = await Promise.all(
|
||||||
samplePrefixes.map((pfx) =>
|
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,51 +2595,46 @@ const server = http.createServer(async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const duration = Date.now() - start;
|
const duration = Date.now() - start;
|
||||||
|
const verifyResult = {
|
||||||
return res.end(
|
meta: {
|
||||||
JSON.stringify(
|
query: "AS" + rawAsn,
|
||||||
{
|
duration_ms: duration,
|
||||||
meta: {
|
timestamp: new Date().toISOString(),
|
||||||
query: "AS" + rawAsn,
|
paths_analyzed: pathResults.length,
|
||||||
duration_ms: duration,
|
total_paths_seen: allPaths.length,
|
||||||
timestamp: new Date().toISOString(),
|
},
|
||||||
paths_analyzed: pathResults.length,
|
asn: targetAsn,
|
||||||
total_paths_seen: allPaths.length,
|
readiness_score: readinessScore,
|
||||||
},
|
aspa_object_exists: aspaObjectExists,
|
||||||
asn: targetAsn,
|
detected_providers: detectedProviders.map((p) => ({
|
||||||
readiness_score: readinessScore,
|
...p,
|
||||||
aspa_object_exists: aspaObjectExists,
|
frequency: providerFrequency.get(p.asn) || 0,
|
||||||
detected_providers: detectedProviders.map((p) => ({
|
frequency_pct: allPaths.length > 0
|
||||||
...p,
|
? Math.round(((providerFrequency.get(p.asn) || 0) / allPaths.length) * 100)
|
||||||
frequency: providerFrequency.get(p.asn) || 0,
|
: 0,
|
||||||
frequency_pct: allPaths.length > 0
|
})),
|
||||||
? Math.round(((providerFrequency.get(p.asn) || 0) / allPaths.length) * 100)
|
provider_audit: {
|
||||||
: 0,
|
declared_count: aspaDeclaredProviders.length,
|
||||||
})),
|
detected_count: detectedProviders.length,
|
||||||
provider_audit: {
|
completeness_pct: providerCompleteness,
|
||||||
declared_count: aspaDeclaredProviders.length,
|
missing_from_aspa: missingFromAspa,
|
||||||
detected_count: detectedProviders.length,
|
extra_in_aspa: extraInAspa,
|
||||||
completeness_pct: providerCompleteness,
|
},
|
||||||
missing_from_aspa: missingFromAspa,
|
path_verification: {
|
||||||
extra_in_aspa: extraInAspa,
|
total: pathResults.length,
|
||||||
},
|
valid: validPaths,
|
||||||
path_verification: {
|
invalid: invalidPaths,
|
||||||
total: pathResults.length,
|
unknown: unknownPaths,
|
||||||
valid: validPaths,
|
as_set_flagged: asSetPaths,
|
||||||
invalid: invalidPaths,
|
valley_detected: valleyPaths,
|
||||||
unknown: unknownPaths,
|
valid_pct: pathValidPct,
|
||||||
as_set_flagged: asSetPaths,
|
not_invalid_pct: pathNotInvalidPct,
|
||||||
valley_detected: valleyPaths,
|
results: pathResults,
|
||||||
valid_pct: pathValidPct,
|
},
|
||||||
not_invalid_pct: pathNotInvalidPct,
|
rpki_coverage: rpkiCoverage,
|
||||||
results: pathResults,
|
};
|
||||||
},
|
resultCacheSet(aspaResultCache, "verify:" + rawAsn, verifyResult);
|
||||||
rpki_coverage: rpkiCoverage,
|
return res.end(JSON.stringify(verifyResult, null, 2));
|
||||||
},
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.writeHead(500);
|
res.writeHead(500);
|
||||||
return res.end(JSON.stringify({ error: "ASPA verification failed", message: err.message }));
|
return res.end(JSON.stringify({ error: "ASPA verification failed", message: err.message }));
|
||||||
|
|||||||
102
server.js
102
server.js
@ -1233,7 +1233,7 @@ function checkRateLimit(ip) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fetchJSON(url, options) {
|
function fetchJSON(url, options) {
|
||||||
const timeoutMs = (options && options.timeout) || 20000;
|
const timeoutMs = (options && options.timeout) || 8000;
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const reqOptions = {
|
const reqOptions = {
|
||||||
headers: { "User-Agent": UA, ...(options && options.headers ? options.headers : {}) },
|
headers: { "User-Agent": UA, ...(options && options.headers ? options.headers : {}) },
|
||||||
@ -2390,24 +2390,29 @@ const server = http.createServer(async (req, res) => {
|
|||||||
res.writeHead(400);
|
res.writeHead(400);
|
||||||
return res.end(JSON.stringify({ error: "Missing or invalid ASN parameter" }));
|
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 targetAsn = parseInt(rawAsn);
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch neighbour and prefix data first
|
// Fetch neighbour and prefix data first
|
||||||
const [neighbourData, prefixData] = await Promise.all([
|
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/asn-neighbours/data.json?resource=AS" + rawAsn, { timeout: 5000 }),
|
||||||
fetchRipeStatCached("https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS" + rawAsn),
|
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
|
// Use looking-glass with actual prefixes to get BGP paths
|
||||||
const announcedPrefixes = prefixData?.data?.prefixes || [];
|
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(
|
const lgResults = await Promise.all(
|
||||||
samplePrefixes.map((pfx) =>
|
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,51 +2595,46 @@ const server = http.createServer(async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const duration = Date.now() - start;
|
const duration = Date.now() - start;
|
||||||
|
const verifyResult = {
|
||||||
return res.end(
|
meta: {
|
||||||
JSON.stringify(
|
query: "AS" + rawAsn,
|
||||||
{
|
duration_ms: duration,
|
||||||
meta: {
|
timestamp: new Date().toISOString(),
|
||||||
query: "AS" + rawAsn,
|
paths_analyzed: pathResults.length,
|
||||||
duration_ms: duration,
|
total_paths_seen: allPaths.length,
|
||||||
timestamp: new Date().toISOString(),
|
},
|
||||||
paths_analyzed: pathResults.length,
|
asn: targetAsn,
|
||||||
total_paths_seen: allPaths.length,
|
readiness_score: readinessScore,
|
||||||
},
|
aspa_object_exists: aspaObjectExists,
|
||||||
asn: targetAsn,
|
detected_providers: detectedProviders.map((p) => ({
|
||||||
readiness_score: readinessScore,
|
...p,
|
||||||
aspa_object_exists: aspaObjectExists,
|
frequency: providerFrequency.get(p.asn) || 0,
|
||||||
detected_providers: detectedProviders.map((p) => ({
|
frequency_pct: allPaths.length > 0
|
||||||
...p,
|
? Math.round(((providerFrequency.get(p.asn) || 0) / allPaths.length) * 100)
|
||||||
frequency: providerFrequency.get(p.asn) || 0,
|
: 0,
|
||||||
frequency_pct: allPaths.length > 0
|
})),
|
||||||
? Math.round(((providerFrequency.get(p.asn) || 0) / allPaths.length) * 100)
|
provider_audit: {
|
||||||
: 0,
|
declared_count: aspaDeclaredProviders.length,
|
||||||
})),
|
detected_count: detectedProviders.length,
|
||||||
provider_audit: {
|
completeness_pct: providerCompleteness,
|
||||||
declared_count: aspaDeclaredProviders.length,
|
missing_from_aspa: missingFromAspa,
|
||||||
detected_count: detectedProviders.length,
|
extra_in_aspa: extraInAspa,
|
||||||
completeness_pct: providerCompleteness,
|
},
|
||||||
missing_from_aspa: missingFromAspa,
|
path_verification: {
|
||||||
extra_in_aspa: extraInAspa,
|
total: pathResults.length,
|
||||||
},
|
valid: validPaths,
|
||||||
path_verification: {
|
invalid: invalidPaths,
|
||||||
total: pathResults.length,
|
unknown: unknownPaths,
|
||||||
valid: validPaths,
|
as_set_flagged: asSetPaths,
|
||||||
invalid: invalidPaths,
|
valley_detected: valleyPaths,
|
||||||
unknown: unknownPaths,
|
valid_pct: pathValidPct,
|
||||||
as_set_flagged: asSetPaths,
|
not_invalid_pct: pathNotInvalidPct,
|
||||||
valley_detected: valleyPaths,
|
results: pathResults,
|
||||||
valid_pct: pathValidPct,
|
},
|
||||||
not_invalid_pct: pathNotInvalidPct,
|
rpki_coverage: rpkiCoverage,
|
||||||
results: pathResults,
|
};
|
||||||
},
|
resultCacheSet(aspaResultCache, "verify:" + rawAsn, verifyResult);
|
||||||
rpki_coverage: rpkiCoverage,
|
return res.end(JSON.stringify(verifyResult, null, 2));
|
||||||
},
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.writeHead(500);
|
res.writeHead(500);
|
||||||
return res.end(JSON.stringify({ error: "ASPA verification failed", message: err.message }));
|
return res.end(JSON.stringify({ error: "ASPA verification failed", message: err.message }));
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user