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
THROTTLE_SIZE_LIMIT = 32768 # 32KB — above typical TCP initial window
def _detect_throttle(proxy, base_url):
"""Detect TSPU throttling by downloading page and checking assets.
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
def _find_assets(proxy, base_url):
"""Download HTML page via proxy and extract CSS/JS asset URLs."""
try:
result = subprocess.run(
["curl", "-4", "--proxy", proxy, "-s", "-m", "10", "-L", base_url],
capture_output=True, timeout=15,
)
if result.returncode != 0:
return None
return []
page = result.stdout.decode("utf-8", errors="ignore")
except (subprocess.TimeoutExpired, OSError):
return None
return []
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)
origin = "%s://%s" % (parsed_url.scheme, parsed_url.netloc)
assets = []
......@@ -235,58 +226,52 @@ def _detect_throttle(proxy, base_url):
elif not href.startswith("http"):
href = base_url.rstrip("/") + "/" + href
assets.append(href)
return assets[:3]
if not assets:
return None
# Try downloading assets to detect throttle
for asset_url in assets[:3]:
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
def _check_throttle(name, proxy, asset_url):
"""Download a single asset via proxy and detect throttling.
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):
"""Check domain: resolve IPs, find in route lists, whois, test gateways."""
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 = {}
workers = len(CHECK_GATEWAYS) * 2 + 2
dgw_proxy = CHECK_GATEWAYS[0][1] # dgw — direct path
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool:
gw_futures = []
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, "-6"))
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):
name, ipver, status, code = future.result()
......@@ -300,7 +285,21 @@ def check_site(domain):
checks[name]["http_code_v6"] = code
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)
routes = find_in_routes(domain, ips)
......@@ -907,7 +906,7 @@ function renderCheck(data) {
container.appendChild(rsec);
// Gateways (IPv4 + IPv6)
const throttled = data.throttle && data.throttle.throttled;
const thr = data.throttle || {};
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'));
const gwrap = document.createElement('div');
......@@ -916,13 +915,13 @@ function renderCheck(data) {
const info = data.checks[name];
if (!info) continue;
let st4 = info.status;
// Override dgw status to SLOW when throttling detected
if (name === 'dgw' && throttled && st4 === 'OK') st4 = 'SLOW';
const gwThr = thr[name];
if (gwThr && gwThr.throttled && st4 === 'OK') st4 = 'SLOW';
const cls4 = st4 === 'OK' ? 'ok' : st4 === 'SLOW' ? 'slow' : st4 === 'BLOCK' ? 'block' : st4 === 'PROXY?' ? 'proxy' : 'other';
const el4 = document.createElement('span');
el4.className = 'check-gw ' + cls4;
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);
if (info.status_v6 && info.status_v6 !== 'PROXY?') {
const cls6 = info.status_v6 === 'OK' ? 'ok' : info.status_v6 === 'BLOCK' ? 'block' : 'other';
......@@ -936,15 +935,18 @@ function renderCheck(data) {
container.appendChild(gsec);
// Throttle detection
if (data.throttle) {
if (Object.keys(thr).length) {
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)'));
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)';
const el = mkDiv('check-route', msg);
el.style.background = '#f8d7da';
el.style.color = '#721c24';
tsec.appendChild(el);
for (const [gw, info] of slowGws) {
const msg = gw + ': ' + info.asset + ' \\u2014 ' + info.size + ' \\u0431\\u0430\\u0439\\u0442 \\u0437\\u0430 ' + info.time + '\\u0441';
const el = mkDiv('check-route', msg);
el.style.background = '#f8d7da';
el.style.color = '#721c24';
tsec.appendChild(el);
}
} else {
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'));
......
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