|
|
@@ -6,13 +6,12 @@ import { MysqlPrismaService } from '@box/db/prisma/mysql-prisma.service';
|
|
|
import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
|
|
|
import { RedisService } from '@box/db/redis/redis.service';
|
|
|
import { CacheKeys } from '@box/common/cache/cache-keys';
|
|
|
-import { ADTYPE_POOLS } from '@box/common/ads/ad-pool-config';
|
|
|
-import type {
|
|
|
- AdType,
|
|
|
- AdPoolEntry,
|
|
|
- AdScene,
|
|
|
- AdSlot,
|
|
|
-} from '@box/common/ads/ad-types';
|
|
|
+import type { AdType } from '@box/common/ads/ad-types';
|
|
|
+import { AdType as PrismaAdType } from '@prisma/mongo/client';
|
|
|
+import { CategoryCacheBuilder } from '@box/core/cache/category/category-cache.builder';
|
|
|
+import { TagCacheBuilder } from '@box/core/cache/tag/tag-cache.builder';
|
|
|
+import { ChannelCacheBuilder } from '@box/core/cache/channel/channel-cache.builder';
|
|
|
+import { AdPoolService } from '@box/core/ad/ad-pool.service';
|
|
|
|
|
|
import {
|
|
|
CacheEntityType,
|
|
|
@@ -22,17 +21,14 @@ import {
|
|
|
} from './cache-sync.types';
|
|
|
|
|
|
// Cache TTL (seconds)
|
|
|
-const CHANNEL_CACHE_TTL = 900; // 15 min
|
|
|
-const CATEGORY_CACHE_TTL = 900; // 15 min
|
|
|
-const TAG_CACHE_TTL = 900; // 15 min
|
|
|
const AD_CACHE_TTL = 300; // 5 min (more dynamic)
|
|
|
-const AD_POOL_TTL = 300; // 5 min
|
|
|
|
|
|
/**
|
|
|
- * CacheSyncService
|
|
|
- * - Writes durable CacheSyncAction records in MySQL.
|
|
|
- * - Rebuilds Redis caches for channels/categories/ads/pools consumed by app-api.
|
|
|
+ * CacheSyncService - Refactored as orchestration wrapper
|
|
|
+ * - Thin wrapper that delegates to builder classes.
|
|
|
+ * - Writes durable CacheSyncAction records in MySQL for audit trail and retry logic.
|
|
|
* - Retries transient failures with backoff using attempts + nextAttemptAt.
|
|
|
+ * - ALL Mongo-to-Redis logic is delegated to builders.
|
|
|
*/
|
|
|
@Injectable()
|
|
|
export class CacheSyncService {
|
|
|
@@ -59,6 +55,11 @@ export class CacheSyncService {
|
|
|
private readonly mongoPrisma: MongoPrismaService,
|
|
|
// Redis: cache store consumed by box-app-api
|
|
|
private readonly redis: RedisService,
|
|
|
+ // Cache builders - delegated builders for actual Mongo-to-Redis logic
|
|
|
+ private readonly channelCacheBuilder: ChannelCacheBuilder,
|
|
|
+ private readonly categoryCacheBuilder: CategoryCacheBuilder,
|
|
|
+ private readonly tagCacheBuilder: TagCacheBuilder,
|
|
|
+ private readonly adPoolService: AdPoolService,
|
|
|
) {}
|
|
|
|
|
|
// Utility to get "now" as BigInt epoch millis
|
|
|
@@ -382,7 +383,8 @@ export class CacheSyncService {
|
|
|
const payload = action.payload as CachePayload | null;
|
|
|
const channelId = (payload as any)?.channelId as string | undefined;
|
|
|
if (channelId) {
|
|
|
- await this.rebuildChannelWithCategories(channelId);
|
|
|
+ // Delegate to builder - rebuilds all channels
|
|
|
+ await this.rebuildChannelsAll();
|
|
|
} else {
|
|
|
this.logger.warn(
|
|
|
`handleChannelAction REFRESH: missing channelId for action id=${action.id}`,
|
|
|
@@ -416,53 +418,6 @@ export class CacheSyncService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // Made public so checklist service can invoke directly when a key is missing.
|
|
|
- async rebuildChannelsAll(): Promise<void> {
|
|
|
- try {
|
|
|
- const channels = await this.mongoPrisma.channel.findMany({
|
|
|
- where: {
|
|
|
- // isDeleted: false,
|
|
|
- },
|
|
|
- orderBy: {
|
|
|
- id: 'asc',
|
|
|
- },
|
|
|
- });
|
|
|
-
|
|
|
- const sanitized = channels.map((c) => ({
|
|
|
- id: c.id,
|
|
|
- name: c.name,
|
|
|
- landingUrl: c.landingUrl,
|
|
|
- videoCdn: c.videoCdn ?? null,
|
|
|
- coverCdn: c.coverCdn ?? null,
|
|
|
- clientName: c.clientName ?? null,
|
|
|
- clientNotice: c.clientNotice ?? null,
|
|
|
- remark: c.remark ?? null,
|
|
|
- createAt:
|
|
|
- typeof c.createAt === 'bigint'
|
|
|
- ? Number(c.createAt)
|
|
|
- : (c as any).createAt,
|
|
|
- updateAt:
|
|
|
- typeof c.updateAt === 'bigint'
|
|
|
- ? Number(c.updateAt)
|
|
|
- : (c as any).updateAt,
|
|
|
- }));
|
|
|
-
|
|
|
- const start = Date.now();
|
|
|
- await this.redis.setJson(
|
|
|
- CacheKeys.appChannelAll,
|
|
|
- sanitized,
|
|
|
- CHANNEL_CACHE_TTL,
|
|
|
- );
|
|
|
-
|
|
|
- this.logger.log(
|
|
|
- `Rebuilt ${CacheKeys.appChannelAll} with ${channels.length} item(s), ${Date.now() - start}ms`,
|
|
|
- );
|
|
|
- } catch (err) {
|
|
|
- this.logger.error('Failed to rebuild channels:all cache', err);
|
|
|
- throw err; // Re-throw to trigger retry mechanism
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
// ─────────────────────────────────────────────
|
|
|
// CATEGORIES
|
|
|
// ─────────────────────────────────────────────
|
|
|
@@ -476,14 +431,8 @@ export class CacheSyncService {
|
|
|
const payload = action.payload as CachePayload | null;
|
|
|
const categoryId = (payload as any)?.categoryId as string | undefined;
|
|
|
if (categoryId) {
|
|
|
- await this.rebuildCategoryWithTags(categoryId);
|
|
|
- // Load category to get channelId for channel-with-categories rebuild
|
|
|
- const category = await this.mongoPrisma.category.findUnique({
|
|
|
- where: { id: categoryId },
|
|
|
- });
|
|
|
- if (category) {
|
|
|
- await this.rebuildChannelWithCategories(category.channelId);
|
|
|
- }
|
|
|
+ // Delegate to builder - rebuilds all categories
|
|
|
+ await this.rebuildCategoriesAll();
|
|
|
} else {
|
|
|
this.logger.warn(
|
|
|
`handleCategoryAction REFRESH: missing categoryId for action id=${action.id}`,
|
|
|
@@ -517,52 +466,6 @@ export class CacheSyncService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // Made public so checklist service can invoke directly when a key is missing.
|
|
|
- async rebuildCategoriesAll(): Promise<void> {
|
|
|
- try {
|
|
|
- const categories = await this.mongoPrisma.category.findMany({
|
|
|
- where: {
|
|
|
- // isDeleted: false,
|
|
|
- status: 1, // only active categories
|
|
|
- },
|
|
|
- orderBy: {
|
|
|
- seq: 'asc',
|
|
|
- },
|
|
|
- });
|
|
|
-
|
|
|
- const sanitized = categories.map((c) => ({
|
|
|
- id: c.id,
|
|
|
- name: c.name,
|
|
|
- subtitle: c.subtitle ?? null,
|
|
|
- channelId: c.channelId,
|
|
|
- seq: c.seq,
|
|
|
- status: c.status,
|
|
|
- createAt:
|
|
|
- typeof c.createAt === 'bigint'
|
|
|
- ? Number(c.createAt)
|
|
|
- : (c as any).createAt,
|
|
|
- updateAt:
|
|
|
- typeof c.updateAt === 'bigint'
|
|
|
- ? Number(c.updateAt)
|
|
|
- : (c as any).updateAt,
|
|
|
- }));
|
|
|
-
|
|
|
- const start = Date.now();
|
|
|
- await this.redis.setJson(
|
|
|
- CacheKeys.appCategoryAll,
|
|
|
- sanitized,
|
|
|
- CATEGORY_CACHE_TTL,
|
|
|
- );
|
|
|
-
|
|
|
- this.logger.log(
|
|
|
- `Rebuilt ${CacheKeys.appCategoryAll} with ${categories.length} item(s), ${Date.now() - start}ms`,
|
|
|
- );
|
|
|
- } catch (err) {
|
|
|
- this.logger.error('Failed to rebuild categories:all cache', err);
|
|
|
- throw err; // Re-throw to trigger retry mechanism
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
// ─────────────────────────────────────────────
|
|
|
// ADS (placeholders for now)
|
|
|
// ─────────────────────────────────────────────
|
|
|
@@ -698,24 +601,14 @@ export class CacheSyncService {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- const placements = ADTYPE_POOLS[adType] ?? [];
|
|
|
-
|
|
|
- if (!placements || placements.length === 0) {
|
|
|
- this.logger.warn(
|
|
|
- `handleAdPoolAction: no placements mapping found for adType=${adType}, action id=${action.id}`,
|
|
|
- );
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
switch (action.operation as CacheOperation) {
|
|
|
case CacheOperation.INVALIDATE: {
|
|
|
try {
|
|
|
// remove all pools for this adType
|
|
|
- // Pattern: app:adpool:*:*:<adType>
|
|
|
- const pattern = `app:adpool:*:*:${adType}`;
|
|
|
- const deleted = await this.redis.deleteByPattern(pattern);
|
|
|
+ const key = CacheKeys.appAdPoolByType(adType);
|
|
|
+ await this.redis.del(key);
|
|
|
this.logger.log(
|
|
|
- `Invalidated ${deleted} pool key(s) for adType=${adType} using pattern=${pattern}`,
|
|
|
+ `Invalidated ad pool key=${key} for adType=${adType}`,
|
|
|
);
|
|
|
} catch (err) {
|
|
|
this.logger.error(
|
|
|
@@ -729,71 +622,15 @@ export class CacheSyncService {
|
|
|
case CacheOperation.REBUILD_POOL:
|
|
|
default: {
|
|
|
this.logger.log(
|
|
|
- `handleAdPoolAction: rebuilding ${placements.length} pool(s) for adType=${adType}, action id=${action.id}`,
|
|
|
+ `handleAdPoolAction: rebuilding ad pool for adType=${adType}, action id=${action.id}`,
|
|
|
);
|
|
|
- for (const placement of placements) {
|
|
|
- await this.rebuildAdPoolForPlacement(
|
|
|
- adType,
|
|
|
- placement.scene,
|
|
|
- placement.slot,
|
|
|
- );
|
|
|
- }
|
|
|
+ // Delegate to builder
|
|
|
+ await this.rebuildAdPoolForType(adType);
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * Rebuild a single ad pool for a placement (scene + slot).
|
|
|
- * Reads active ads for the adType and atomically swaps the cache key to avoid
|
|
|
- * partially-written pools being read by app-api.
|
|
|
- */
|
|
|
- // Made public so checklist service can invoke targeted pool rebuild.
|
|
|
- async rebuildAdPoolForPlacement(
|
|
|
- adType: AdType,
|
|
|
- scene: AdScene,
|
|
|
- slot: AdSlot,
|
|
|
- ): Promise<void> {
|
|
|
- try {
|
|
|
- const now = this.nowBigInt();
|
|
|
-
|
|
|
- const ads = await this.mongoPrisma.ads.findMany({
|
|
|
- where: {
|
|
|
- status: 1,
|
|
|
- startDt: { lte: now },
|
|
|
- OR: [{ expiryDt: BigInt(0) }, { expiryDt: { gte: now } }],
|
|
|
- adsModule: {
|
|
|
- is: { adType },
|
|
|
- },
|
|
|
- },
|
|
|
- orderBy: { seq: 'asc' },
|
|
|
- });
|
|
|
-
|
|
|
- const poolEntries: AdPoolEntry[] = ads.map((ad) => ({
|
|
|
- id: ad.id,
|
|
|
- weight: 1,
|
|
|
- }));
|
|
|
-
|
|
|
- const key = CacheKeys.appAdPoolByType(adType);
|
|
|
-
|
|
|
- // Atomic swap to avoid partial-read windows
|
|
|
- const start = Date.now();
|
|
|
- await this.redis.atomicSwapJson([
|
|
|
- { key, value: poolEntries, ttlSeconds: AD_POOL_TTL },
|
|
|
- ]);
|
|
|
-
|
|
|
- this.logger.log(
|
|
|
- `Rebuilt ad pool ${key} with ${poolEntries.length} ad(s) for adType=${adType}, scene=${scene}, slot=${slot}, ${Date.now() - start}ms`,
|
|
|
- );
|
|
|
- } catch (err) {
|
|
|
- this.logger.error(
|
|
|
- `Failed to rebuild ad pool for adType=${adType}, scene=${scene}, slot=${slot}`,
|
|
|
- err,
|
|
|
- );
|
|
|
- throw err;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
// ─────────────────────────────────────────────
|
|
|
// VIDEO LISTS (placeholder)
|
|
|
// ─────────────────────────────────────────────
|
|
|
@@ -819,7 +656,8 @@ export class CacheSyncService {
|
|
|
const payload = action.payload as CachePayload | null;
|
|
|
const categoryId = action.entityId || (payload as any)?.categoryId;
|
|
|
if (categoryId && categoryId !== 'null') {
|
|
|
- await this.rebuildCategoryWithTags(categoryId);
|
|
|
+ // Delegate to builder - rebuilds all tags
|
|
|
+ await this.rebuildTagAll();
|
|
|
} else {
|
|
|
this.logger.warn(
|
|
|
`handleTagAction REFRESH: missing categoryId for action id=${action.id}`,
|
|
|
@@ -857,240 +695,88 @@ export class CacheSyncService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // ─────────────────────────────────────────────
|
|
|
+ // Cache warming
|
|
|
+ // ─────────────────────────────────────────────
|
|
|
+
|
|
|
/**
|
|
|
- * Rebuild channel with its categories tree for a specific channel.
|
|
|
+ * Pre-warm critical cache keys on startup or on-demand.
|
|
|
+ * Call this from app startup or via admin endpoint.
|
|
|
+ * Delegates to all cache builders and ad pool service.
|
|
|
*/
|
|
|
- async rebuildChannelWithCategories(channelId: string): Promise<void> {
|
|
|
- const cacheKey = CacheKeys.appChannelWithCategories(channelId);
|
|
|
+ async warmCache(): Promise<void> {
|
|
|
+ this.logger.log('Cache warming started');
|
|
|
+ const start = Date.now();
|
|
|
|
|
|
try {
|
|
|
- const channel = await this.mongoPrisma.channel.findUnique({
|
|
|
- where: { id: channelId },
|
|
|
- });
|
|
|
+ // Rebuild channels
|
|
|
+ await this.rebuildChannelsAll();
|
|
|
|
|
|
- if (!channel) {
|
|
|
- try {
|
|
|
- await this.redis.del(cacheKey);
|
|
|
- this.logger.warn(
|
|
|
- `rebuildChannelWithCategories: channel not found, removed cache key=${cacheKey}`,
|
|
|
- );
|
|
|
- } catch (redisErr) {
|
|
|
- this.logger.error(`Failed to delete Redis key ${cacheKey}`, redisErr);
|
|
|
- }
|
|
|
- return;
|
|
|
- }
|
|
|
+ // Rebuild categories
|
|
|
+ await this.rebuildCategoriesAll();
|
|
|
|
|
|
- const categories = await this.mongoPrisma.category.findMany({
|
|
|
- where: { channelId: channel.id, status: 1 },
|
|
|
- orderBy: [{ seq: 'asc' }, { name: 'asc' }],
|
|
|
- });
|
|
|
+ // Rebuild tags
|
|
|
+ await this.rebuildTagAll();
|
|
|
|
|
|
- const channelLite = {
|
|
|
- id: channel.id,
|
|
|
- name: channel.name,
|
|
|
- landingUrl: channel.landingUrl,
|
|
|
- videoCdn: channel.videoCdn ?? null,
|
|
|
- coverCdn: channel.coverCdn ?? null,
|
|
|
- clientName: channel.clientName ?? null,
|
|
|
- clientNotice: channel.clientNotice ?? null,
|
|
|
- createAt:
|
|
|
- typeof channel.createAt === 'bigint'
|
|
|
- ? Number(channel.createAt)
|
|
|
- : (channel as any).createAt,
|
|
|
- updateAt:
|
|
|
- typeof channel.updateAt === 'bigint'
|
|
|
- ? Number(channel.updateAt)
|
|
|
- : (channel as any).updateAt,
|
|
|
- };
|
|
|
-
|
|
|
- const categoryLites = categories.map((c) => ({
|
|
|
- id: c.id,
|
|
|
- name: c.name,
|
|
|
- subtitle: c.subtitle ?? null,
|
|
|
- channelId: c.channelId,
|
|
|
- seq: c.seq,
|
|
|
- status: c.status,
|
|
|
- createAt:
|
|
|
- typeof c.createAt === 'bigint'
|
|
|
- ? Number(c.createAt)
|
|
|
- : (c as any).createAt,
|
|
|
- updateAt:
|
|
|
- typeof c.updateAt === 'bigint'
|
|
|
- ? Number(c.updateAt)
|
|
|
- : (c as any).updateAt,
|
|
|
- }));
|
|
|
-
|
|
|
- const payload = {
|
|
|
- channel: channelLite,
|
|
|
- categories: categoryLites,
|
|
|
- schemaVersion: 1,
|
|
|
- updatedAt: Date.now(),
|
|
|
- };
|
|
|
-
|
|
|
- const start = Date.now();
|
|
|
- await this.redis.setJson(cacheKey, payload, CATEGORY_CACHE_TTL);
|
|
|
+ // Rebuild all ad pools
|
|
|
+ await this.rebuildAllAdPools();
|
|
|
|
|
|
- this.logger.log(
|
|
|
- `Rebuilt ${cacheKey} with ${categories.length} category(ies), ${Date.now() - start}ms`,
|
|
|
- );
|
|
|
+ this.logger.log(`Cache warming complete in ${Date.now() - start}ms`);
|
|
|
} catch (err) {
|
|
|
this.logger.error(
|
|
|
- `Failed to rebuild channel with categories for channelId=${channelId}`,
|
|
|
- err,
|
|
|
+ `Cache warming failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
|
);
|
|
|
- throw err; // Re-throw to trigger retry mechanism
|
|
|
+ throw err;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Rebuild category with its tags tree for a specific category.
|
|
|
+ * Public method to rebuild channels cache.
|
|
|
+ * Delegates to ChannelCacheBuilder.buildAll().
|
|
|
*/
|
|
|
- async rebuildCategoryWithTags(categoryId: string): Promise<void> {
|
|
|
- // Validate categoryId to prevent 'null' string or invalid ObjectID
|
|
|
- if (!categoryId || categoryId === 'null' || categoryId === 'undefined') {
|
|
|
- this.logger.warn(
|
|
|
- `rebuildCategoryWithTags: invalid categoryId="${categoryId}"`,
|
|
|
- );
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const cacheKey = CacheKeys.appCategoryWithTags(categoryId);
|
|
|
-
|
|
|
- try {
|
|
|
- const category = await this.mongoPrisma.category.findUnique({
|
|
|
- where: { id: categoryId },
|
|
|
- });
|
|
|
-
|
|
|
- if (!category) {
|
|
|
- try {
|
|
|
- await this.redis.del(cacheKey);
|
|
|
- this.logger.warn(
|
|
|
- `rebuildCategoryWithTags: category not found, removed cache key=${cacheKey}`,
|
|
|
- );
|
|
|
- } catch (redisErr) {
|
|
|
- this.logger.error(`Failed to delete Redis key ${cacheKey}`, redisErr);
|
|
|
- }
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const tags = await this.mongoPrisma.tag.findMany({
|
|
|
- where: { categoryId, status: 1 },
|
|
|
- orderBy: [{ seq: 'asc' }, { name: 'asc' }],
|
|
|
- });
|
|
|
-
|
|
|
- const payload = {
|
|
|
- category: {
|
|
|
- id: category.id,
|
|
|
- name: category.name,
|
|
|
- subtitle: category.subtitle ?? null,
|
|
|
- channelId: category.channelId,
|
|
|
- seq: category.seq,
|
|
|
- status: category.status,
|
|
|
- createAt:
|
|
|
- typeof category.createAt === 'bigint'
|
|
|
- ? Number(category.createAt)
|
|
|
- : (category as any).createAt,
|
|
|
- updateAt:
|
|
|
- typeof category.updateAt === 'bigint'
|
|
|
- ? Number(category.updateAt)
|
|
|
- : (category as any).updateAt,
|
|
|
- },
|
|
|
- tags: tags.map((t) => ({
|
|
|
- id: t.id,
|
|
|
- name: t.name,
|
|
|
- channelId: t.channelId,
|
|
|
- categoryId: t.categoryId,
|
|
|
- seq: t.seq,
|
|
|
- status: t.status,
|
|
|
- createAt:
|
|
|
- typeof t.createAt === 'bigint'
|
|
|
- ? Number(t.createAt)
|
|
|
- : (t as any).createAt,
|
|
|
- updateAt:
|
|
|
- typeof t.updateAt === 'bigint'
|
|
|
- ? Number(t.updateAt)
|
|
|
- : (t as any).updateAt,
|
|
|
- })),
|
|
|
- schemaVersion: 1,
|
|
|
- updatedAt: Date.now(),
|
|
|
- };
|
|
|
-
|
|
|
- const start = Date.now();
|
|
|
- await this.redis.setJson(cacheKey, payload, TAG_CACHE_TTL);
|
|
|
+ async rebuildChannelsAll(): Promise<void> {
|
|
|
+ await this.channelCacheBuilder.buildAll();
|
|
|
+ }
|
|
|
|
|
|
- this.logger.log(
|
|
|
- `Rebuilt ${cacheKey} with category and ${tags.length} tag(s), ${Date.now() - start}ms`,
|
|
|
- );
|
|
|
- } catch (err) {
|
|
|
- this.logger.error(
|
|
|
- `Failed to rebuild category with tags for categoryId=${categoryId}`,
|
|
|
- err,
|
|
|
- );
|
|
|
- throw err; // Re-throw to trigger retry mechanism
|
|
|
- }
|
|
|
+ /**
|
|
|
+ * Public method to rebuild categories cache.
|
|
|
+ * Delegates to CategoryCacheBuilder.buildAll().
|
|
|
+ */
|
|
|
+ async rebuildCategoriesAll(): Promise<void> {
|
|
|
+ await this.categoryCacheBuilder.buildAll();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Rebuild global tag:all suggestion pool.
|
|
|
+ * Public method to rebuild tags cache.
|
|
|
+ * Delegates to TagCacheBuilder.buildAll().
|
|
|
*/
|
|
|
async rebuildTagAll(): Promise<void> {
|
|
|
- try {
|
|
|
- const tags = await this.mongoPrisma.tag.findMany({
|
|
|
- where: { status: 1 },
|
|
|
- orderBy: [{ name: 'asc' }],
|
|
|
- });
|
|
|
-
|
|
|
- const payload = {
|
|
|
- tags: tags.map((t) => ({
|
|
|
- id: t.id,
|
|
|
- name: t.name,
|
|
|
- channelId: t.channelId,
|
|
|
- categoryId: t.categoryId,
|
|
|
- })),
|
|
|
- schemaVersion: 1,
|
|
|
- updatedAt: Date.now(),
|
|
|
- };
|
|
|
-
|
|
|
- const start = Date.now();
|
|
|
- await this.redis.setJson(CacheKeys.appTagAll, payload, TAG_CACHE_TTL);
|
|
|
-
|
|
|
- this.logger.log(
|
|
|
- `Rebuilt ${CacheKeys.appTagAll} with ${tags.length} tag(s), ${Date.now() - start}ms`,
|
|
|
- );
|
|
|
- } catch (err) {
|
|
|
- this.logger.error('Failed to rebuild tag:all cache', err);
|
|
|
- throw err; // Re-throw to trigger retry mechanism
|
|
|
- }
|
|
|
+ await this.tagCacheBuilder.buildAll();
|
|
|
}
|
|
|
|
|
|
- // ─────────────────────────────────────────────
|
|
|
- // Cache warming
|
|
|
- // ─────────────────────────────────────────────
|
|
|
-
|
|
|
/**
|
|
|
- * Pre-warm critical cache keys on startup or on-demand.
|
|
|
- * Call this from app startup or via admin endpoint.
|
|
|
+ * Public method to rebuild a single ad pool for an AdType.
|
|
|
+ * Delegates to AdPoolService.rebuildPoolForType().
|
|
|
*/
|
|
|
- async warmCache(): Promise<void> {
|
|
|
- this.logger.log('Cache warming started');
|
|
|
- const start = Date.now();
|
|
|
-
|
|
|
- await Promise.all([
|
|
|
- this.rebuildChannelsAll(),
|
|
|
- this.rebuildCategoriesAll(),
|
|
|
- this.rebuildTagAll(),
|
|
|
- this.warmAdPools(),
|
|
|
- ]);
|
|
|
-
|
|
|
- this.logger.log(`Cache warming complete in ${Date.now() - start}ms`);
|
|
|
+ async rebuildAdPoolForType(adType: AdType): Promise<void> {
|
|
|
+ await this.adPoolService.rebuildPoolForType(adType);
|
|
|
}
|
|
|
|
|
|
- private async warmAdPools(): Promise<void> {
|
|
|
- const allAdTypes = Object.keys(ADTYPE_POOLS) as AdType[];
|
|
|
+ /**
|
|
|
+ * Private helper to rebuild all ad pools.
|
|
|
+ * Iterates through all AdTypes and delegates to AdPoolService.
|
|
|
+ */
|
|
|
+ private async rebuildAllAdPools(): Promise<void> {
|
|
|
+ const allAdTypes = Object.values(PrismaAdType) as AdType[];
|
|
|
for (const adType of allAdTypes) {
|
|
|
- const placements = ADTYPE_POOLS[adType];
|
|
|
- for (const { scene, slot } of placements) {
|
|
|
- await this.rebuildAdPoolForPlacement(adType, scene, slot);
|
|
|
+ try {
|
|
|
+ await this.rebuildAdPoolForType(adType);
|
|
|
+ } catch (err) {
|
|
|
+ this.logger.error(
|
|
|
+ `Failed to rebuild ad pool for adType=${adType}`,
|
|
|
+ err instanceof Error ? err.stack : String(err),
|
|
|
+ );
|
|
|
+ // Continue with other ad types even if one fails
|
|
|
}
|
|
|
}
|
|
|
}
|