diff --git a/apps/api/src/integrations/pl24/pl24-auth.service.ts b/apps/api/src/integrations/pl24/pl24-auth.service.ts index 2a2c455..a22a0e5 100644 --- a/apps/api/src/integrations/pl24/pl24-auth.service.ts +++ b/apps/api/src/integrations/pl24/pl24-auth.service.ts @@ -659,6 +659,15 @@ export class PL24AuthService implements OnModuleInit { }; } + /** + * The account a request will really use (tr → de under PL24_TR_DISABLED). + * Telemetry must log this, not the raw argument, or `proxy_logs` claims the + * dead account served traffic. + */ + activeAccount(account: PL24Account): PL24Account { + return this.effectiveAccount(account); + } + /** Return ProxyAgent for account 'de', null for 'tr'. */ async getProxyAgent4Account(account: PL24Account): Promise { if (this.effectiveAccount(account) !== "de") return null; diff --git a/apps/api/src/integrations/pl24/pl24.service.ts b/apps/api/src/integrations/pl24/pl24.service.ts index cb79ead..8914af9 100644 --- a/apps/api/src/integrations/pl24/pl24.service.ts +++ b/apps/api/src/integrations/pl24/pl24.service.ts @@ -1003,13 +1003,14 @@ export class PL24Service { } await this.budget.consume("catalog"); const startedAt = Date.now(); + const activeAccount = this.authService.activeAccount(account); try { const response = await fetch(url, opts); this.budget.record({ kind: "catalog", url, proxied, - account, + account: activeAccount, statusCode: response.status, success: response.ok, startedAt, @@ -1020,7 +1021,7 @@ export class PL24Service { kind: "catalog", url, proxied, - account, + account: activeAccount, success: false, startedAt, error, diff --git a/apps/api/src/jobs/prefetch-worker.service.spec.ts b/apps/api/src/jobs/prefetch-worker.service.spec.ts index ddb333b..65c9843 100644 --- a/apps/api/src/jobs/prefetch-worker.service.spec.ts +++ b/apps/api/src/jobs/prefetch-worker.service.spec.ts @@ -314,3 +314,28 @@ describe("PrefetchWorkerService — fast lane (lifo) + backlog gating", () => { }); }); }); + +// ── PL24 reaktif drill derinliği + backfill pacing (plv2 Faz 1 / adım 2b) ── +// Ban'ı süren hacim, her yeni decode'da tüm ağacın gezilmesiydi (bir Passat = +// 1.251 kategori). Fast lane artık PL24'te 1. seviyede durur; derin drill ya +// kullanıcı tıklamasıyla ya da bütçeli backfill lane'inde olur. +describe("PrefetchWorkerService — PL24 derinlik tavanı", () => { + const load = async () => { + const mod = await import("./prefetch-worker.service"); + return mod as unknown as { + __testables?: { maxDepthFor(source: string, fast: boolean): number }; + }; + }; + + it("pl24 fast lane 1. seviyede durur, diğer kaynaklar tam derinlik kullanır", async () => { + // maxDepthFor modül-özel; davranışı dolaylı doğrula: env varsayılanları + process.env.PREFETCH_PL24_FAST_DEPTH = ""; + process.env.PREFETCH_MAX_DEPTH = ""; + const mod = await load(); + const fn = mod.__testables?.maxDepthFor; + if (!fn) return; // testable export yoksa atla (davranış e2e'de doğrulanır) + expect(fn("pl24", true)).toBe(1); + expect(fn("pl24", false)).toBeGreaterThan(1); + expect(fn("parts-catalogs", true)).toBeGreaterThan(1); + }); +}); diff --git a/apps/api/src/jobs/prefetch-worker.service.ts b/apps/api/src/jobs/prefetch-worker.service.ts index 0ead731..c93187c 100644 --- a/apps/api/src/jobs/prefetch-worker.service.ts +++ b/apps/api/src/jobs/prefetch-worker.service.ts @@ -170,6 +170,40 @@ const SOURCE_DAILY_MAX: Record = { */ const PCAT_PACE_MS = Number(process.env.PREFETCH_PCAT_DELAY_MS) || 1_500; +/** + * Per-job pacing for PL24 BACKFILL jobs (main lane only — a user waiting on a + * fresh decode must never be slowed down). Both account bans followed days of + * thousands of back-to-back PL24 calls; a paced, jittered stream looks nothing + * like that. Set 0 to disable. + */ +const PL24_PACE_MS = Number(process.env.PREFETCH_PL24_DELAY_MS) || 8_000; + +/** + * How deep the REACTIVE (fast-lane) drill may go for PL24. + * + * A freshly decoded vehicle used to be walked to the bottom immediately: one + * Passat produced 1,251 categories, one L200 2,323 — 1.4k-6.8k categories/day + * from 3-17 decodes, which is exactly the volume that preceded both bans + * (plv2.md §2.2). Depth 1 = top groups and their direct children; anything + * deeper is fetched lazily when the user actually opens that node, or by the + * budgeted backfill lane. Other sources keep MAX_DEPTH. + */ +const PL24_FAST_MAX_DEPTH = Number(process.env.PREFETCH_PL24_FAST_DEPTH) || 1; + +/** Depth ceiling for this source+lane. */ +function maxDepthFor(source: string, fast: boolean): number { + if (source === "pl24" && fast) return PL24_FAST_MAX_DEPTH; + return MAX_DEPTH; +} + +/** Jittered pace so our request stream is not a metronome. */ +function jitter(ms: number): number { + return Math.round(ms * (0.5 + Math.random())); +} + +/** Test-only surface for the pure helpers above. */ +export const __testables = { maxDepthFor, jitter }; + // ── Phase-1 residue exclusion ── /** * Skip a zero-parts vehicle once this many backfill attempts have completed with @@ -330,6 +364,15 @@ export class PrefetchWorkerService implements OnModuleInit, OnModuleDestroy { if (data.source === "parts-catalogs" && PCAT_PACE_MS > 0) { await new Promise((r) => setTimeout(r, PCAT_PACE_MS)); } + // PL24 backfill only: pace + jitter. The fast (user) lane is never delayed. + if ( + data.source === "pl24" && + PL24_PACE_MS > 0 && + !(job.data as { fast?: boolean }).fast && + (job.name === "prefetch-children" || job.name === "prefetch-parts") + ) { + await new Promise((r) => setTimeout(r, jitter(PL24_PACE_MS))); + } if (job.name === "backfill-scan") { return await this.processBackfillScan(); @@ -522,8 +565,14 @@ export class PrefetchWorkerService implements OnModuleInit, OnModuleDestroy { await checkCooldown(this.redis, source); checkTimeWindow(source); - if (depth >= MAX_DEPTH) { - this.logger.warn(`[prefetch] Max depth reached for category=${categoryId}`); + const depthCeiling = maxDepthFor(source, fast); + if (depth >= depthCeiling) { + // For the PL24 fast lane this is the normal stopping point, not a problem: + // deeper nodes are drilled lazily on user click or by the backfill lane. + const level = source === "pl24" && fast ? "log" : "warn"; + this.logger[level]( + `[prefetch] Depth ceiling ${depthCeiling} reached for category=${categoryId} (source=${source}, lane=${fast ? "fast" : "main"})`, + ); return; }