fix: cap lookup/validate at ≤10s cold, fix infinite skeleton spinner
lookup: - Remove WithRetry on Prefixes/Neighbours (8s+retry+8s=17s → 8s max) - Add 9s hard cap inside timedFetch per source - Visibility timeout 12s → 8s validate: - Phase 1 timeout 8s → 5s (prevents blocking Phase 2) - Phase 2 per-check cap: 10s → 5s (total ≤10s for any ASN) - rdns sample: 20 → 3 (was 20 concurrent RIPE Stat calls) - rdns per-call timeout: 5s → 4s Frontend doLookup: - Add 15s AbortController on /api/lookup fetch - Show 'timed out — try again' instead of infinite skeleton spinner
This commit is contained in:
parent
e1dcbe517f
commit
5b04fc663f
@ -18,3 +18,6 @@
|
||||
{"d":"2026-04-08","t":"FEAT","m":"New /api/quick-ix endpoint: lightweight PeeringDB IX connections + network name, 1h cache"}
|
||||
{"d":"2026-04-08","t":"FIX","m":"validate: reduce reverse-dns timeout 15s→5s, route-leak asn-neighbours 30s→8s, comparison endpoint 4x 30s→8s — prevent semaphore starvation"}
|
||||
{"d":"2026-04-08","t":"FEAT","m":"validate: add 15min result cache — subsequent lookups return in ~18ms vs 700ms+ cold"}
|
||||
{"d":"2026-04-09","t":"FIX","m":"lookup: remove WithRetry on Prefixes+Neighbours (was 8s+8s=16s, now 8s max), add 9s timedFetch hard cap per source"}
|
||||
{"d":"2026-04-09","t":"FIX","m":"validate Phase1: reduce timeout 8s→5s; Phase2 per-check cap 10s→5s; rdns sample 20→3; total cold ≤10s vs 16s before"}
|
||||
{"d":"2026-04-09","t":"FIX","m":"doLookup: add 15s AbortController on initial fetch — skeleton no longer spins indefinitely on slow/failed lookups"}
|
||||
|
||||
@ -1162,8 +1162,11 @@ async function doLookup() {
|
||||
$('skeleton').classList.remove('hidden');
|
||||
$('metaBar').textContent = '';
|
||||
|
||||
const lookupCtrl = new AbortController();
|
||||
const lookupTimer = setTimeout(() => lookupCtrl.abort(), 15000);
|
||||
try {
|
||||
const resp = await fetch('/api/lookup?asn=' + raw);
|
||||
const resp = await fetch('/api/lookup?asn=' + raw, { signal: lookupCtrl.signal });
|
||||
clearTimeout(lookupTimer);
|
||||
const d = await resp.json();
|
||||
|
||||
if (d.error) {
|
||||
@ -1200,8 +1203,9 @@ async function doLookup() {
|
||||
// v0.6.1 new features
|
||||
loadNewFeatures(raw);
|
||||
} catch (e) {
|
||||
clearTimeout(lookupTimer);
|
||||
$('skeleton').classList.add('hidden');
|
||||
$('metaBar').textContent = 'Error: ' + e.message;
|
||||
$('metaBar').textContent = e.name === 'AbortError' ? 'Lookup timed out — try again' : 'Error: ' + e.message;
|
||||
} finally {
|
||||
$('searchBtn').disabled = false;
|
||||
$('searchBtn').textContent = 'Lookup';
|
||||
|
||||
40
server.js
40
server.js
@ -2927,12 +2927,12 @@ const server = http.createServer(async (req, res) => {
|
||||
const targetAsn = parseInt(rawAsn);
|
||||
|
||||
try {
|
||||
// Phase 1: Fetch core data needed by multiple validations
|
||||
// Phase 1: Fetch core data — 5s cap prevents large ASNs from blocking Phase 2
|
||||
const [prefixData, pdbNet, neighbourData, overviewData] = await Promise.all([
|
||||
fetchRipeStatCached("https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS" + rawAsn, { timeout: 8000 }),
|
||||
fetchRipeStatCached("https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS" + rawAsn, { timeout: 5000 }),
|
||||
fetchPeeringDB("/net?asn=" + rawAsn),
|
||||
fetchRipeStatCached("https://stat.ripe.net/data/asn-neighbours/data.json?resource=AS" + rawAsn, { timeout: 8000 }),
|
||||
fetchRipeStatCached("https://stat.ripe.net/data/as-overview/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/as-overview/data.json?resource=AS" + rawAsn, { timeout: 5000 }),
|
||||
]);
|
||||
|
||||
const allPrefixes = (prefixData && prefixData.data && prefixData.data.prefixes ? prefixData.data.prefixes : []).map(function(p) { return p.prefix; });
|
||||
@ -3060,11 +3060,11 @@ const server = http.createServer(async (req, res) => {
|
||||
return checkManrsMembership(rawAsn);
|
||||
}).catch(function(e) { return { status: "info", participant: "unknown", message: "MANRS check unavailable: " + e.message, note: "https://www.manrs.org/netops/participants/" }; });
|
||||
|
||||
// 17. Reverse DNS Coverage (sample up to 20 prefixes for better coverage)
|
||||
var rdnsSampleSize = Math.min(20, samplePrefixes.length);
|
||||
// 17. Reverse DNS Coverage (3 prefix sample — more causes semaphore starvation on large ASNs)
|
||||
var rdnsSampleSize = Math.min(3, samplePrefixes.length);
|
||||
validationPromises.rdns = Promise.all(
|
||||
samplePrefixes.slice(0, rdnsSampleSize).map(function(pfx) {
|
||||
return fetchRipeStatCached("https://stat.ripe.net/data/reverse-dns-consistency/data.json?resource=" + encodeURIComponent(pfx), { timeout: 5000 }).then(function(data) {
|
||||
return fetchRipeStatCached("https://stat.ripe.net/data/reverse-dns-consistency/data.json?resource=" + encodeURIComponent(pfx), { timeout: 4000 }).then(function(data) {
|
||||
var pfxData = data && data.data && data.data.prefixes ? data.data.prefixes : {};
|
||||
var hasDelegation = false;
|
||||
var details = [];
|
||||
@ -3248,9 +3248,14 @@ const server = http.createServer(async (req, res) => {
|
||||
}).catch(function() { return []; })
|
||||
: Promise.resolve([]);
|
||||
|
||||
// Run all validations in parallel
|
||||
// Run all validations in parallel — 5s cap per check, total validate bounded to ~10s
|
||||
var keys = Object.keys(validationPromises);
|
||||
var promises = keys.map(function(k) { return validationPromises[k]; });
|
||||
var promises = keys.map(function(k) {
|
||||
return Promise.race([
|
||||
validationPromises[k],
|
||||
new Promise(function(resolve) { setTimeout(function() { resolve({ status: "info", message: "timed out" }); }, 5000); }),
|
||||
]);
|
||||
});
|
||||
var settled = await Promise.allSettled(promises);
|
||||
var facCountries = await facCountriesPromise;
|
||||
|
||||
@ -3413,13 +3418,16 @@ const server = http.createServer(async (req, res) => {
|
||||
let cachedIxlan = pdbSourceCache.get("netixlan", ixCacheKey);
|
||||
let cachedFac = netId ? pdbSourceCache.get("netfac", String(netId)) : null;
|
||||
|
||||
// Per-source timing tracking
|
||||
// Per-source timing tracking — 9s hard cap per source to prevent long-tail blocking
|
||||
const sourceTiming = {};
|
||||
function timedFetch(name, promise) {
|
||||
const ts = Date.now();
|
||||
return Promise.resolve(promise)
|
||||
.then(r => { sourceTiming[name] = Date.now() - ts; return r; })
|
||||
.catch(() => { sourceTiming[name] = null; return null; });
|
||||
return Promise.race([
|
||||
Promise.resolve(promise),
|
||||
new Promise(function(r) { setTimeout(function() { r(null); }, 9000); }),
|
||||
])
|
||||
.then(function(r) { sourceTiming[name] = Date.now() - ts; return r; })
|
||||
.catch(function() { sourceTiming[name] = null; return null; });
|
||||
}
|
||||
|
||||
const pocQuery = netId ? "/poc?net_id=" + netId + "&limit=25" : null;
|
||||
@ -3440,13 +3448,13 @@ const server = http.createServer(async (req, res) => {
|
||||
]).then(d => { rdapCacheSet(asn, d); return d; });
|
||||
|
||||
const promises = [
|
||||
timedFetch("RIPE Stat Prefixes", fetchRipeStatCachedWithRetry("https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS" + asn, { timeout: 8000 })),
|
||||
timedFetch("RIPE Stat Neighbours", fetchRipeStatCachedWithRetry("https://stat.ripe.net/data/asn-neighbours/data.json?resource=AS" + asn, { timeout: 8000 })),
|
||||
timedFetch("RIPE Stat Prefixes", fetchRipeStatCached("https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS" + asn, { timeout: 8000 })),
|
||||
timedFetch("RIPE Stat Neighbours", fetchRipeStatCached("https://stat.ripe.net/data/asn-neighbours/data.json?resource=AS" + asn, { timeout: 8000 })),
|
||||
timedFetch("RIPE Stat Overview", fetchRipeStatCached("https://stat.ripe.net/data/as-overview/data.json?resource=AS" + asn)),
|
||||
timedFetch("RIPE Stat RIR", fetchRipeStatCached("https://stat.ripe.net/data/rir-stats-country/data.json?resource=AS" + asn)),
|
||||
timedFetch("RIPE Atlas", fetchJSON("https://atlas.ripe.net/api/v2/probes/?asn_v4=" + asn + "&page_size=500")),
|
||||
timedFetch("bgp.he.net", fetchBgpHeNet(asn)),
|
||||
timedFetch("RIPE Stat Visibility", fetchRipeStatCached("https://stat.ripe.net/data/visibility/data.json?resource=AS" + asn, { timeout: 12000 })),
|
||||
timedFetch("RIPE Stat Visibility", fetchRipeStatCached("https://stat.ripe.net/data/visibility/data.json?resource=AS" + asn, { timeout: 8000 })),
|
||||
timedFetch("RIPE Stat PrefixSize", fetchRipeStatCached("https://stat.ripe.net/data/prefix-size-distribution/data.json?resource=AS" + asn)),
|
||||
timedFetch("PeeringDB IXLan", cachedIxlan ? Promise.resolve(cachedIxlan) : fetchPeeringDBWithRetry(ixQuery)),
|
||||
timedFetch("PeeringDB Facilities", cachedFac ? Promise.resolve(cachedFac) : (netId ? fetchPeeringDBWithRetry("/netfac?net_id=" + netId + "&limit=1000") : Promise.resolve(null))),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user