Commit 44b22a63 authored by Vitaly Lipatov's avatar Vitaly Lipatov

route-web-api: check throttling on all gateways, not just dgw

Fetch HTML once to find asset URLs, then download the asset through every gateway in parallel. Show SLOW badge on each throttled gateway. Throttle details section lists all affected gateways with sizes/times. Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 3e09135e
...@@ -196,31 +196,22 @@ THROTTLE_TIMEOUT = 8 ...@@ -196,31 +196,22 @@ THROTTLE_TIMEOUT = 8
THROTTLE_SIZE_LIMIT = 32768 # 32KB — above typical TCP initial window THROTTLE_SIZE_LIMIT = 32768 # 32KB — above typical TCP initial window
def _detect_throttle(proxy, base_url): def _find_assets(proxy, base_url):
"""Detect TSPU throttling by downloading page and checking assets. """Download HTML page via proxy and extract CSS/JS asset URLs."""
Downloads the HTML page via proxy. If the page is small (< 32KB),
parses it for CSS/JS asset URLs and tries downloading them.
Throttling signature: download stalls at ~16KB after timeout.
Returns dict with throttle info or None.
"""
# Download main page
try: try:
result = subprocess.run( result = subprocess.run(
["curl", "-4", "--proxy", proxy, "-s", "-m", "10", "-L", base_url], ["curl", "-4", "--proxy", proxy, "-s", "-m", "10", "-L", base_url],
capture_output=True, timeout=15, capture_output=True, timeout=15,
) )
if result.returncode != 0: if result.returncode != 0:
return None return []
page = result.stdout.decode("utf-8", errors="ignore") page = result.stdout.decode("utf-8", errors="ignore")
except (subprocess.TimeoutExpired, OSError): except (subprocess.TimeoutExpired, OSError):
return None return []
if len(result.stdout) >= THROTTLE_SIZE_LIMIT: if len(result.stdout) >= THROTTLE_SIZE_LIMIT:
return None # page itself is large enough, no throttle detected return [] # page itself is large, no need to check assets
# Parse asset URLs (CSS, JS) from HTML
parsed_url = urllib.parse.urlparse(base_url) parsed_url = urllib.parse.urlparse(base_url)
origin = "%s://%s" % (parsed_url.scheme, parsed_url.netloc) origin = "%s://%s" % (parsed_url.scheme, parsed_url.netloc)
assets = [] assets = []
...@@ -235,58 +226,52 @@ def _detect_throttle(proxy, base_url): ...@@ -235,58 +226,52 @@ def _detect_throttle(proxy, base_url):
elif not href.startswith("http"): elif not href.startswith("http"):
href = base_url.rstrip("/") + "/" + href href = base_url.rstrip("/") + "/" + href
assets.append(href) assets.append(href)
return assets[:3]
if not assets:
return None
# Try downloading assets to detect throttle def _check_throttle(name, proxy, asset_url):
for asset_url in assets[:3]: """Download a single asset via proxy and detect throttling.
try:
result = subprocess.run(
["curl", "-4", "--proxy", proxy, "-o", "/dev/null", "-s",
"-w", "%{size_download} %{time_total}",
"-m", str(THROTTLE_TIMEOUT), "-L", asset_url],
capture_output=True, text=True, timeout=THROTTLE_TIMEOUT + 5,
)
parts = result.stdout.strip().split()
if len(parts) < 2:
continue
size = int(float(parts[0]))
elapsed = float(parts[1])
# Throttle: stuck near 16KB, timeout reached
if 0 < size < THROTTLE_SIZE_LIMIT and elapsed >= THROTTLE_TIMEOUT - 1:
fname = asset_url.split("/")[-1].split("?")[0]
return {
"throttled": True,
"asset": fname,
"url": asset_url,
"size": size,
"time": round(elapsed, 1),
}
# Got large file — not throttled
if size >= THROTTLE_SIZE_LIMIT:
return {"throttled": False}
except (subprocess.TimeoutExpired, OSError, ValueError):
continue
return None Returns (name, result_dict) or (name, None).
"""
try:
result = subprocess.run(
["curl", "-4", "--proxy", proxy, "-o", "/dev/null", "-s",
"-w", "%{size_download} %{time_total}",
"-m", str(THROTTLE_TIMEOUT), "-L", asset_url],
capture_output=True, text=True, timeout=THROTTLE_TIMEOUT + 5,
)
parts = result.stdout.strip().split()
if len(parts) < 2:
return (name, None)
size = int(float(parts[0]))
elapsed = float(parts[1])
fname = asset_url.split("/")[-1].split("?")[0]
if 0 < size < THROTTLE_SIZE_LIMIT and elapsed >= THROTTLE_TIMEOUT - 1:
return (name, {"throttled": True, "asset": fname,
"size": size, "time": round(elapsed, 1)})
if size >= THROTTLE_SIZE_LIMIT:
return (name, {"throttled": False})
except (subprocess.TimeoutExpired, OSError, ValueError):
pass
return (name, None)
def check_site(domain): def check_site(domain):
"""Check domain: resolve IPs, find in route lists, whois, test gateways.""" """Check domain: resolve IPs, find in route lists, whois, test gateways."""
url = "https://%s/" % domain url = "https://%s/" % domain
# Run gateway checks (IPv4 + IPv6), whois and throttle in parallel # Phase 1: gateway checks (IPv4 + IPv6), whois, and fetch assets in parallel
checks = {} checks = {}
workers = len(CHECK_GATEWAYS) * 2 + 2 workers = len(CHECK_GATEWAYS) * 2 + 2
dgw_proxy = CHECK_GATEWAYS[0][1] # dgw — direct path
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool: with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool:
gw_futures = [] gw_futures = []
for name, proxy in CHECK_GATEWAYS: for name, proxy in CHECK_GATEWAYS:
gw_futures.append(pool.submit(_check_one, name, proxy, url, "-4")) gw_futures.append(pool.submit(_check_one, name, proxy, url, "-4"))
gw_futures.append(pool.submit(_check_one, name, proxy, url, "-6")) gw_futures.append(pool.submit(_check_one, name, proxy, url, "-6"))
whois_future = pool.submit(get_whois, domain) whois_future = pool.submit(get_whois, domain)
throttle_future = pool.submit(_detect_throttle, dgw_proxy, url) # Fetch HTML and parse assets via first available gateway
assets_future = pool.submit(_find_assets, CHECK_GATEWAYS[0][1], url)
for future in concurrent.futures.as_completed(gw_futures): for future in concurrent.futures.as_completed(gw_futures):
name, ipver, status, code = future.result() name, ipver, status, code = future.result()
...@@ -300,7 +285,21 @@ def check_site(domain): ...@@ -300,7 +285,21 @@ def check_site(domain):
checks[name]["http_code_v6"] = code checks[name]["http_code_v6"] = code
whois_info = whois_future.result() whois_info = whois_future.result()
throttle = throttle_future.result() assets = assets_future.result()
# Phase 2: throttle check — download first asset via all gateways in parallel
throttle = {}
if assets:
asset_url = assets[0]
with concurrent.futures.ThreadPoolExecutor(max_workers=len(CHECK_GATEWAYS)) as pool:
throttle_futures = [
pool.submit(_check_throttle, name, proxy, asset_url)
for name, proxy in CHECK_GATEWAYS
]
for future in concurrent.futures.as_completed(throttle_futures):
gw_name, result = future.result()
if result:
throttle[gw_name] = result
ips = resolve_domain(domain) ips = resolve_domain(domain)
routes = find_in_routes(domain, ips) routes = find_in_routes(domain, ips)
...@@ -907,7 +906,7 @@ function renderCheck(data) { ...@@ -907,7 +906,7 @@ function renderCheck(data) {
container.appendChild(rsec); container.appendChild(rsec);
// Gateways (IPv4 + IPv6) // Gateways (IPv4 + IPv6)
const throttled = data.throttle && data.throttle.throttled; const thr = data.throttle || {};
const gsec = mkDiv('check-section'); const gsec = mkDiv('check-section');
gsec.appendChild(mkDiv('check-section-title', '\\u0414\\u043e\\u0441\\u0442\\u0443\\u043f\\u043d\\u043e\\u0441\\u0442\\u044c \\u0447\\u0435\\u0440\\u0435\\u0437 \\u0448\\u043b\\u044e\\u0437\\u044b')); gsec.appendChild(mkDiv('check-section-title', '\\u0414\\u043e\\u0441\\u0442\\u0443\\u043f\\u043d\\u043e\\u0441\\u0442\\u044c \\u0447\\u0435\\u0440\\u0435\\u0437 \\u0448\\u043b\\u044e\\u0437\\u044b'));
const gwrap = document.createElement('div'); const gwrap = document.createElement('div');
...@@ -916,13 +915,13 @@ function renderCheck(data) { ...@@ -916,13 +915,13 @@ function renderCheck(data) {
const info = data.checks[name]; const info = data.checks[name];
if (!info) continue; if (!info) continue;
let st4 = info.status; let st4 = info.status;
// Override dgw status to SLOW when throttling detected const gwThr = thr[name];
if (name === 'dgw' && throttled && st4 === 'OK') st4 = 'SLOW'; if (gwThr && gwThr.throttled && st4 === 'OK') st4 = 'SLOW';
const cls4 = st4 === 'OK' ? 'ok' : st4 === 'SLOW' ? 'slow' : st4 === 'BLOCK' ? 'block' : st4 === 'PROXY?' ? 'proxy' : 'other'; const cls4 = st4 === 'OK' ? 'ok' : st4 === 'SLOW' ? 'slow' : st4 === 'BLOCK' ? 'block' : st4 === 'PROXY?' ? 'proxy' : 'other';
const el4 = document.createElement('span'); const el4 = document.createElement('span');
el4.className = 'check-gw ' + cls4; el4.className = 'check-gw ' + cls4;
el4.textContent = name + ': ' + st4; el4.textContent = name + ': ' + st4;
if (name === 'dgw' && throttled) el4.title = data.throttle.asset + ': ' + data.throttle.size + ' \\u0431\\u0430\\u0439\\u0442 \\u0437\\u0430 ' + data.throttle.time + '\\u0441'; if (gwThr && gwThr.throttled) el4.title = gwThr.asset + ': ' + gwThr.size + ' \\u0431\\u0430\\u0439\\u0442 \\u0437\\u0430 ' + gwThr.time + '\\u0441';
gwrap.appendChild(el4); gwrap.appendChild(el4);
if (info.status_v6 && info.status_v6 !== 'PROXY?') { if (info.status_v6 && info.status_v6 !== 'PROXY?') {
const cls6 = info.status_v6 === 'OK' ? 'ok' : info.status_v6 === 'BLOCK' ? 'block' : 'other'; const cls6 = info.status_v6 === 'OK' ? 'ok' : info.status_v6 === 'BLOCK' ? 'block' : 'other';
...@@ -936,15 +935,18 @@ function renderCheck(data) { ...@@ -936,15 +935,18 @@ function renderCheck(data) {
container.appendChild(gsec); container.appendChild(gsec);
// Throttle detection // Throttle detection
if (data.throttle) { if (Object.keys(thr).length) {
const tsec = mkDiv('check-section'); const tsec = mkDiv('check-section');
if (data.throttle.throttled) { const slowGws = Object.entries(thr).filter(([,v]) => v.throttled);
if (slowGws.length) {
tsec.appendChild(mkDiv('check-section-title', '\\u0417\\u0430\\u043c\\u0435\\u0434\\u043b\\u0435\\u043d\\u0438\\u0435 (\\u0422\\u0421\\u041f\\u0423)')); tsec.appendChild(mkDiv('check-section-title', '\\u0417\\u0430\\u043c\\u0435\\u0434\\u043b\\u0435\\u043d\\u0438\\u0435 (\\u0422\\u0421\\u041f\\u0423)'));
const msg = data.throttle.asset + ': ' + data.throttle.size + ' \\u0431\\u0430\\u0439\\u0442 \\u0437\\u0430 ' + data.throttle.time + '\\u0441 (\\u043e\\u0436\\u0438\\u0434\\u0430\\u043b\\u043e\\u0441\\u044c \\u0431\\u043e\\u043b\\u044c\\u0448\\u0435)'; for (const [gw, info] of slowGws) {
const el = mkDiv('check-route', msg); const msg = gw + ': ' + info.asset + ' \\u2014 ' + info.size + ' \\u0431\\u0430\\u0439\\u0442 \\u0437\\u0430 ' + info.time + '\\u0441';
el.style.background = '#f8d7da'; const el = mkDiv('check-route', msg);
el.style.color = '#721c24'; el.style.background = '#f8d7da';
tsec.appendChild(el); el.style.color = '#721c24';
tsec.appendChild(el);
}
} else { } else {
tsec.appendChild(mkDiv('check-section-title', '\\u0417\\u0430\\u043c\\u0435\\u0434\\u043b\\u0435\\u043d\\u0438\\u0435')); tsec.appendChild(mkDiv('check-section-title', '\\u0417\\u0430\\u043c\\u0435\\u0434\\u043b\\u0435\\u043d\\u0438\\u0435'));
tsec.appendChild(mkDiv('check-no-route', '\\u041d\\u0435 \\u043e\\u0431\\u043d\\u0430\\u0440\\u0443\\u0436\\u0435\\u043d\\u043e')); tsec.appendChild(mkDiv('check-no-route', '\\u041d\\u0435 \\u043e\\u0431\\u043d\\u0430\\u0440\\u0443\\u0436\\u0435\\u043d\\u043e'));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment