perf(emex): Floxy residential primary transport (was DataImpulse-first)
EMEX egress was DataImpulse-first: fetchEmexHtml tried 3 dead-prone DataImpulse ports before falling to the Floxy fallback, and the browser scraper launched on a random DataImpulse port. With ~50% of DataImpulse ports dead, this burned the 25s decode budget before reaching Floxy (observed: a Ford VIN where pcat correctly returned null fast, then EMEX's DataImpulse retries ate the budget). ssd tokens are replayed from emex's own HTML and are NOT strictly IP-bound on the .aspx endpoints (the prior random-port-per-call primary proved that), so a sticky Floxy IP across the flow is strictly safer. - EMEX_PROXY_PROVIDER (default "floxy" | "dataimpulse" | "none"). floxy → fetchEmexHtml schedule = [floxy, floxy, dataimpulse] (Floxy sticky primary, rolls IP on transport failure, single DataImpulse last-ditch). dataimpulse → legacy [di,di,di,floxy,floxy]. Default HTTP agent (image-dims) follows provider. - EmexBrowserService launches chromium on a sticky Floxy session (one residential exit IP for the browser lifetime; fresh id per relaunch) under the same flag. - Reuses the existing EMEX_FLOXY_* config (same Floxy account pcat now uses). Rollback: EMEX_PROXY_PROVIDER=dataimpulse. typecheck+biome+11 emex tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -61,22 +61,44 @@ export class EmexBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
private launching: Promise<void> | null = null;
|
||||
|
||||
private readonly useProxy: boolean;
|
||||
// "floxy" residential (default) | "dataimpulse" legacy | "none".
|
||||
private readonly proxyProvider: "floxy" | "dataimpulse" | "none";
|
||||
private readonly proxyHost: string;
|
||||
private readonly proxyPortStart: number;
|
||||
private readonly proxyPortEnd: number;
|
||||
private readonly proxyUsername: string;
|
||||
private readonly proxyPassword: string;
|
||||
// Floxy residential — a sticky session pins ONE exit IP for the browser's whole
|
||||
// lifetime (relaunch mints a fresh session id), which suits the chained scrape.
|
||||
private readonly floxyHost: string;
|
||||
private readonly floxyPort: number;
|
||||
private readonly floxyUser: string;
|
||||
private readonly floxyPass: string;
|
||||
private readonly floxyLifetime: number;
|
||||
private startedAt = 0;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
this.semaphore = new Semaphore(MAX_CONCURRENT_PAGES);
|
||||
|
||||
this.useProxy = this.configService.get<string>("EMEX_USE_PROXY", "true") === "true";
|
||||
const rawProvider = this.configService
|
||||
.get<string>("EMEX_PROXY_PROVIDER", "floxy")
|
||||
.toLowerCase();
|
||||
this.proxyProvider = !this.useProxy
|
||||
? "none"
|
||||
: rawProvider === "dataimpulse" || rawProvider === "none"
|
||||
? (rawProvider as "dataimpulse" | "none")
|
||||
: "floxy";
|
||||
this.proxyHost = this.configService.get<string>("EMEX_PROXY_HOST", "74.81.81.81");
|
||||
this.proxyPortStart = this.configService.get<number>("EMEX_PROXY_PORT_START", 10001);
|
||||
this.proxyPortEnd = this.configService.get<number>("EMEX_PROXY_PORT_END", 10099);
|
||||
this.proxyUsername = this.configService.get<string>("EMEX_PROXY_USER", "1726bbe361918676d44e");
|
||||
this.proxyPassword = this.configService.get<string>("EMEX_PROXY_PASS", "f11c7b6128cc86c6");
|
||||
this.floxyHost = this.configService.get<string>("EMEX_FLOXY_HOST", "residential.floxy.io");
|
||||
this.floxyPort = Number(this.configService.get("EMEX_FLOXY_PORT", 12321)) || 12321;
|
||||
this.floxyUser = this.configService.get<string>("EMEX_FLOXY_USER", "d739255e819b");
|
||||
this.floxyPass = this.configService.get<string>("EMEX_FLOXY_PASS", "9092873ba4e0");
|
||||
this.floxyLifetime = Number(this.configService.get("EMEX_FLOXY_LIFETIME", 300)) || 300;
|
||||
}
|
||||
|
||||
async onModuleInit(): Promise<void> {
|
||||
@@ -184,14 +206,30 @@ export class EmexBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
const executablePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH;
|
||||
if (executablePath) launchOptions.executablePath = executablePath;
|
||||
|
||||
if (this.useProxy) {
|
||||
if (this.proxyProvider === "floxy") {
|
||||
// Sticky Floxy session for the browser's lifetime → one residential exit IP
|
||||
// for the whole chained scrape; a fresh id is minted on each (re)launch.
|
||||
const sid = Math.random().toString(36).slice(2, 10);
|
||||
const pass =
|
||||
this.floxyLifetime > 0
|
||||
? `${this.floxyPass}_session-${sid}_lifetime-${this.floxyLifetime}`
|
||||
: this.floxyPass;
|
||||
launchOptions.proxy = {
|
||||
server: `http://${this.floxyHost}:${this.floxyPort}`,
|
||||
username: this.floxyUser,
|
||||
password: pass,
|
||||
};
|
||||
this.logger.log(
|
||||
`Using Floxy residential proxy: ${this.floxyHost}:${this.floxyPort} (sticky ${this.floxyLifetime}s)`,
|
||||
);
|
||||
} else if (this.proxyProvider === "dataimpulse") {
|
||||
const port = this.randomProxyPort();
|
||||
launchOptions.proxy = {
|
||||
server: `http://${this.proxyHost}:${port}`,
|
||||
username: this.proxyUsername,
|
||||
password: this.proxyPassword,
|
||||
};
|
||||
this.logger.log(`Using proxy: ${this.proxyHost}:${port}`);
|
||||
this.logger.log(`Using DataImpulse proxy: ${this.proxyHost}:${port}`);
|
||||
}
|
||||
|
||||
this.browser = await chromium.launch(launchOptions);
|
||||
|
||||
@@ -132,6 +132,10 @@ export class EmexService {
|
||||
private floxySessionId = Math.random().toString(36).slice(2, 10);
|
||||
private floxySessionBornMs = Date.now();
|
||||
private readonly emexDirectFallback: boolean;
|
||||
// Transport provider for EMEX egress (HTTP + image-dims). "floxy" residential
|
||||
// is primary by default (sticky session pins one exit IP across a chained flow),
|
||||
// DataImpulse is the last-ditch fallback. See constructor.
|
||||
private readonly emexProxyProvider: "floxy" | "dataimpulse" | "none";
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
@@ -169,15 +173,11 @@ export class EmexService {
|
||||
portStart: toPort("EMEX_PROXY_PORT_START", 10001),
|
||||
portEnd: toPort("EMEX_PROXY_PORT_END", 10099),
|
||||
};
|
||||
// Default agent for the low-stakes image-dims path; fetchEmexHtml builds a
|
||||
// fresh agent per request so a flaky port can't pin every call.
|
||||
this.proxyAgent = this.newProxyAgent();
|
||||
this.logger.log(
|
||||
`EMEX HTTP proxy enabled: ${this.emexProxy.host}:${this.emexProxy.portStart}-${this.emexProxy.portEnd}`,
|
||||
`EMEX DataImpulse fallback enabled: ${this.emexProxy.host}:${this.emexProxy.portStart}-${this.emexProxy.portEnd}`,
|
||||
);
|
||||
} else {
|
||||
this.emexProxy = null;
|
||||
this.proxyAgent = null;
|
||||
}
|
||||
|
||||
// Floxy residential fallback for when the DataImpulse pool flakes (connect
|
||||
@@ -200,6 +200,28 @@ export class EmexService {
|
||||
this.emexFloxy = null;
|
||||
}
|
||||
|
||||
// Transport provider. EMEX_PROXY_PROVIDER: "floxy" (default) → Floxy residential
|
||||
// sticky primary + a single DataImpulse last-ditch fallback; "dataimpulse" →
|
||||
// legacy DataImpulse-first order; "none" → direct. ssd tokens are replayed from
|
||||
// emex's own HTML and aren't strictly IP-bound on the .aspx endpoints (the old
|
||||
// random-port-per-call primary proved that), but a sticky Floxy IP across the
|
||||
// chained flow is still the safest choice — and avoids DataImpulse's ~50% dead
|
||||
// ports that were eating the decode budget before reaching the Floxy fallback.
|
||||
this.emexProxyProvider = !useProxy
|
||||
? "none"
|
||||
: (() => {
|
||||
const p = this.configService.get<string>("EMEX_PROXY_PROVIDER", "floxy").toLowerCase();
|
||||
return p === "dataimpulse" || p === "none" ? p : "floxy";
|
||||
})();
|
||||
// Default agent for the low-stakes image-dims path; fetchEmexHtml builds a
|
||||
// fresh agent per request so a flaky exit can't pin every call.
|
||||
this.proxyAgent =
|
||||
this.emexProxyProvider === "none"
|
||||
? null
|
||||
: (this.newProxyAgent(this.emexProxyProvider === "dataimpulse" ? "dataimpulse" : "floxy") ??
|
||||
this.newProxyAgent("dataimpulse"));
|
||||
this.logger.log(`EMEX transport provider: ${this.emexProxyProvider}`);
|
||||
|
||||
this.logger.log(`EMEX Service initialized with scraper path: ${this.scraperPath}`);
|
||||
}
|
||||
|
||||
@@ -347,14 +369,21 @@ export class EmexService {
|
||||
}
|
||||
|
||||
private async fetchEmexHtml(url: string): Promise<string> {
|
||||
// Attempt schedule: DataImpulse (rotating port, fresh agent each try to dodge
|
||||
// a flaky sticky port — ~42% blip rate, undecoded-vin-rca.md EMEX #1) first,
|
||||
// then the Floxy residential fallback when the DataImpulse pool throws
|
||||
// transport errors. A definitive HTTP answer (e.g. 404) stops the schedule —
|
||||
// it's a real result, and a different proxy IP must not "retry" it away.
|
||||
// Attempt schedule by provider. floxy (default): Floxy residential sticky
|
||||
// primary (one exit IP across the flow; rolls to a fresh IP on transport
|
||||
// failure), then a single DataImpulse last-ditch try — this avoids DataImpulse's
|
||||
// ~50% dead ports that used to burn the decode budget before reaching Floxy.
|
||||
// dataimpulse (legacy): DataImpulse-first (rotating port each try), Floxy after.
|
||||
// A definitive HTTP answer (e.g. 404) stops the schedule — it's a real result,
|
||||
// and a different proxy IP must not "retry" it away.
|
||||
const schedule: Array<"dataimpulse" | "floxy"> = [];
|
||||
if (this.emexProxy) schedule.push("dataimpulse", "dataimpulse", "dataimpulse");
|
||||
if (this.emexFloxy) schedule.push("floxy", "floxy");
|
||||
if (this.emexProxyProvider === "floxy") {
|
||||
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");
|
||||
}
|
||||
const maxAttempts = schedule.length || 1; // 0 → single proxy-less attempt
|
||||
let lastErr: unknown;
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
|
||||
Reference in New Issue
Block a user