fix(api): gate catalog backfill on prod host, not NODE_ENV

dev.sase.tr (staging) and sase.tr (prod) BOTH run NODE_ENV=production with
SEPARATE databases, so the previous NODE_ENV check would have let the hourly
backfill sweep run against the dev DB too. Gate on the canonical prod host
instead (COOLIFY_FQDN / BETTER_AUTH_URL), with an explicit
CATALOG_BACKFILL_ENABLED override. Default off for any unknown host.

New isCatalogBackfillEnabled() helper used by both the cron registration and
processBackfillScan; dev redeploy now removes the stale scheduler.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 14:29:07 +03:00
parent d0ee4df57f
commit 5f0bdb467d
3 changed files with 29 additions and 7 deletions

View File

@@ -1,6 +1,7 @@
import { Inject, Module, type OnModuleDestroy, type OnModuleInit } from "@nestjs/common";
import { Queue } from "bullmq";
import { CategoriesModule } from "../categories/categories.module";
import { isCatalogBackfillEnabled } from "./prefetch-utils";
import { PrefetchWorkerService } from "./prefetch-worker.service";
import {
CATALOG_PREFETCH_QUEUE,
@@ -92,8 +93,9 @@ export class JobsModule implements OnModuleInit, OnModuleDestroy {
// Catalog backfill: every hour at :00. Scans for decoded vehicles whose catalog
// isn't fully prefetched and queues them. Self-throttled (queue-depth guard),
// cooldown- and business-hours-aware — handled by PrefetchWorkerService.
// PRODUCTION ONLY: dev runs against a separate DB and must not sweep/scrape.
if (process.env.NODE_ENV === "production") {
// PRODUCTION (sase.tr) ONLY: dev.sase.tr is a separate DB and must not
// sweep/scrape. NODE_ENV is "production" on BOTH, so gate on the prod host.
if (isCatalogBackfillEnabled()) {
await this.catalogPrefetchQueue.upsertJobScheduler(
"catalog-backfill-hourly",
{ pattern: "0 * * * *" },
@@ -110,7 +112,7 @@ export class JobsModule implements OnModuleInit, OnModuleDestroy {
} else {
// Clean up any stale scheduler (e.g. if NODE_ENV changed) and stay idle.
await this.catalogPrefetchQueue.removeJobScheduler("catalog-backfill-hourly").catch(() => {});
console.log("[jobs] Skipped catalog-backfill cron (NODE_ENV != production)");
console.log("[jobs] Skipped catalog-backfill cron (not prod host)");
}
}

View File

@@ -1,5 +1,23 @@
import { RedisService } from "../redis/redis.service";
/**
* True ONLY on the real production deployment (sase.tr).
*
* NOTE: both dev.sase.tr (staging) and sase.tr (prod) run with
* `NODE_ENV=production` (hard-coded in the Dockerfile / compose) but have
* SEPARATE databases. So NODE_ENV cannot tell them apart. The catalog backfill
* must only sweep the prod DB, so we key off the canonical prod host. An
* explicit `CATALOG_BACKFILL_ENABLED` env wins (e.g. to force-test on staging).
* Anything else (local dev, unknown host) → disabled, the safe default.
*/
export function isCatalogBackfillEnabled(): boolean {
const flag = process.env.CATALOG_BACKFILL_ENABLED;
if (flag != null && flag !== "") return flag === "true";
return (
process.env.COOLIFY_FQDN === "sase.tr" || process.env.BETTER_AUTH_URL === "https://sase.tr"
);
}
/**
* Custom error that tells BullMQ to retry after a delay.
* The worker catches this and re-queues the job with the specified delay.

View File

@@ -17,6 +17,7 @@ import {
checkCooldown,
checkTimeWindow,
initProgress,
isCatalogBackfillEnabled,
isWithinTimeWindow,
updateProgress,
} from "./prefetch-utils";
@@ -293,10 +294,11 @@ export class PrefetchWorkerService implements OnModuleInit, OnModuleDestroy {
* business-hours window. Goal: no decoded vehicle is left without catalog data.
*/
private async processBackfillScan(): Promise<void> {
// Production only — dev uses a separate DB and must never sweep/scrape.
// Defense-in-depth in case a scan job lands here via a shared Redis/queue.
if (process.env.NODE_ENV !== "production") {
this.logger.log("[backfill] Skip — NODE_ENV != production");
// Production (sase.tr) only — dev.sase.tr uses a separate DB and must never
// sweep/scrape. Defense-in-depth in case a scan job lands here via a stale
// scheduler. NODE_ENV is "production" on both envs, so gate on the prod host.
if (!isCatalogBackfillEnabled()) {
this.logger.log("[backfill] Skip — not prod host");
return;
}