feat(proxy): otomatik Floxy→DataImpulse failover (decode regresyonu)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Floxy residential proxy bitince/çökünce (402 bakiye veya tünel reddi — ERR_TUNNEL_CONNECTION_FAILED), EMEX + pcat decode istekleri ölü proxy'ye çarpıp decode başarı oranını çökertiyordu (2026-06-13: ~17k tünel hatası/24s, decode %78→%45). Önceki "DataImpulse last-ditch fallback" tarayıcı yolunda hiç yoktu ve HTTP yolunda her istekte 2 ölü Floxy denemesi ziyan ediyordu. Paylaşılan ProxyHealthService kapısı (in-memory cooldown): herhangi bir tüketici bir Floxy bağlantı hatası görünce kapıyı tetikler; cooldown boyunca TÜM tüketiciler DataImpulse'a düşer. Floxy'den ilk başarılı yanıt veya cooldown bitişi kapıyı temizler (kendi kendini iyileştirir, periyodik yeniden-deneme). FLOXY_FAILOVER_COOLDOWN_MS ile ayarlanır (varsayılan 180s). Bağlanan tüketiciler: - EMEX HTTP (fetchEmexHtml): kapı açıkken DataImpulse-öncelikli zamanlama - EMEX tarayıcı (Playwright): launch'ta dinamik sağlayıcı seçimi + ensureSession health-gate'i (Floxy ölünce DataImpulse'a relaunch, kapı temizlenince Floxy'i yeniden dene); scrape-içi tünel ölümünde tripFloxyFailover - pcat (buildProxy): hem call hem capture (Playwright JWT) bacakları DataImpulse'a düşer isProxyConnectFailure(): yalnız gerçek bağlantı hatalarında tetikler — yavaş-ama- canlı exit'in nav timeout'u failover'ı tetiklemez. Test: ProxyHealthService + isProxyConnectFailure birim testleri; tüm api suite (303) yeşil. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,9 +23,11 @@ import { ProxyAgent } from "undici";
|
||||
import { isBackfillContext } from "../../jobs/prefetch-context";
|
||||
import { PostHogService } from "../../posthog/posthog.service";
|
||||
import { RedisService } from "../../redis/redis.service";
|
||||
import { ProxyHealthService } from "../proxy-telemetry/proxy-health.service";
|
||||
import {
|
||||
ProxyTelemetryService,
|
||||
classifyTransportError,
|
||||
isProxyConnectFailure,
|
||||
} from "../proxy-telemetry/proxy-telemetry.service";
|
||||
import { parseUnitLeaves, parseVehicleTree } from "./emex-tree.parser";
|
||||
import { EmexBrowserService } from "./emex.browser";
|
||||
@@ -147,6 +149,7 @@ export class EmexService {
|
||||
private redis: RedisService,
|
||||
private posthog: PostHogService,
|
||||
private proxyTelemetry: ProxyTelemetryService,
|
||||
private proxyHealth: ProxyHealthService,
|
||||
) {
|
||||
// __dirname is apps/api/src/integrations/emex/ or dist/integrations/emex/
|
||||
// Scraper lives at <monorepo-root>/scripts/emex-vin-scraper.js
|
||||
@@ -383,8 +386,14 @@ export class EmexService {
|
||||
// and a different proxy IP must not "retry" it away.
|
||||
const schedule: Array<"dataimpulse" | "floxy"> = [];
|
||||
if (this.emexProxyProvider === "floxy") {
|
||||
if (this.emexFloxy) schedule.push("floxy", "floxy");
|
||||
if (this.emexProxy) schedule.push("dataimpulse");
|
||||
if (this.emexFloxy && this.proxyHealth.isFloxyDown() && this.emexProxy) {
|
||||
// Floxy gate tripped → DataImpulse-first (rotating ports cover its ~50%
|
||||
// dead ports), with one Floxy probe last so a recovery is still detected.
|
||||
schedule.push("dataimpulse", "dataimpulse", "floxy");
|
||||
} else {
|
||||
if (this.emexFloxy) schedule.push("floxy", "floxy");
|
||||
if (this.emexProxy) schedule.push("dataimpulse");
|
||||
}
|
||||
} else if (this.emexProxyProvider === "dataimpulse") {
|
||||
if (this.emexProxy) schedule.push("dataimpulse", "dataimpulse", "dataimpulse");
|
||||
if (this.emexFloxy) schedule.push("floxy", "floxy");
|
||||
@@ -417,6 +426,7 @@ export class EmexService {
|
||||
throw new Error(`EMEX HTTP ${res.status} for ${url}`);
|
||||
}
|
||||
if (provider === "floxy") {
|
||||
this.proxyHealth.reportFloxySuccess(); // Floxy answered — clear cooldown
|
||||
this.logger.log(`EMEX fetch via Floxy fallback succeeded for ${url}`);
|
||||
}
|
||||
return await res.text();
|
||||
@@ -446,6 +456,11 @@ export class EmexService {
|
||||
/fetch failed|ECONNRESET|ECONNREFUSED|EAI_AGAIN|socket hang up|other side closed|terminated|UND_ERR/i.test(
|
||||
`${e.message} ${String(e.cause ?? "")}`,
|
||||
));
|
||||
// A Floxy connect failure (dead exit / tunnel refused) trips the shared
|
||||
// gate so EMEX + pcat fail over to DataImpulse for the cooldown window.
|
||||
if (provider === "floxy" && isProxyConnectFailure(e)) {
|
||||
this.proxyHealth.reportFloxyFailure(`emex http: ${classifyTransportError(e)}`);
|
||||
}
|
||||
if (!transient || attempt === maxAttempts) break;
|
||||
// A Floxy transport failure means the current sticky exit IP is dead —
|
||||
// roll to a fresh session so the next Floxy attempt gets a new IP.
|
||||
@@ -909,6 +924,12 @@ export class EmexService {
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
|
||||
// Floxy tunnel dead mid-scrape → trip failover so the next acquirePage
|
||||
// re-establishes the session on DataImpulse.
|
||||
if (isProxyConnectFailure(err)) {
|
||||
void this.browserService.tripFloxyFailover(`emex decode: ${classifyTransportError(err)}`);
|
||||
}
|
||||
|
||||
if (
|
||||
err instanceof BadRequestException ||
|
||||
err instanceof ServiceUnavailableException ||
|
||||
@@ -1052,6 +1073,10 @@ export class EmexService {
|
||||
return { parts: [], schemaImageUrl: null, hotspots: [], schemaWidth: 0, schemaHeight: 0 };
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
// Floxy tunnel dead mid-scrape → trip failover for the next acquirePage.
|
||||
if (isProxyConnectFailure(err)) {
|
||||
void this.browserService.tripFloxyFailover(`emex parts: ${classifyTransportError(err)}`);
|
||||
}
|
||||
this.logger.error(`Failed to fetch category parts: ${err.message}`);
|
||||
return { parts: [], schemaImageUrl: null, hotspots: [], schemaWidth: 0, schemaHeight: 0 };
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user