ad-cache-warmup.service.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
  2. import { CacheKeys } from '@box/common/cache/cache-keys';
  3. import { RedisService } from '@box/db/redis/redis.service';
  4. import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
  5. import type { AdType } from '@box/common/ads/ad-types';
  6. interface CachedAd {
  7. id: string;
  8. channelId: string;
  9. adsModuleId: string;
  10. advertiser: string;
  11. title: string;
  12. adsContent: string | null;
  13. adsCoverImg: string | null;
  14. adsUrl: string | null;
  15. adType: string;
  16. }
  17. /**
  18. * AdCacheWarmupService
  19. *
  20. * Responsible for warming up individual ad caches (app:ad:by-id:${adId})
  21. * on application startup or on-demand.
  22. *
  23. * This complements AdPoolWarmupService which handles ad pools.
  24. */
  25. @Injectable()
  26. export class AdCacheWarmupService implements OnModuleInit {
  27. private readonly logger = new Logger(AdCacheWarmupService.name);
  28. private readonly AD_CACHE_TTL = 300; // 5 minutes
  29. constructor(
  30. private readonly redis: RedisService,
  31. private readonly mongoPrisma: MongoPrismaService,
  32. ) {}
  33. async onModuleInit(): Promise<void> {
  34. try {
  35. this.logger.log('Individual ad cache warmup starting...');
  36. await this.warmupAllAdCaches();
  37. } catch (err) {
  38. this.logger.error(
  39. 'Individual ad cache warmup encountered an error but will not block startup',
  40. err instanceof Error ? err.stack : String(err),
  41. );
  42. }
  43. }
  44. /**
  45. * Warm up all active ads' individual caches.
  46. * Only caches ads that are enabled and within their date range.
  47. */
  48. async warmupAllAdCaches(): Promise<void> {
  49. const startTime = Date.now();
  50. const now = BigInt(Date.now());
  51. try {
  52. // Fetch all active ads
  53. const ads = await this.mongoPrisma.ads.findMany({
  54. where: {
  55. status: 1, // enabled
  56. startDt: { lte: now },
  57. OR: [{ expiryDt: BigInt(0) }, { expiryDt: { gte: now } }],
  58. },
  59. include: {
  60. adsModule: { select: { adType: true } },
  61. },
  62. });
  63. this.logger.log(`Found ${ads.length} active ads to cache`);
  64. let successCount = 0;
  65. let errorCount = 0;
  66. // Cache each ad individually
  67. for (const ad of ads) {
  68. try {
  69. await this.cacheAd(ad.id, {
  70. id: ad.id,
  71. channelId: ad.channelId,
  72. adsModuleId: ad.adsModuleId,
  73. advertiser: ad.advertiser,
  74. title: ad.title,
  75. adsContent: ad.adsContent ?? null,
  76. adsCoverImg: ad.adsCoverImg ?? null,
  77. adsUrl: ad.adsUrl ?? null,
  78. adType: ad.adsModule.adType,
  79. });
  80. successCount++;
  81. } catch (err) {
  82. errorCount++;
  83. this.logger.warn(
  84. `Failed to cache ad ${ad.id}: ${err instanceof Error ? err.message : String(err)}`,
  85. );
  86. }
  87. }
  88. const duration = Date.now() - startTime;
  89. this.logger.log(
  90. `Ad cache warmup completed: ${successCount} cached, ${errorCount} errors, ${duration}ms`,
  91. );
  92. } catch (err) {
  93. this.logger.error(
  94. 'Ad cache warmup failed',
  95. err instanceof Error ? err.stack : String(err),
  96. );
  97. throw err;
  98. }
  99. }
  100. /**
  101. * Cache a single ad by ID
  102. */
  103. private async cacheAd(adId: string, cachedAd: CachedAd): Promise<void> {
  104. const key = CacheKeys.appAdById(adId);
  105. await this.redis.setJson(key, cachedAd, this.AD_CACHE_TTL);
  106. }
  107. /**
  108. * Warm up a single ad cache (used for on-demand refresh)
  109. */
  110. async warmupSingleAd(adId: string): Promise<void> {
  111. const now = BigInt(Date.now());
  112. const ad = await this.mongoPrisma.ads.findUnique({
  113. where: { id: adId },
  114. include: {
  115. adsModule: { select: { adType: true } },
  116. },
  117. });
  118. if (!ad) {
  119. // Ad doesn't exist - remove from cache
  120. const key = CacheKeys.appAdById(adId);
  121. await this.redis.del(key);
  122. this.logger.debug(`Ad ${adId} not found, removed from cache`);
  123. return;
  124. }
  125. // Check if ad is active
  126. const isActive =
  127. ad.status === 1 &&
  128. ad.startDt <= now &&
  129. (ad.expiryDt === BigInt(0) || ad.expiryDt >= now);
  130. if (!isActive) {
  131. // Ad is not active - remove from cache
  132. const key = CacheKeys.appAdById(adId);
  133. await this.redis.del(key);
  134. this.logger.debug(`Ad ${adId} is not active, removed from cache`);
  135. return;
  136. }
  137. // Cache the ad
  138. await this.cacheAd(adId, {
  139. id: ad.id,
  140. channelId: ad.channelId,
  141. adsModuleId: ad.adsModuleId,
  142. advertiser: ad.advertiser,
  143. title: ad.title,
  144. adsContent: ad.adsContent ?? null,
  145. adsCoverImg: ad.adsCoverImg ?? null,
  146. adsUrl: ad.adsUrl ?? null,
  147. adType: ad.adsModule.adType,
  148. });
  149. this.logger.debug(`Cached ad ${adId}`);
  150. }
  151. }