feat(prefetch/faz1): tam-derinlik (MAX_DEPTH 2→12) + no-op children job üretimini durdur

Hedef = decode edilmiş tüm araçların TAM kategori+parça ağacı DB'de. Cap artık
hedef değil güvenlik tavanı (gerçek ağaçlar ~5-7 derin; doğal yaprak tespiti
durduruyor). queueCategoryJob non-leaf dalı `depth < MAX_DEPTH` ile kapılandı:
processChildren'ın early-return'ünü aynalar → kuyruğun ~%85'ini oluşturan
boş depth-job'ları (rate-limit slotu harcayan no-op) hiç üretilmez.
+spec: depth>=cap'te children enqueue YOK, cap altında VAR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 11:17:36 +03:00
parent a36008017c
commit 61436db48f
2 changed files with 46 additions and 11 deletions

View File

@@ -175,4 +175,35 @@ describe("PrefetchWorkerService — fast lane (lifo) + backlog gating", () => {
expect(queue.add).not.toHaveBeenCalled();
});
});
describe("queueCategoryJob — depth cap (no-op guard)", () => {
// pl24 linkPath with no leaf marker (/bom/, /partinfo/ …) → treated as a
// non-leaf folder that would normally queue a prefetch-children job.
const nonLeaf = {
id: "c1",
linkPath: "/catalog/groups/x",
source: "pl24",
unavailable: false,
hasSubgroups: false,
};
type QCJ = {
queueCategoryJob: (c: unknown, v: string, s: string, d: number, f: boolean) => Promise<void>;
};
it("does NOT enqueue a children job at depth >= MAX_DEPTH (avoids a no-op)", async () => {
const { service, queue } = makeDeps({ waiting: 0, limitResults: [] });
// depth 12 == default MAX_DEPTH → processChildren would early-return, so the
// job must never be created.
await (service as never as QCJ).queueCategoryJob(nonLeaf, "v1", "pl24", 12, false);
expect(queue.add).not.toHaveBeenCalled();
});
it("enqueues a children job for a non-leaf within the cap", async () => {
// childCheck query (.limit(1)) returns no rows → enqueue a children fetch.
const { service, queue } = makeDeps({ waiting: 0, limitResults: [[]] });
await (service as never as QCJ).queueCategoryJob(nonLeaf, "v1", "pl24", 1, false);
expect(queue.add).toHaveBeenCalledTimes(1);
expect(queue.add.mock.calls[0][0]).toBe("prefetch-children");
});
});
});

View File

@@ -27,17 +27,18 @@ import { PrefetchCategoryJobData, PrefetchInitJobData } from "./prefetch.types";
import { CATALOG_PREFETCH_QUEUE } from "./queues/catalog-prefetch.queue";
/**
* How deep the background prefetch BFS drills the category tree.
* Depth cap for the prefetch BFS — a safety ceiling, NOT the target depth.
*
* Kept SHALLOW (2) on purpose: pre-warming the top ~2 levels for every decoded
* vehicle is bounded work that finishes in days, kills empty catalog pages, and
* makes the first clicks instant. Deeper categories are fetched lazily on-view
* (getCategoryTree in CategoriesService) and cached permanently — so the long
* tail nobody opens never costs an upstream request. Exhaustive depth-5
* pre-warm produced an unbounded BFS fan-out (~460k jobs) that never drained
* and starved the useful shallow + reactive work. Env-tunable.
* Goal is COMPLETE coverage: every decoded vehicle's full category tree + all
* parts in the DB so viewing is instant (DB read). The BFS terminates naturally
* at real leaves (isLeafLinkPath); this cap only guards against pathological /
* cyclic trees running away. Real PL24/EMEX/pcat trees are ~5-7 deep, so 12 is
* comfortably beyond every genuine leaf while still bounding a runaway.
* Efficiency is handled separately: queueCategoryJob never enqueues a children
* job past the cap (no early-return no-ops), and completed vehicles are skipped
* by the scan. Env-tunable (PREFETCH_MAX_DEPTH).
*/
const MAX_DEPTH = Number(process.env.PREFETCH_MAX_DEPTH) || 2;
const MAX_DEPTH = Number(process.env.PREFETCH_MAX_DEPTH) || 12;
// ── Backfill scan (hourly cron) tuning ──
/**
@@ -592,8 +593,11 @@ export class PrefetchWorkerService implements OnModuleInit, OnModuleDestroy {
fast,
});
}
} else if (cat.linkPath) {
// Check if children already exist
} else if (cat.linkPath && depth < MAX_DEPTH) {
// Non-leaf within the depth cap — explore children. The `depth < MAX_DEPTH`
// gate mirrors processChildren's early-return: without it we'd enqueue a
// prefetch-children job that processChildren just drops, burning a rate-limit
// slot on a no-op (this was ~85% of the queue at MAX_DEPTH=2).
const [childCheck] = await this.db
.select({ id: categories.id })
.from(categories)