Prechádzať zdrojové kódy

feat(cache): enhance cache handling for ad pools and implement tracking ID generation

Dave 2 mesiacov pred
rodič
commit
d1791bc307

+ 16 - 3
apps/box-mgnt-api/src/cache-sync/cache-checklist.service.ts

@@ -73,9 +73,22 @@ export class CacheChecklistService implements OnApplicationBootstrap {
       }
 
       if (exists) {
-        const json = await this.redis.getJson<unknown>(key);
-        if (Array.isArray(json)) {
-          items = json.length;
+        try {
+          // Check if this is an ad pool key (SET type) or regular key (JSON type)
+          if (key.startsWith('app:adpool:')) {
+            // Ad pools are stored as Redis SETs; get cardinality
+            const scard = await this.redis.scard(key);
+            items = scard ?? 0;
+          } else {
+            // Other keys are JSON arrays
+            const json = await this.redis.getJson<unknown>(key);
+            if (Array.isArray(json)) {
+              items = json.length;
+            }
+          }
+        } catch (e) {
+          // Log but don't fail the check if we can't introspect
+          error = e instanceof Error ? e.message : String(e);
         }
       }
 

+ 3 - 3
apps/box-mgnt-api/src/cache-sync/cache-sync.module.ts

@@ -5,10 +5,10 @@ import { CacheSyncDebugController } from './cache-sync-debug.controller';
 import { CacheChecklistService } from './cache-checklist.service';
 import { CacheChecklistController } from './cache-checklist.controller';
 
+import { CacheManagerModule } from '@box/core/cache/cache-manager.module';
+
 @Module({
-  imports: [
-    // RedisModule is now global, no need to import
-  ],
+  imports: [CacheManagerModule],
   controllers: [CacheSyncDebugController, CacheChecklistController],
   providers: [CacheSyncService, CacheChecklistService],
   exports: [CacheSyncService, CacheChecklistService],

+ 13 - 2
apps/box-mgnt-api/src/cache-sync/cache-sync.service.ts

@@ -718,8 +718,19 @@ export class CacheSyncService {
       // Rebuild tags
       await this.rebuildTagAll();
 
-      // Rebuild all ad pools
-      await this.rebuildAllAdPools();
+      // Rebuild all ad pools by iterating AdType enum
+      const allAdTypes = Object.values(PrismaAdType) as AdType[];
+      for (const adType of allAdTypes) {
+        try {
+          await this.rebuildAdPoolForType(adType);
+        } catch (err) {
+          this.logger.error(
+            `Failed to rebuild ad pool for adType=${adType} during cache warming`,
+            err instanceof Error ? err.stack : String(err),
+          );
+          // Continue with other ad types even if one fails
+        }
+      }
 
       this.logger.log(`Cache warming complete in ${Date.now() - start}ms`);
     } catch (err) {

+ 18 - 0
libs/core/src/ad/ad-pool.service.ts

@@ -1,4 +1,5 @@
 import { Injectable, Logger } from '@nestjs/common';
+import { randomUUID } from 'crypto';
 import { CacheKeys } from '@box/common/cache/cache-keys';
 import type { AdType } from '@box/common/ads/ad-types';
 import { AdType as PrismaAdType } from '@prisma/mongo/client';
@@ -16,6 +17,7 @@ export interface AdPayload {
   adsContent?: string | null;
   adsCoverImg?: string | null;
   adsUrl?: string | null;
+  trackingId?: string;
 }
 
 @Injectable()
@@ -27,6 +29,16 @@ export class AdPoolService {
     private readonly mongoPrisma: MongoPrismaService,
   ) {}
 
+  /** Generate a unique tracking ID for ad impression tracking. */
+  private generateTrackingId(): string {
+    try {
+      return randomUUID();
+    } catch {
+      // Fallback: generate a simple UUID-like string if crypto fails
+      return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+    }
+  }
+
   /** Rebuild all ad pools keyed by AdType. */
   async rebuildAllAdPools(): Promise<void> {
     const adTypes = Object.values(PrismaAdType) as AdType[];
@@ -79,6 +91,9 @@ export class AdPoolService {
     await this.redis.del(key);
 
     if (!payloads.length) {
+      // Ensure the key exists (as an empty SET) so checklist doesn't fail
+      // Use a placeholder that will be ignored by consumers
+      await this.redis.sadd(key, '__empty__');
       return 0;
     }
 
@@ -94,6 +109,8 @@ export class AdPoolService {
       const key = CacheKeys.appAdPoolByType(adType);
       const raw = await this.redis.srandmember(key);
       if (!raw) return null;
+      // Skip the empty placeholder marker
+      if (raw === '__empty__') return null;
       const parsed = JSON.parse(raw) as Partial<AdPayload>;
       if (!parsed || typeof parsed !== 'object' || !parsed.id) return null;
       return parsed as AdPayload;
@@ -139,6 +156,7 @@ export class AdPoolService {
         adsContent: ad.adsContent ?? null,
         adsCoverImg: ad.adsCoverImg ?? null,
         adsUrl: ad.adsUrl ?? null,
+        trackingId: this.generateTrackingId(),
       };
 
       // Fire-and-forget rebuild for freshness.

+ 4 - 0
libs/core/src/cache/cache-manager.module.ts

@@ -40,9 +40,13 @@ import { ChannelWarmupService } from './channel/channel-warmup.service';
   ],
   exports: [
     AdPoolService,
+    AdPoolBuilder,
     CategoryCacheService,
+    CategoryCacheBuilder,
     TagCacheService,
+    TagCacheBuilder,
     ChannelCacheService,
+    ChannelCacheBuilder,
   ],
 })
 export class CacheManagerModule {}

+ 5 - 0
libs/db/src/redis/redis.service.ts

@@ -60,6 +60,11 @@ export class RedisService {
     return client.srandmember(key);
   }
 
+  async scard(key: string): Promise<number> {
+    const client = this.ensureClient();
+    return client.scard(key);
+  }
+
   async expire(key: string, ttlSeconds: number): Promise<boolean> {
     const client = this.ensureClient();
     const result = await client.expire(key, ttlSeconds);