video-cache-coverage.service.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. // box-nestjs-monorepo/apps/box-mgnt-api/src/dev/services/video-cache-coverage.service.ts
  2. import { Injectable, Logger } from '@nestjs/common';
  3. import { RedisService } from '@box/db/redis/redis.service';
  4. import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
  5. import { tsCacheKeys } from '@box/common/cache/ts-cache-key.provider';
  6. /**
  7. * Response structure for video cache coverage check
  8. */
  9. export interface VideoCacheCoverageResponse {
  10. totalChannels: number;
  11. totalCategories: number;
  12. totalTags: number;
  13. missingCategoryVideoLists: string[];
  14. missingTagVideoLists: string[];
  15. missingTagMetadataLists: string[];
  16. }
  17. /**
  18. * Service for checking video cache coverage.
  19. * Dev-only: verifies that all expected cache keys exist in Redis.
  20. *
  21. * Compares database structure (channels, categories, tags) with actual Redis keys
  22. * to identify missing cache entries.
  23. *
  24. * Smart detection:
  25. * - Only reports missing category video list if videos exist and Redis key is missing
  26. * - Only reports missing tag video list if videos exist and Redis key is missing
  27. * - Tag metadata lists always checked (expected if category has tags)
  28. */
  29. @Injectable()
  30. export class VideoCacheCoverageService {
  31. private readonly logger = new Logger(VideoCacheCoverageService.name);
  32. constructor(
  33. private readonly redis: RedisService,
  34. private readonly mongoPrisma: MongoPrismaService,
  35. ) {}
  36. /**
  37. * Check video cache coverage across all channels, categories, and tags.
  38. * Identifies missing category lists, tag lists, and tag metadata lists.
  39. *
  40. * SMART MISSING-KEY DETECTION:
  41. * - Only reports category video list as missing if: videoCount > 0 AND Redis key missing
  42. * - Only reports tag video list as missing if: videoCount > 0 AND Redis key missing
  43. * - Tag metadata lists are checked regardless (always expected if tags exist)
  44. *
  45. * This avoids false positives for empty categories/tags with no videos.
  46. */
  47. async checkCoverage(): Promise<VideoCacheCoverageResponse> {
  48. this.logger.log('[Coverage] Starting video cache coverage check');
  49. const missingCategoryVideoLists: string[] = [];
  50. const missingTagVideoLists: string[] = [];
  51. const missingTagMetadataLists: string[] = [];
  52. let totalChannels = 0;
  53. let totalCategories = 0;
  54. let totalTags = 0;
  55. // Get all channels
  56. const channels = await this.mongoPrisma.channel.findMany();
  57. totalChannels = channels.length;
  58. this.logger.debug(`[Coverage] Found ${totalChannels} channels`);
  59. for (const channel of channels) {
  60. // Get all enabled categories (no longer tied to channel)
  61. const categories = await this.mongoPrisma.category.findMany({
  62. where: { status: 1 },
  63. });
  64. for (const category of categories) {
  65. totalCategories++;
  66. // Count videos in this category (using same filter as builder)
  67. const categoryVideoCount = await this.mongoPrisma.videoMedia.count({
  68. where: {
  69. categoryIds: { has: category.id },
  70. status: 'Completed',
  71. // listStatus: 1, // Only "on shelf" videos
  72. },
  73. });
  74. // Check category video list cache key ONLY if there are videos
  75. const categoryListKey = tsCacheKeys.video.categoryList(category.id);
  76. const categoryListExists = await this.redis.exists(categoryListKey);
  77. if (categoryVideoCount > 0 && !categoryListExists) {
  78. missingCategoryVideoLists.push(categoryListKey);
  79. this.logger.warn(
  80. `[Coverage] Missing category list: ${categoryListKey} (videos: ${categoryVideoCount})`,
  81. );
  82. } else if (categoryVideoCount === 0) {
  83. this.logger.debug(
  84. `[Coverage] Empty category OK (no videos, no cache required): ${categoryListKey}`,
  85. );
  86. }
  87. // Check tag metadata list cache key for this category
  88. const tagMetadataKey = tsCacheKeys.tag.metadataByCategory(category.id);
  89. const tagMetadataExists = await this.redis.exists(tagMetadataKey);
  90. if (!tagMetadataExists) {
  91. missingTagMetadataLists.push(tagMetadataKey);
  92. this.logger.warn(
  93. `[Coverage] Missing tag metadata list: ${tagMetadataKey}`,
  94. );
  95. }
  96. // Get all enabled tags for this category (no longer filtered by channel)
  97. const tags = await this.mongoPrisma.tag.findMany({
  98. where: {
  99. status: 1,
  100. categoryId: category.id,
  101. },
  102. });
  103. for (const tag of tags) {
  104. totalTags++;
  105. // Count videos with this tag in this category (same filter as builder)
  106. const tagVideoCount = await this.mongoPrisma.videoMedia.count({
  107. where: {
  108. categoryIds: { has: category.id },
  109. status: 'Completed',
  110. // listStatus: 1, // Only "on shelf" videos
  111. // tagIds: { has: tag.id }, // Has this specific tag
  112. },
  113. });
  114. // Check tag-filtered video list cache key ONLY if there are videos
  115. const tagListKey = tsCacheKeys.video.tagList(category.id, tag.id);
  116. const tagListExists = await this.redis.exists(tagListKey);
  117. if (tagVideoCount > 0 && !tagListExists) {
  118. missingTagVideoLists.push(tagListKey);
  119. this.logger.warn(
  120. `[Coverage] Missing tag video list: ${tagListKey} (videos: ${tagVideoCount})`,
  121. );
  122. } else if (tagVideoCount === 0) {
  123. this.logger.debug(
  124. `[Coverage] Empty tag OK (no videos, no cache required): ${tagListKey}`,
  125. );
  126. }
  127. }
  128. }
  129. }
  130. const totalMissing =
  131. missingCategoryVideoLists.length +
  132. missingTagVideoLists.length +
  133. missingTagMetadataLists.length;
  134. this.logger.log(
  135. `[Coverage] Complete: channels=${totalChannels} categories=${totalCategories} tags=${totalTags} missing=${totalMissing}`,
  136. );
  137. if (totalMissing > 0) {
  138. this.logger.warn(
  139. `[Coverage] Found ${totalMissing} missing cache keys (categoryLists: ${missingCategoryVideoLists.length}, tagLists: ${missingTagVideoLists.length}, tagMetadata: ${missingTagMetadataLists.length})`,
  140. );
  141. } else {
  142. this.logger.log('[Coverage] All expected cache keys exist');
  143. }
  144. return {
  145. totalChannels,
  146. totalCategories,
  147. totalTags,
  148. missingCategoryVideoLists,
  149. missingTagVideoLists,
  150. missingTagMetadataLists,
  151. };
  152. }
  153. }