perf(pl24): reaktif drill derinliğini sınırla, backfill'i jitter'lı aralıkla yavaşlat
Some checks are pending
QA Gate (P0/P1) / Test affected app (pull_request) Waiting to run

Faz 1 / adım 2b (analiz: /home/s/ss/plv2.md, bulgu consumers_jobs-03).

SORUN: Ban'ı süren hacim backfill değil, her yeni decode'da çalışan fast-lane
TAM AĞAÇ drill'iydi. 2026-08-18→09-04 arasında (PL24_TR_DISABLED main lane'i
park etmişken) yeni kategorilerin %100'ü aynı gün decode edilen araçlardan
geldi: günde 3-17 decode → 1.4k-6.8k kategori (bir Passat 1.251, bir L200
2.323). Araç başına drill derinliği sınırsızdı ve PL24 istekleri arasında hiç
bekleme yoktu.

DEĞİŞİKLİK:
- `PREFETCH_PL24_FAST_DEPTH` (varsayılan 1): PL24 reaktif/fast lane yalnız üst
  gruplar + doğrudan çocuklarını gezer. Daha derini ya kullanıcı o düğümü
  açtığında (lazy) ya da bütçeli backfill lane'inde çekilir. Diğer kaynaklar ve
  PL24 main lane MAX_DEPTH kullanmaya devam eder. Fast lane'in derinlik tavanına
  ulaşması normal durum olduğu için log seviyesi warn değil log.
- `PREFETCH_PL24_DELAY_MS` (varsayılan 8000) + ±%50 jitter: PL24 BACKFILL
  işlerine per-job bekleme. Kullanıcının fast lane'i asla yavaşlatılmaz.
  Metronom gibi düzenli akış otomasyon imzasıdır; jitter onu kırar.
- Telemetri düzeltmesi: `proxy_logs`'a ham hesap etiketi ("tr") yerine gerçekte
  kullanılan hesap yazılıyor (PL24_TR_DISABLED altında "de"). Yeni
  `PL24AuthService.activeAccount()`.

Test: `__testables` ile maxDepthFor/jitter dışa açıldı + derinlik tavanı testi.
Etkilenen paketler: 198 test geçti. tsc + biome temiz.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
semih
2026-09-17 13:44:59 +03:00
parent 56a8ea09be
commit 02ec3856d5
4 changed files with 88 additions and 4 deletions

View File

@@ -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<any | null> {
if (this.effectiveAccount(account) !== "de") return null;

View File

@@ -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,

View File

@@ -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);
});
});

View File

@@ -170,6 +170,40 @@ const SOURCE_DAILY_MAX: Record<string, number> = {
*/
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;
}