Files
9router/src/lib/network/poolEgressProbe.js
T
MUH. IQRAM BAHRING 37d03aa0d5 feat(proxy-fitness): on/off toggle for egress geo probe
Header toggle in Proxy Fitness persists settings.poolGeoProbeEnabled
(default on); the probe scheduler reads it each pass and skips when off.
The env POOL_GEO_PROBE_DISABLED remains a hard override. Smart rotation is
unaffected — it runs off the fitness registry from real request failures.
2026-08-07 08:01:55 +08:00

115 lines
4.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Background pool egress geo probe — fills the poolGeo cache so the Proxy
// Fitness / Proxy Pools UI can show each pool's egress IP + country, and
// future provider region policies can pre-mark pools unfit.
// Fail-open everywhere; never blocks startup or requests.
//
// Rate safety: each probe hits ipinfo.io through the pool; quotas are small,
// so failing pools get a negative backoff instead of being re-probed every
// pass, and per-pass output is a single aggregated summary line.
import { getProxyPools } from "@/models";
import { probePoolGeo, setPoolGeo, getPoolGeo } from "open-sse/services/poolGeo.js";
import { isNonServerRuntime } from "@/sse/services/backgroundTokenRefresh.js";
import { getSettings } from "@/lib/localDb";
const PROBE_INTERVAL_MS = 30 * 60 * 1000;
const INITIAL_DELAY_MS = 15 * 1000;
const CONCURRENCY = 3;
// Re-probe a pool when its last sample is older than this — multiple samples
// per pool are what let us flag flapping (changing egress) relays.
const GEO_REPROBE_MS = 30 * 60 * 1000;
// Backoff windows per failure family: server/rate problems are likely to
// persist (broken relay, exhausted quota), network blips recover sooner.
const BACKOFF = {
"rate-limit": 2 * 60 * 60 * 1000,
server: 2 * 60 * 60 * 1000,
network: 30 * 60 * 1000,
timeout: 30 * 60 * 1000,
"no-ip": 30 * 60 * 1000,
};
let started = false;
let intervalHandle = null;
let initialTimeoutHandle = null;
let probing = false;
const probeBackoff = new Map(); // poolId -> retryAfter (ms epoch)
function isTruthyEnv(v) {
if (v == null || v === "") return false;
return ["1", "true", "yes", "on"].includes(String(v).trim().toLowerCase());
}
// probe with bounded concurrency; skips pools in backoff or with fresh samples.
async function probeAll() {
if (probing) return;
probing = true;
try {
// UI toggle (settings.poolGeoProbeEnabled) gates the whole feature; the
// env var remains a hard override for headless setups.
try {
const settings = await getSettings();
if (settings?.poolGeoProbeEnabled === false) return;
} catch { /* DB hiccup — keep probing (fail-open) */ }
const pools = await getProxyPools({ isActive: true });
const now = Date.now();
const active = (pools || []).filter((p) => !!p?.proxyUrl);
const targets = active.filter((p) => {
const until = probeBackoff.get(p.id);
if (until && until > now) return false; // waiting out a failure
const geo = getPoolGeo(p.id);
return !geo || now - geo.ts >= GEO_REPROBE_MS;
});
if (targets.length === 0) return;
const failTally = {};
let next = 0;
const workers = Array.from({ length: Math.min(CONCURRENCY, Math.max(targets.length, 1)) }, async () => {
while (next < targets.length) {
const pool = targets[next++];
const res = await probePoolGeo(pool);
if (res.ok) {
setPoolGeo(pool.id, res.geo);
if (probeBackoff.has(pool.id)) probeBackoff.delete(pool.id);
} else {
failTally[res.error] = (failTally[res.error] || 0) + 1;
probeBackoff.set(pool.id, now + (BACKOFF[res.error] || BACKOFF.network));
}
}
});
await Promise.allSettled(workers);
const filled = active.filter((p) => getPoolGeo(p.id)).length;
const failSummary = Object.entries(failTally).map(([k, n]) => `${k}×${n}`).join(", ") || "none";
console.log(`[PoolEgressProbe] geo ${filled}/${active.length} · fail: ${failSummary}`);
} catch (e) {
console.log(`[PoolEgressProbe] pass failed: ${e?.message || e}`);
} finally {
probing = false;
}
}
export function startPoolEgressProbe({ intervalMs } = {}) {
if (started) return false;
if (isNonServerRuntime()) return false;
if (isTruthyEnv(process.env.POOL_GEO_PROBE_DISABLED)) return false;
started = true;
const period = Number.isFinite(intervalMs) && intervalMs > 0 ? intervalMs : PROBE_INTERVAL_MS;
console.log("[PoolEgressProbe] Scheduler started", { intervalMs: period, initialDelayMs: INITIAL_DELAY_MS });
initialTimeoutHandle = setTimeout(() => { probeAll().catch(() => {}); }, INITIAL_DELAY_MS);
if (initialTimeoutHandle.unref) initialTimeoutHandle.unref();
intervalHandle = setInterval(() => { probeAll().catch(() => {}); }, period);
if (intervalHandle.unref) intervalHandle.unref();
return true;
}
export function stopPoolEgressProbe() {
if (initialTimeoutHandle) clearTimeout(initialTimeoutHandle);
if (intervalHandle) clearInterval(intervalHandle);
initialTimeoutHandle = null;
intervalHandle = null;
started = false;
}
export const __test__ = { probeAll };