|
|
@@ -1,5 +1,7 @@
|
|
|
// 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 { CacheKeys } from '@box/common/cache/cache-keys';
|
|
|
@@ -58,12 +60,20 @@ 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,
|
|
|
+ ) {
|
|
|
+ // 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:
|
|
|
@@ -364,9 +374,9 @@ export class AdService {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Get an ad by ID and validate it's enabled and within date range.
|
|
|
- * Returns the ad with its relationships (channel, adsModule) loaded.
|
|
|
- * Returns null if ad is not found, disabled, or outside date range.
|
|
|
+ * Get an ad by ID from Redis cache with MongoDB fallback.
|
|
|
+ * 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;
|
|
|
@@ -377,9 +387,31 @@ export class AdService {
|
|
|
advertiser: string;
|
|
|
title: string;
|
|
|
} | null> {
|
|
|
- const now = BigInt(Date.now());
|
|
|
+ const adKey = CacheKeys.appAdById(adsId);
|
|
|
|
|
|
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,
|
|
|
+ channelId: cachedAd.channelId ?? '',
|
|
|
+ adsModuleId: cachedAd.adsModuleId ?? '',
|
|
|
+ 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 = BigInt(Date.now());
|
|
|
const ad = await this.mongoPrisma.ads.findUnique({
|
|
|
where: { id: adsId },
|
|
|
include: {
|
|
|
@@ -389,7 +421,7 @@ export class AdService {
|
|
|
});
|
|
|
|
|
|
if (!ad) {
|
|
|
- this.logger.debug(`Ad not found: adsId=${adsId}`);
|
|
|
+ this.logger.debug(`Ad not found in MongoDB: adsId=${adsId}`);
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
@@ -411,18 +443,48 @@ export class AdService {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
+ // Cache the ad for future requests (fire-and-forget)
|
|
|
+ const cacheData: CachedAd = {
|
|
|
+ id: ad.id,
|
|
|
+ channelId: ad.channelId,
|
|
|
+ adsModuleId: ad.adsModuleId,
|
|
|
+ advertiser: ad.advertiser,
|
|
|
+ title: ad.title,
|
|
|
+ adsContent: ad.adsContent,
|
|
|
+ adsCoverImg: ad.adsCoverImg,
|
|
|
+ adsUrl: ad.adsUrl,
|
|
|
+ adType: ad.adsModule.adType,
|
|
|
+ };
|
|
|
+
|
|
|
+ // 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.adsModule.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,
|
|
|
channelId: ad.channelId,
|
|
|
adsModuleId: ad.adsModuleId,
|
|
|
adType: ad.adsModule.adType,
|
|
|
adsUrl: ad.adsUrl,
|
|
|
- advertiser: ad.advertiser,
|
|
|
- title: ad.title,
|
|
|
+ advertiser: ad.advertiser ?? '',
|
|
|
+ title: ad.title ?? '',
|
|
|
};
|
|
|
} catch (err) {
|
|
|
this.logger.error(
|
|
|
- `Error fetching ad by ID: adsId=${adsId}`,
|
|
|
+ `Error fetching ad: adsId=${adsId}, key=${adKey}`,
|
|
|
err instanceof Error ? err.stack : String(err),
|
|
|
);
|
|
|
return null;
|
|
|
@@ -581,4 +643,43 @@ 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}`,
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|