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
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:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user