ソースを参照

refactor(ad): remove per-ad cache warmup logic and related dependencies

Dave 3 ヶ月 前
コミット
f28855ca79

+ 63 - 163
apps/box-app-api/src/feature/ads/ad.service.ts

@@ -1,7 +1,5 @@
 // apps/box-app-api/src/feature/ads/ad.service.ts
 import { Injectable, Logger, NotFoundException } from '@nestjs/common';
-import { ConfigService } from '@nestjs/config';
-import { HttpService } from '@nestjs/axios';
 import { RedisService } from '@box/db/redis/redis.service';
 import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
 import { tsCacheKeys } from '@box/common/cache/ts-cache-key.provider';
@@ -58,21 +56,13 @@ export interface GetAdForPlacementParams {
 @Injectable()
 export class AdService {
   private readonly logger = new Logger(AdService.name);
-  private readonly mgntApiBaseUrl: string;
 
   constructor(
     private readonly redis: RedisService,
     private readonly mongoPrisma: MongoPrismaService,
     private readonly rabbitmqPublisher: RabbitmqPublisherService,
-    private readonly configService: ConfigService,
-    private readonly httpService: HttpService,
     private readonly sysParamsService: SysParamsService,
-  ) {
-    // Get mgnt-api base URL for cache rebuild notifications
-    this.mgntApiBaseUrl =
-      this.configService.get<string>('MGNT_API_BASE_URL') ||
-      'http://localhost:3300';
-  }
+  ) {}
 
   /**
    * Core method for app-api:
@@ -109,14 +99,11 @@ export class AdService {
       usedIndexes.add(idx);
 
       const entry = pool[idx];
-      const adKey = tsCacheKeys.ad.byId(entry.id);
-
-      const cachedAd =
-        (await this.redis.getJson<CachedAd | null>(adKey)) ?? null;
+      const cachedAd = await this.fetchActiveAd(entry.id);
 
       if (!cachedAd) {
         this.logger.debug(
-          `getAdForPlacement: missing per-ad cache for adId=${entry.id}, key=${adKey}, poolKey=${poolKey}`,
+          `getAdForPlacement: missing active ad data for adId=${entry.id}, poolKey=${poolKey}`,
         );
         continue;
       }
@@ -228,6 +215,63 @@ export class AdService {
     };
   }
 
+  private async fetchActiveAd(adId: string): Promise<CachedAd | null> {
+    const now = nowSecBigInt();
+    const ad = await this.mongoPrisma.ads.findUnique({
+      where: { id: adId },
+      select: {
+        id: true,
+        adId: true,
+        adType: true,
+        advertiser: true,
+        title: true,
+        adsContent: true,
+        adsCoverImg: true,
+        adsUrl: true,
+        imgSource: true,
+        startDt: true,
+        expiryDt: true,
+        seq: true,
+        status: true,
+      },
+    });
+
+    if (!ad) {
+      this.logger.debug(`Ad not found in MongoDB: adId=${adId}`);
+      return null;
+    }
+
+    if (ad.status !== 1) {
+      this.logger.debug(`Ad is disabled: adId=${adId}`);
+      return null;
+    }
+
+    if (ad.startDt > now) {
+      this.logger.debug(`Ad not started yet: adId=${adId}`);
+      return null;
+    }
+
+    if (ad.expiryDt !== BigInt(0) && ad.expiryDt < now) {
+      this.logger.debug(`Ad expired: adId=${adId}`);
+      return null;
+    }
+
+    return {
+      id: ad.id,
+      adId: ad.adId,
+      adType: ad.adType,
+      advertiser: ad.advertiser,
+      title: ad.title,
+      adsContent: ad.adsContent,
+      adsCoverImg: ad.adsCoverImg,
+      adsUrl: ad.adsUrl,
+      imgSource: ad.imgSource,
+      startDt: ad.startDt,
+      expiryDt: ad.expiryDt,
+      seq: ad.seq,
+    };
+  }
+
   /**
    * Get all ads grouped by ad type.
    * Returns a list of all ad types (from SysParamsService.getAdTypes)
@@ -467,117 +511,12 @@ export class AdService {
    * Returns the cached ad data if found in Redis, otherwise queries MongoDB.
    * Note: Cache validation (status, date range) is handled during cache rebuild or query.
    */
-  async getAdByIdValidated(adsId: string): Promise<{
-    id: string;
-    adType: string;
-    adsUrl: string | null;
-    advertiser: string;
-    title: string;
-  } | null> {
-    const adKey = tsCacheKeys.ad.byId(adsId);
-
+  async getAdByIdValidated(adsId: string): Promise<CachedAd | null> {
     try {
-      // Try Redis cache first
-      const cachedAd = await this.redis.getJson<CachedAd | null>(adKey);
-
-      if (cachedAd) {
-        // Cache hit - return cached data
-        return {
-          id: cachedAd.id,
-          adType: cachedAd.adType,
-          adsUrl: cachedAd.adsUrl,
-          advertiser: cachedAd.advertiser ?? '',
-          title: cachedAd.title ?? '',
-        };
-      }
-
-      // Cache miss - fallback to MongoDB
-      this.logger.debug(
-        `Ad cache miss: adsId=${adsId}, key=${adKey}, falling back to MongoDB`,
-      );
-
-      const now = nowSecBigInt();
-      const ad = await this.mongoPrisma.ads.findUnique({
-        where: { id: adsId },
-        select: {
-          id: true,
-          adType: true,
-          advertiser: true,
-          title: true,
-          adsContent: true,
-          adsCoverImg: true,
-          adsUrl: true,
-          imgSource: true,
-          startDt: true,
-          expiryDt: true,
-          seq: true,
-          status: true,
-        },
-      });
-
-      if (!ad) {
-        this.logger.debug(`Ad not found in MongoDB: adsId=${adsId}`);
-        return null;
-      }
-
-      // Validate status (1 = enabled)
-      if (ad.status !== 1) {
-        this.logger.debug(`Ad is disabled: adsId=${adsId}`);
-        return null;
-      }
-
-      // Validate date range
-      if (ad.startDt > now) {
-        this.logger.debug(`Ad not started yet: adsId=${adsId}`);
-        return null;
-      }
-
-      // If expiryDt is 0, it means no expiry; otherwise check if expired
-      if (ad.expiryDt !== BigInt(0) && ad.expiryDt < now) {
-        this.logger.debug(`Ad expired: adsId=${adsId}`);
-        return null;
-      }
-
-      // Cache the ad for future requests (fire-and-forget)
-      const cacheData: CachedAd = {
-        id: ad.id,
-        adType: ad.adType,
-        advertiser: ad.advertiser,
-        title: ad.title,
-        adsContent: ad.adsContent,
-        adsCoverImg: ad.adsCoverImg,
-        adsUrl: ad.adsUrl,
-        imgSource: ad.imgSource,
-        startDt: ad.startDt,
-        expiryDt: ad.expiryDt,
-        seq: ad.seq,
-      };
-
-      // Warm cache in Redis (fire-and-forget)
-      this.redis.setJson(adKey, cacheData, 300).catch((err) => {
-        this.logger.warn(
-          `Failed to warm Redis cache for adsId=${adsId}: ${err instanceof Error ? err.message : String(err)}`,
-        );
-      });
-
-      // Also notify mgnt-api to persist cache rebuild for durability (fire-and-forget)
-      // This ensures the ad remains cached even if Redis is cleared
-      this.notifyCacheSyncForAdRefresh(ad.id, ad.adType).catch((err) => {
-        this.logger.debug(
-          `Failed to notify mgnt-api for cache rebuild: ${err instanceof Error ? err.message : String(err)}`,
-        );
-      });
-
-      return {
-        id: ad.id,
-        adType: ad.adType,
-        adsUrl: ad.adsUrl,
-        advertiser: ad.advertiser ?? '',
-        title: ad.title ?? '',
-      };
+      return await this.fetchActiveAd(adsId);
     } catch (err) {
       this.logger.error(
-        `Error fetching ad: adsId=${adsId}, key=${adKey}`,
+        `Error fetching ad: adsId=${adsId}`,
         err instanceof Error ? err.stack : String(err),
       );
       return null;
@@ -738,43 +677,4 @@ export class AdService {
       `Initiated stats.ad.impression publish for adId=${body.adId}, uid=${uid}`,
     );
   }
-
-  /**
-   * Notify mgnt-api to schedule a cache rebuild for a specific ad.
-   * This is called when an ad is loaded from MongoDB (cache miss),
-   * so that mgnt-api can persist the cache rebuild for durability.
-   * Uses fire-and-forget with timeout to avoid blocking requests.
-   *
-   * @param adId - The ad ID to refresh
-   * @param adType - The ad type for pool rebuild
-   */
-  private async notifyCacheSyncForAdRefresh(
-    adId: string,
-    adType: string,
-  ): Promise<void> {
-    try {
-      const url = `${this.mgntApiBaseUrl}/mgnt-debug/cache-sync/ad/refresh`;
-      const timeout = 5000; // 5 second timeout
-      const controller = new AbortController();
-      const timeoutId = setTimeout(() => controller.abort(), timeout);
-
-      const response = await this.httpService.axiosRef.post(
-        url,
-        { adId, adType },
-        { signal: controller.signal },
-      );
-
-      clearTimeout(timeoutId);
-
-      this.logger.debug(
-        `Successfully notified mgnt-api to rebuild cache for adId=${adId}`,
-      );
-    } catch (err) {
-      // Log but don't fail - this is best-effort
-      const errorMsg = err instanceof Error ? err.message : String(err);
-      this.logger.debug(
-        `Failed to notify mgnt-api for cache rebuild (adId=${adId}): ${errorMsg}`,
-      );
-    }
-  }
 }

+ 45 - 23
apps/box-app-api/src/feature/homepage/homepage.service.ts

@@ -2,6 +2,7 @@
 import { Injectable, Logger } from '@nestjs/common';
 import { RedisService } from '@box/db/redis/redis.service';
 import { tsCacheKeys } from '@box/common/cache/ts-cache-key.provider';
+import { nowSecBigInt } from '@box/common/time/time.util';
 import { AdType } from '@prisma/mongo/client';
 import type { AdPoolEntry } from '@box/common/ads/ad-types';
 import { PrismaMongoService } from '../../prisma/prisma-mongo.service';
@@ -24,16 +25,6 @@ import {
 } from './homepage.constants';
 import type { HomeCategoryCacheItem, HomeTagCacheItem } from './homepage.types';
 
-interface CachedAd {
-  id: string;
-  advertiser?: string;
-  title?: string;
-  adsContent?: string | null;
-  adsCoverImg?: string | null;
-  adsUrl?: string | null;
-  adType?: string | null;
-}
-
 @Injectable()
 export class HomepageService {
   private readonly logger = new Logger(HomepageService.name);
@@ -206,22 +197,53 @@ export class HomepageService {
    * Fetch ad details from per-ad cache
    */
   private async fetchAdDetails(adId: string): Promise<HomeAdDto | null> {
-    const cacheKey = tsCacheKeys.ad.byId(adId);
-    const cached = await this.redis.getJson<CachedAd>(cacheKey);
+    try {
+      const now = nowSecBigInt();
+      const ad = await this.prisma.ads.findUnique({
+        where: { id: adId },
+        select: {
+          id: true,
+          adType: true,
+          advertiser: true,
+          title: true,
+          adsContent: true,
+          adsCoverImg: true,
+          adsUrl: true,
+          startDt: true,
+          expiryDt: true,
+          status: true,
+        },
+      });
+
+      if (!ad) {
+        this.logger.debug(`Ad not found for homepage slot: adId=${adId}`);
+        return null;
+      }
+
+      if (ad.status !== 1 || ad.startDt > now) {
+        return null;
+      }
+
+      if (ad.expiryDt !== BigInt(0) && ad.expiryDt < now) {
+        return null;
+      }
 
-    if (!cached) {
+      return {
+        id: ad.id,
+        adType: ad.adType ?? 'UNKNOWN',
+        title: ad.title ?? '',
+        advertiser: ad.advertiser ?? '',
+        content: ad.adsContent ?? undefined,
+        coverImg: ad.adsCoverImg ?? undefined,
+        targetUrl: ad.adsUrl ?? undefined,
+      };
+    } catch (err) {
+      this.logger.error(
+        `Failed to fetch ad ${adId}`,
+        err instanceof Error ? err.stack : String(err),
+      );
       return null;
     }
-
-    return {
-      id: cached.id,
-      adType: cached.adType ?? 'UNKNOWN',
-      title: cached.title ?? '',
-      advertiser: cached.advertiser ?? '',
-      content: cached.adsContent ?? undefined,
-      coverImg: cached.adsCoverImg ?? undefined,
-      targetUrl: cached.adsUrl ?? undefined,
-    };
   }
 
   /**

+ 1 - 119
apps/box-mgnt-api/src/cache-sync/cache-sync.service.ts

@@ -24,9 +24,6 @@ import {
 } from './cache-sync.types';
 import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
 
-// Cache TTL (seconds)
-const AD_CACHE_TTL = 300; // 5 min (more dynamic)
-
 /**
  * CacheSyncService - Refactored as orchestration wrapper
  *  - Thin wrapper that delegates to builder classes.
@@ -475,126 +472,11 @@ export class CacheSyncService {
   // ─────────────────────────────────────────────
 
   private async handleAdAction(action: CacheSyncAction): Promise<void> {
-    const payload = action.payload as (CachePayload & { adId?: string }) | null;
-
-    if (!payload?.adId) {
-      this.logger.warn(
-        `handleAdAction: missing adId in payload for action id=${action.id}`,
-      );
-      return;
-    }
-
-    const adId = payload.adId;
-    const adType = payload.type;
-
-    switch (action.operation as CacheOperation) {
-      case CacheOperation.INVALIDATE: {
-        try {
-          const key = tsCacheKeys.ad.byId(adId);
-          await this.redis.del(key);
-          this.logger.log(`Invalidated per-ad cache key=${key}`);
-        } catch (err) {
-          this.logger.error(
-            `Failed to invalidate ad cache for adId=${adId}`,
-            err,
-          );
-          throw err;
-        }
-        break;
-      }
-      case CacheOperation.REFRESH:
-      default:
-        await this.rebuildSingleAdCache(adId, adType);
-        break;
-    }
-
     this.logger.debug(
-      `handleAdAction: rebuilt per-ad cache for adId=${adId}, adType=${adType ?? 'N/A'}, action id=${action.id}`,
+      `handleAdAction: per-ad cache operations disabled; ignoring action id=${action.id}, op=${action.operation}`,
     );
   }
 
-  // Still private, only used internally for per-ad refresh logic.
-  private async rebuildSingleAdCache(
-    adId: string,
-    adType?: string,
-  ): Promise<void> {
-    const now = this.nowBigInt();
-
-    // Fetch the ad by Mongo ObjectId
-    const ad = await this.mongoPrisma.ads.findUnique({
-      where: { id: adId },
-    });
-
-    const cacheKey = tsCacheKeys.ad.byId(adId);
-
-    if (!ad) {
-      // Ad no longer exists → ensure cache is cleared
-      try {
-        await this.redis.del(cacheKey);
-        this.logger.log(
-          `rebuildSingleAdCache: ad not found, removed cache key=${cacheKey}`,
-        );
-      } catch (err) {
-        this.logger.error(
-          `Failed to delete Redis key ${cacheKey} for missing ad`,
-          err,
-        );
-      }
-      return;
-    }
-
-    // Validate business rules:
-    // - status = 1 (enabled)
-    // - startDt <= now
-    // - expiryDt == 0 (no expiry) OR expiryDt >= now
-    const isActive =
-      ad.status === 1 &&
-      ad.startDt <= now &&
-      (ad.expiryDt === BigInt(0) || ad.expiryDt >= now);
-
-    if (!isActive) {
-      try {
-        await this.redis.del(cacheKey);
-        this.logger.log(
-          `rebuildSingleAdCache: adId=${adId} is not active (status/time window), removed cache key=${cacheKey}`,
-        );
-      } catch (err) {
-        this.logger.error(
-          `Failed to delete Redis key ${cacheKey} for inactive ad`,
-          err,
-        );
-      }
-      return;
-    }
-
-    // Decide what to store in per-ad cache.
-    // You can store the full ad document or a trimmed DTO.
-    // For now, let's store the full ad + its module's adType.
-    const cachedAd = {
-      id: ad.id,
-      advertiser: ad.advertiser,
-      title: ad.title,
-      adsContent: ad.adsContent ?? null,
-      adsCoverImg: ad.adsCoverImg ?? null,
-      adsUrl: ad.adsUrl ?? null,
-      imgSource: ad.imgSource ?? null,
-      adType: ad.adType ?? null,
-      startDt: ad.startDt,
-      expiryDt: ad.expiryDt,
-      seq: ad.seq ?? 0,
-    };
-
-    try {
-      await this.redis.setJson(cacheKey, cachedAd, AD_CACHE_TTL);
-      this.logger.log(
-        `rebuildSingleAdCache: updated per-ad cache for adId=${adId}, key=${cacheKey}`,
-      );
-    } catch (err) {
-      this.logger.error(`Failed to set Redis cache for ad adId=${adId}`, err);
-      throw err;
-    }
-  }
-
   private async handleAdPoolAction(action: CacheSyncAction): Promise<void> {
     const payload = action.payload as CachePayload | null;
     const adType = payload?.type as AdType | undefined;

+ 0 - 2
libs/common/src/cache/cache-keys.ts

@@ -45,8 +45,6 @@ export const CacheKeys = {
   // ─────────────────────────────────────────────
   // ADS
   // ─────────────────────────────────────────────
-  appAdById: (adId: string | number): string => `box:app:ad:by-id:${adId}`,
-
   // ─────────────────────────────────────────────
   // AD POOLS (AdType-based)
   // ─────────────────────────────────────────────

+ 3 - 4
libs/common/src/cache/cache-semantics.constants.ts

@@ -2,10 +2,9 @@
  * Redis cache key semantic types and constants.
  *
  * This file provides TypeScript types and constants to enforce semantic correctness
- * when working with Redis cache keys. Use these to ensure:
- * - Video ID keys always contain video IDs (strings), never JSON
- * - Tag keys always contain Tag JSON, not video IDs
- * - Operations match the key type (LRANGE for LIST, ZREVRANGE for ZSET, etc.)
+ * when working with Redis cache keys. Use these to ensure video ID keys only contain
+ * video IDs (strings), tag keys always carry Tag JSON data, and the operations match
+ * the key type (e.g., LRANGE for LIST, ZREVRANGE for ZSET).
  *
  * SEE: libs/common/src/cache/CACHE_SEMANTICS.md for detailed documentation
  */

+ 15 - 32
libs/common/src/cache/ts-cache-key.provider.ts

@@ -17,13 +17,11 @@ export type { VideoSortKey, VideoHomeSectionKey };
  * USAGE & SEMANTICS
  * ═══════════════════════════════════════════════════════════════════════════
  *
- * Use this provider instead of directly calling CacheKeys functions.
- * It provides:
- * - Better IDE autocomplete and discoverability
- * - Grouped access by entity type (video, tag, category, etc.)
- * - Type safety for parameters
+ * Use this provider instead of directly calling CacheKeys functions. It gives better IDE
+ * autocomplete and discoverability, grouped access by entity type (video, tag, category, etc.),
+ * and type safety for parameters.
  *
- * Example:
+ * Example usage:
  *   const categoryListKey = tsCacheKeys.video.categoryList('channel-1');
  *   const items = await redis.lrange(categoryListKey, 0, -1);
  *
@@ -31,12 +29,12 @@ export type { VideoSortKey, VideoHomeSectionKey };
  *
  * KEY SEMANTICS:
  * ──────────────
- * - video.categoryList() → LIST of VIDEO IDs (strings only)
- * - video.tagList() → LIST of VIDEO IDs (strings only)
- * - video.categoryPool() → ZSET of video IDs with score
- * - video.tagPool() → ZSET of video IDs with score
- * - tag.all() → JSON array of Tag objects
- * - video.detail() → JSON single video object
+ * video.categoryList() returns a LIST of VIDEO IDs (strings only).
+ * video.tagList() returns a LIST of VIDEO IDs (strings only).
+ * video.categoryPool() returns a ZSET of video IDs with scores.
+ * video.tagPool() returns a ZSET of video IDs with scores.
+ * tag.all() returns a JSON array of Tag objects.
+ * video.detail() returns a JSON single video object.
  */
 export interface TsCacheKeyBuilder {
   /**
@@ -68,9 +66,8 @@ export interface TsCacheKeyBuilder {
   /**
    * Tag-related cache keys.
    *
-   * ⚠️ CRITICAL: Distinguish between:
-   * - tag.all() → Global TAG POOL (contains Tag JSON objects)
-   * - video.tagList() → Video IDs filtered by tag (contains VIDEO IDs only)
+   * ⚠️ CRITICAL: Distinguish between tag.all() (a global TAG POOL that contains Tag JSON objects)
+   * and video.tagList() (which returns video IDs filtered by tag and contains VIDEO IDs only).
    *
    * SEE: libs/common/src/cache/CACHE_SEMANTICS.md
    */
@@ -110,8 +107,6 @@ export interface TsCacheKeyBuilder {
    * Ad-related cache keys.
    */
   ad: {
-    /** Get ad by ID. */
-    byId(adId: string | number): string;
     /** Get ad pool by type. */
     poolByType(adType: AdType | string): string;
   };
@@ -119,22 +114,11 @@ export interface TsCacheKeyBuilder {
   /**
    * Video-related cache keys.
    *
-   * ⚠️ CRITICAL: Understand the distinction between "list" and "pool" keys:
+   * ⚠️ CRITICAL: Understand the distinction between "list" and "pool" keys.
    *
-   * LIST keys (video.categoryList, video.tagList):
-   *   - Redis Type: LIST
-   *   - Elements: Video IDs ONLY (strings like "64a2b3c4d5e6f7g8h9i0j1k2")
-   *   - Order: Business sequence
-   *   - Operations: LRANGE, RPUSH
-   *   - Use: Return all videos in category/tag to client
+   * LIST keys (video.categoryList, video.tagList) map to Redis LISTs that store video IDs only (strings like "64a2b3c4d5e6f7g8h9i0j1k2") in business sequence order and operate via LRANGE/RPUSH to return videos for categories or tags.
    *
-   * POOL keys (video.categoryPool, video.tagPool):
-   *   - Redis Type: ZSET (sorted set)
-   *   - Members: Video IDs (strings)
-   *   - Scores: Timestamp for sorting
-   *   - Order: Latest first (ZREVRANGE)
-   *   - Operations: ZREVRANGE, ZADD
-   *   - Use: Pagination with sort order
+   * POOL keys (video.categoryPool, video.tagPool) map to Redis ZSETs whose members are video IDs with timestamp scores sorted from newest to oldest; they rely on ZREVRANGE/ZADD for pagination.
    *
    * SEE: libs/common/src/cache/CACHE_SEMANTICS.md for complete documentation
    */
@@ -254,7 +238,6 @@ export function createTsCacheKeyBuilder(): TsCacheKeyBuilder {
         CacheKeys.appTagByCategoryKey(categoryId),
     },
     ad: {
-      byId: (adId) => CacheKeys.appAdById(adId),
       poolByType: (adType) => CacheKeys.appAdPoolByType(adType),
     },
     video: {

+ 7 - 25
libs/common/src/cache/video-cache.helper.ts

@@ -87,26 +87,11 @@ export function parseVideoPayload(value: string | null): VideoPayload | null {
 /**
  * VideoCacheHelper provides type-safe, centralized Redis operations for video cache keys.
  *
- * KEY TYPE EXPECTATIONS:
- * - Video ID lists (category, tag, pool): Redis LIST containing video IDs (24-char hex strings)
- * - Tag metadata lists: Redis LIST containing Tag JSON objects
- * - Video details: Redis STRING containing Video JSON object
+ * Key type expectations cover video ID lists (category, tag, pool) stored as Redis LIST entries of 24-character hex video IDs, tag metadata lists stored as Redis LISTs containing Tag JSON objects, and video details saved as Redis STRING JSON values.
  *
- * This helper ensures:
- * 1. Consistent Redis command usage (LIST commands for lists, not GET/SET)
- * 2. Type safety for video IDs vs metadata
- * 3. Centralized error handling and logging
- * 4. TTL management
+ * The helper enforces consistent Redis command usage (prefers LIST operations over GET/SET), maintains type safety between video IDs and metadata, centralizes error handling/logging, and manages TTL policies.
  *
- * @example
- * // Save video IDs to a category list
- * await helper.saveVideoIdList('box:app:video:category:list:123', ['vid1', 'vid2'], 3600);
- *
- * // Read video IDs with pagination
- * const videoIds = await helper.getVideoIdList('box:app:video:category:list:123', 0, 19);
- *
- * // Read tag metadata
- * const tags = await helper.getTagListForCategory('box:app:tag:list:123');
+ * Example usage includes saving video IDs to a category list with `saveVideoIdList('box:app:video:category:list:123', ['vid1', 'vid2'], 3600)`, reading video IDs via `getVideoIdList('box:app:video:category:list:123', 0, 19)`, and reading tag metadata through `getTagListForCategory('box:app:tag:list:123')`.
  */
 @Injectable()
 export class VideoCacheHelper {
@@ -117,13 +102,10 @@ export class VideoCacheHelper {
   /**
    * Save a list of video IDs to a Redis LIST key.
    *
-   * REDIS OPERATIONS:
-   * 1. DEL key (remove existing data)
-   * 2. RPUSH key videoId1 videoId2 ... (push all IDs)
-   * 3. EXPIRE key ttlSeconds (set TTL if provided)
+   * REDIS OPERATIONS: delete the existing key, push all IDs with RPUSH, and optionally set TTL if requested.
    *
-   * KEY TYPE: LIST
-   * VALUE TYPE: Video IDs (24-character hex strings, MongoDB ObjectId format)
+   * KEY TYPE: LIST, holding video IDs as 24-character hex strings (MongoDB ObjectId format).
+   * VALUE TYPE: the video IDs themselves.
    *
    * @param key - Redis key (e.g., 'box:app:video:category:list:{categoryId}')
    * @param videoIds - Array of video IDs to store
@@ -131,7 +113,7 @@ export class VideoCacheHelper {
    *
    * @throws Error if Redis operations fail
    *
-   * @example
+   * Example call:
    * await helper.saveVideoIdList(
    *   'box:app:video:category:list:abc123',
    *   ['507f1f77bcf86cd799439011', '507f1f77bcf86cd799439012'],

+ 1 - 2
libs/common/src/utils/image-lib.ts

@@ -9,8 +9,7 @@ export const ENCRYPTED_HEADERS: Uint8Array[] = [
 
 /**
  * Pick a header index:
- * - if headerIndex is provided, use that
- * - otherwise pick one randomly from ENCRYPTED_HEADERS
+ * Prefer the provided headerIndex when it is valid; otherwise choose one randomly from ENCRYPTED_HEADERS.
  */
 function pickHeaderIndex(headerIndex?: number): number {
   if (

+ 9 - 189
libs/core/src/ad/ad-cache-warmup.service.ts

@@ -1,204 +1,24 @@
 // libs/core/src/ad/ad-cache-warmup.service.ts
 import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
-import { tsCacheKeys } from '@box/common/cache/ts-cache-key.provider';
-import { RedisService } from '@box/db/redis/redis.service';
-import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
-import type { AdType } from '@box/common/ads/ad-types';
-import { nowSecBigInt } from '@box/common/time/time.util';
-
-interface CachedAd {
-  id: string;
-  adId: number;
-  adType: string;
-  advertiser: string;
-  title: string;
-  adsContent: string | null;
-  adsCoverImg: string | null;
-  adsUrl: string | null;
-  imgSource: string | null;
-  startDt: bigint;
-  expiryDt: bigint;
-  seq: number;
-}
 
 /**
- * AdCacheWarmupService
- *
- * Responsible for warming up individual ad caches (app:ad:by-id:${adId})
- * on application startup or on-demand.
- *
- * This complements AdPoolWarmupService which handles ad pools.
+ * Formerly responsible for per-ad cache warmups.
+ * Per-ad cache keys have been removed, so this service now
+ * short-circuits and merely logs its disabled state to satisfy module wiring.
  */
 @Injectable()
 export class AdCacheWarmupService implements OnModuleInit {
   private readonly logger = new Logger(AdCacheWarmupService.name);
-  private readonly AD_CACHE_TTL = 300; // 5 minutes
-
-  constructor(
-    private readonly redis: RedisService,
-    private readonly mongoPrisma: MongoPrismaService,
-  ) {}
 
   async onModuleInit(): Promise<void> {
-    try {
-      this.logger.log('Individual ad cache warmup starting...');
-      await this.warmupAllAdCaches();
-    } catch (err) {
-      this.logger.error(
-        'Individual ad cache warmup encountered an error but will not block startup',
-        err instanceof Error ? err.stack : String(err),
-      );
-    }
+    this.logger.log(
+      'Individual ad cache warmup is disabled; no action is taken on startup.',
+    );
   }
 
-  /**
-   * Warm up all active ads' individual caches.
-   * Only caches ads that are enabled and within their date range.
-   */
   async warmupAllAdCaches(): Promise<void> {
-    const startTime = Date.now();
-    const now = nowSecBigInt();
-
-    try {
-      // Fetch all active ads
-      const ads = await this.mongoPrisma.ads.findMany({
-        where: {
-          status: 1, // enabled
-          // startDt: { lte: now },
-          // OR: [{ expiryDt: BigInt(0) }, { expiryDt: { gte: now } }],
-        },
-        orderBy: { seq: 'asc' },
-        select: {
-          id: true,
-          adId: true,
-          adType: true,
-          advertiser: true,
-          title: true,
-          adsContent: true,
-          adsCoverImg: true,
-          adsUrl: true,
-          imgSource: true,
-          startDt: true,
-          expiryDt: true,
-          seq: true,
-        },
-      });
-
-      this.logger.log(`Found ${ads.length} active ads to cache`);
-
-      let successCount = 0;
-      let errorCount = 0;
-
-      // Cache each ad individually
-      for (const ad of ads) {
-        try {
-          await this.cacheAd(ad.id, {
-            id: ad.id,
-            adId: ad.adId,
-            adType: ad.adType,
-            advertiser: ad.advertiser,
-            title: ad.title,
-            adsContent: ad.adsContent ?? null,
-            adsCoverImg: ad.adsCoverImg ?? null,
-            adsUrl: ad.adsUrl ?? null,
-            imgSource: ad.imgSource ?? null,
-            startDt: ad.startDt,
-            expiryDt: ad.expiryDt,
-            seq: ad.seq,
-          });
-          successCount++;
-        } catch (err) {
-          errorCount++;
-          this.logger.warn(
-            `Failed to cache ad ${ad.id}: ${err instanceof Error ? err.message : String(err)}`,
-          );
-        }
-      }
-
-      const duration = Date.now() - startTime;
-      this.logger.log(
-        `Ad cache warmup completed: ${successCount} cached, ${errorCount} errors, ${duration}ms`,
-      );
-    } catch (err) {
-      this.logger.error(
-        'Ad cache warmup failed',
-        err instanceof Error ? err.stack : String(err),
-      );
-      throw err;
-    }
-  }
-
-  /**
-   * Cache a single ad by ID
-   */
-  private async cacheAd(adId: string, cachedAd: CachedAd): Promise<void> {
-    const key = tsCacheKeys.ad.byId(adId);
-    await this.redis.setJson(key, cachedAd, this.AD_CACHE_TTL);
-  }
-
-  /**
-   * Warm up a single ad cache (used for on-demand refresh)
-   */
-  async warmupSingleAd(adId: string): Promise<void> {
-    const now = nowSecBigInt();
-
-    const ad = await this.mongoPrisma.ads.findUnique({
-      where: { id: adId },
-      select: {
-        id: true,
-        adId: true,
-        adType: true,
-        advertiser: true,
-        title: true,
-        adsContent: true,
-        adsCoverImg: true,
-        adsUrl: true,
-        imgSource: true,
-        startDt: true,
-        expiryDt: true,
-        seq: true,
-        status: true,
-      },
-    });
-
-    if (!ad) {
-      // Ad doesn't exist - remove from cache
-      const key = tsCacheKeys.ad.byId(adId);
-      await this.redis.del(key);
-      this.logger.debug(`Ad ${adId} not found, removed from cache`);
-      return;
-    }
-
-    // Check if ad is active
-    const isActive =
-      ad.status === 1 &&
-      ad.startDt <= now &&
-      (ad.expiryDt === BigInt(0) || ad.expiryDt >= now);
-
-    if (!isActive) {
-      // Ad is not active - remove from cache
-      const key = tsCacheKeys.ad.byId(adId);
-      await this.redis.del(key);
-      this.logger.debug(`Ad ${adId} is not active, removed from cache`);
-      return;
-    }
-
-    // Cache the ad
-    await this.cacheAd(adId, {
-      id: ad.id,
-      adId: ad.adId,
-      adType: ad.adType,
-      advertiser: ad.advertiser,
-      title: ad.title,
-      adsContent: ad.adsContent ?? null,
-      adsCoverImg: ad.adsCoverImg ?? null,
-      adsUrl: ad.adsUrl ?? null,
-      imgSource: ad.imgSource ?? null,
-      startDt: ad.startDt,
-      expiryDt: ad.expiryDt,
-      seq: ad.seq,
-    });
-
-    this.logger.debug(`Cached ad ${adId}`);
+    this.logger.debug(
+      'warmupAllAdCaches skipped because per-ad cache keys no longer exist.',
+    );
   }
 }