Kaynağa Gözat

feat(redis-inspector): add rebuild by key and support endpoints with DTOs

Dave 1 ay önce
ebeveyn
işleme
db19298592

+ 13 - 13
apps/box-mgnt-api/src/mgnt-backend/feature/provider-video-sync/provider-video-sync.service.ts

@@ -634,19 +634,19 @@ export class ProviderVideoSyncService {
       }
     }
 
-    const hasSecondTags = normalized.some(
-      (v) =>
-        Array.isArray(v.sanitizedSecondTags) &&
-        v.sanitizedSecondTags.length > 0,
-    );
-
-    if (hasSecondTags) {
-      await this.upsertSecondTagsFromVideos_NoUniqueName(
-        normalized.map((v) => ({
-          secondTags: v.sanitizedSecondTags ?? [],
-        })),
-      );
-    }
+    // const hasSecondTags = normalized.some(
+    //   (v) =>
+    //     Array.isArray(v.sanitizedSecondTags) &&
+    //     v.sanitizedSecondTags.length > 0,
+    // );
+
+    // if (hasSecondTags) {
+    //   await this.upsertSecondTagsFromVideos_NoUniqueName(
+    //     normalized.map((v) => ({
+    //       secondTags: v.sanitizedSecondTags ?? [],
+    //     })),
+    //   );
+    // }
 
     let maxUpdatedAtSeen = currentMaxUpdatedAt;
     for (const n of normalized) {

+ 12 - 0
apps/box-mgnt-api/src/mgnt-backend/feature/redis-inspector/dto/rebuild-by-key.dto.ts

@@ -0,0 +1,12 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsNotEmpty, IsString } from 'class-validator';
+
+export class RebuildCacheByKeyDto {
+  @ApiProperty({
+    description: 'Redis key to map to a logical cache code',
+    example: 'box:app:video:latest',
+  })
+  @IsString()
+  @IsNotEmpty()
+  key: string;
+}

+ 12 - 0
apps/box-mgnt-api/src/mgnt-backend/feature/redis-inspector/dto/rebuild-support.dto.ts

@@ -0,0 +1,12 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsNotEmpty, IsString } from 'class-validator';
+
+export class RebuildSupportDto {
+  @ApiProperty({
+    description: 'Redis key to check for rebuild support',
+    example: 'box:app:video:latest',
+  })
+  @IsString()
+  @IsNotEmpty()
+  key: string;
+}

+ 102 - 1
apps/box-mgnt-api/src/mgnt-backend/feature/redis-inspector/redis-cache-registry.ts

@@ -1,7 +1,19 @@
 import { LatestVideosCacheBuilder } from '@box/core/cache/video/latest/latest-videos-cache.builder';
 import { RecommendedVideosCacheBuilder } from '@box/core/cache/video/recommended/recommended-videos-cache.builder';
+import { TagCacheBuilder } from '@box/core/cache/tag/tag-cache.builder';
+import { CategoryCacheBuilder } from '@box/core/cache/category/category-cache.builder';
+import { ChannelCacheBuilder } from '@box/core/cache/channel/channel-cache.builder';
+import { AdPoolBuilder } from '@box/core/ad/ad-pool.builder';
 
-export type RedisCacheCode = 'VIDEO_LATEST' | 'VIDEO_RECOMMENDED';
+export type RedisCacheCode =
+  | 'VIDEO_LATEST'
+  | 'VIDEO_RECOMMENDED'
+  | 'TAG_ALL'
+  | 'CATEGORY_ALL'
+  | 'CATEGORY_BY_ID'
+  | 'CHANNEL_ALL'
+  | 'CHANNEL_BY_ID'
+  | 'ADPOOL';
 
 export interface RedisCacheRebuildHandler {
   cacheCode: RedisCacheCode;
@@ -13,6 +25,10 @@ export interface RedisCacheRebuildHandler {
 interface RedisCacheRegistryDeps {
   latestVideosCacheBuilder: LatestVideosCacheBuilder;
   recommendedVideosCacheBuilder: RecommendedVideosCacheBuilder;
+  tagCacheBuilder: TagCacheBuilder;
+  categoryCacheBuilder: CategoryCacheBuilder;
+  channelCacheBuilder: ChannelCacheBuilder;
+  adPoolBuilder: AdPoolBuilder;
 }
 
 export function getRedisCacheRegistry(
@@ -43,5 +59,90 @@ export function getRedisCacheRegistry(
         };
       },
     },
+    {
+      cacheCode: 'TAG_ALL',
+      label: 'Tag Cache',
+      description: 'Rebuilds the global tag metadata cache.',
+      rebuild: async () => {
+        await deps.tagCacheBuilder.buildAll();
+        return { ok: true, message: 'Tag cache rebuilt' };
+      },
+    },
+    {
+      cacheCode: 'CATEGORY_ALL',
+      label: 'Category Cache (all)',
+      description: 'Rebuilds the full category listing cache.',
+      rebuild: async () => {
+        await deps.categoryCacheBuilder.buildAll();
+        return { ok: true, message: 'Category cache rebuilt' };
+      },
+    },
+    {
+      cacheCode: 'CHANNEL_ALL',
+      label: 'Channel Cache (all)',
+      description: 'Rebuilds the complete channel cache.',
+      rebuild: async () => {
+        await deps.channelCacheBuilder.buildAll();
+        return { ok: true, message: 'Channel cache rebuilt' };
+      },
+    },
+    {
+      cacheCode: 'ADPOOL',
+      label: 'Ad Pools',
+      description: 'Rebuilds all ad pools.',
+      rebuild: async () => {
+        await deps.adPoolBuilder.buildAll();
+        return { ok: true, message: 'Ad pools rebuilt' };
+      },
+    },
   ];
 }
+
+export interface RedisCacheKeyAllowListEntry {
+  cacheCode: RedisCacheCode;
+  matcher: (key: string) => boolean;
+  description: string;
+}
+
+export const RedisCacheKeyAllowList: RedisCacheKeyAllowListEntry[] = [
+  {
+    cacheCode: 'VIDEO_LATEST',
+    matcher: (key) => key === 'box:app:video:latest',
+    description: 'Latest videos key',
+  },
+  {
+    cacheCode: 'VIDEO_RECOMMENDED',
+    matcher: (key) => key === 'box:app:video:recommended',
+    description: 'Recommended videos key',
+  },
+  {
+    cacheCode: 'TAG_ALL',
+    matcher: (key) => key === 'box:app:tag:all',
+    description: 'Global tag metadata key',
+  },
+  {
+    cacheCode: 'CATEGORY_ALL',
+    matcher: (key) => key === 'box:app:category:all',
+    description: 'Category all key',
+  },
+  {
+    cacheCode: 'CATEGORY_BY_ID',
+    matcher: (key) => key.startsWith('box:app:category:by-id:'),
+    description: 'Category detail key',
+  },
+  {
+    cacheCode: 'CHANNEL_ALL',
+    matcher: (key) => key === 'box:app:channel:all',
+    description: 'Channel all key',
+  },
+  {
+    cacheCode: 'CHANNEL_BY_ID',
+    matcher: (key) => key.startsWith('box:app:channel:by-id:'),
+    description: 'Channel detail key',
+  },
+  {
+    cacheCode: 'ADPOOL',
+    matcher: (key) => key.startsWith('box:app:adpool:'),
+    description: 'Ad pool key',
+  },
+];

+ 51 - 0
apps/box-mgnt-api/src/mgnt-backend/feature/redis-inspector/redis-inspector.controller.ts

@@ -2,6 +2,8 @@ import { Body, Controller, Get, Post, Query } from '@nestjs/common';
 import { ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
 import { InspectRedisKeyDto } from './dto/inspect-redis-key.dto';
 import { RebuildCacheDto } from './dto/rebuild-cache.dto';
+import { RebuildCacheByKeyDto } from './dto/rebuild-by-key.dto';
+import { RebuildSupportDto } from './dto/rebuild-support.dto';
 import {
   ScanRedisKeysDto,
   RedisInspectorGroupCode,
@@ -10,6 +12,8 @@ import { RedisInspectorService } from './redis-inspector.service';
 import {
   RedisInspectorRecordDetail,
   RedisInspectorRebuildResult,
+  RedisInspectorKeyRebuildResult,
+  RedisInspectorRebuildSupportResult,
   RedisInspectorScanResult,
 } from './types/redis-inspector.types';
 
@@ -139,4 +143,51 @@ export class RedisInspectorController {
   rebuild(@Body() dto: RebuildCacheDto): Promise<RedisInspectorRebuildResult> {
     return this.service.rebuild(dto);
   }
+
+  @Post('rebuild-by-key')
+  @ApiOperation({ summary: 'Rebuild a cache by redis key (allow list)' })
+  @ApiResponse({
+    schema: {
+      type: 'object',
+      properties: {
+        key: { type: 'string', example: 'box:app:video:latest' },
+        cacheCode: { type: 'string', example: 'VIDEO_LATEST' },
+        status: { type: 'string', example: 'OK' },
+        rebuiltAtSec: { type: 'integer', example: 1730000000 },
+        message: { type: 'string', nullable: true },
+        affected: { type: 'integer', nullable: true },
+      },
+    },
+  })
+  rebuildByKey(
+    @Body() dto: RebuildCacheByKeyDto,
+  ): Promise<RedisInspectorKeyRebuildResult> {
+    return this.service.rebuildByKey(dto);
+  }
+
+  @Get('rebuild-support')
+  @ApiOperation({ summary: 'Check if a Redis key can be rebuilt' })
+  @ApiQuery({
+    name: 'key',
+    required: true,
+    description: 'Redis key to validate for rebuild support',
+  })
+  @ApiResponse({
+    status: 200,
+    schema: {
+      type: 'object',
+      properties: {
+        key: { type: 'string' },
+        cacheCode: { type: 'string', nullable: true },
+        supported: { type: 'boolean' },
+        reason: { type: 'string', nullable: true },
+        description: { type: 'string', nullable: true },
+      },
+    },
+  })
+  rebuildSupport(
+    @Query() dto: RebuildSupportDto,
+  ): Promise<RedisInspectorRebuildSupportResult> {
+    return this.service.rebuildSupport(dto);
+  }
 }

+ 108 - 0
apps/box-mgnt-api/src/mgnt-backend/feature/redis-inspector/redis-inspector.service.ts

@@ -10,14 +10,24 @@ import {
   RedisInspectorKeySummary,
   RedisInspectorRecordDetail,
   RedisInspectorRebuildResult,
+  RedisInspectorRebuildSupportResult,
   RedisInspectorScanResult,
+  RedisInspectorKeyRebuildResult,
 } from './types/redis-inspector.types';
 import {
   getRedisCacheRegistry,
+  RedisCacheCode,
+  RedisCacheKeyAllowList,
   RedisCacheRebuildHandler,
 } from './redis-cache-registry';
 import { LatestVideosCacheBuilder } from '@box/core/cache/video/latest/latest-videos-cache.builder';
 import { RecommendedVideosCacheBuilder } from '@box/core/cache/video/recommended/recommended-videos-cache.builder';
+import { TagCacheBuilder } from '@box/core/cache/tag/tag-cache.builder';
+import { CategoryCacheBuilder } from '@box/core/cache/category/category-cache.builder';
+import { ChannelCacheBuilder } from '@box/core/cache/channel/channel-cache.builder';
+import { AdPoolBuilder } from '@box/core/ad/ad-pool.builder';
+import { RebuildCacheByKeyDto } from './dto/rebuild-by-key.dto';
+import { RebuildSupportDto } from './dto/rebuild-support.dto';
 
 const GROUP_PATTERNS: Record<RedisInspectorGroupCode, string> = {
   [RedisInspectorGroupCode.CHANNEL]: 'box:app:channel*',
@@ -37,10 +47,18 @@ export class RedisInspectorService {
     private readonly redisService: RedisService,
     latestVideosCacheBuilder: LatestVideosCacheBuilder,
     recommendedVideosCacheBuilder: RecommendedVideosCacheBuilder,
+    tagCacheBuilder: TagCacheBuilder,
+    categoryCacheBuilder: CategoryCacheBuilder,
+    channelCacheBuilder: ChannelCacheBuilder,
+    adPoolBuilder: AdPoolBuilder,
   ) {
     this.cacheRegistry = getRedisCacheRegistry({
       latestVideosCacheBuilder,
       recommendedVideosCacheBuilder,
+      tagCacheBuilder,
+      categoryCacheBuilder,
+      channelCacheBuilder,
+      adPoolBuilder,
     });
   }
 
@@ -137,6 +155,96 @@ export class RedisInspectorService {
     };
   }
 
+  async rebuildByKey(
+    dto: RebuildCacheByKeyDto,
+  ): Promise<RedisInspectorKeyRebuildResult> {
+    const key = dto.key?.trim() ?? '';
+    const rebuiltAtSec = Math.floor(Date.now() / 1000);
+    if (!key || !key.startsWith('box:app:')) {
+      return {
+        key,
+        status: 'NOT_SUPPORTED',
+        rebuiltAtSec,
+        message: 'Invalid key format',
+      };
+    }
+
+    const entry = RedisCacheKeyAllowList.find(({ matcher }) => matcher(key));
+    if (!entry) {
+      return {
+        key,
+        status: 'NOT_SUPPORTED',
+        rebuiltAtSec,
+        message: 'Key is not mapped to a rebuildable cache',
+      };
+    }
+
+    const handler = this.findHandler(entry.cacheCode);
+    if (!handler) {
+      return {
+        key,
+        cacheCode: entry.cacheCode,
+        status: 'NOT_SUPPORTED',
+        rebuiltAtSec,
+        message: `No rebuild handler registered for ${entry.description.toLowerCase()}`,
+      };
+    }
+
+    const result = await handler.rebuild();
+    return {
+      key,
+      cacheCode: entry.cacheCode,
+      status: 'OK',
+      rebuiltAtSec,
+      message: result.message,
+      affected: result.affected,
+    };
+  }
+
+  async rebuildSupport(
+    dto: RebuildSupportDto,
+  ): Promise<RedisInspectorRebuildSupportResult> {
+    const key = dto.key?.trim() ?? '';
+    if (!key || !key.startsWith('box:app:')) {
+      return {
+        key,
+        supported: false,
+        reason: 'Invalid key format',
+      };
+    }
+
+    const entry = RedisCacheKeyAllowList.find(({ matcher }) => matcher(key));
+    if (!entry) {
+      return {
+        key,
+        supported: false,
+        reason: 'Key not in allow list',
+      };
+    }
+
+    const handler = this.findHandler(entry.cacheCode);
+    if (!handler) {
+      return {
+        key,
+        cacheCode: entry.cacheCode,
+        supported: false,
+        reason: 'No rebuild handler registered for this cacheCode',
+        description: entry.description,
+      };
+    }
+
+    return {
+      key,
+      cacheCode: entry.cacheCode,
+      supported: true,
+      description: entry.description,
+    };
+  }
+
+  private findHandler(cacheCode: RedisCacheCode) {
+    return this.cacheRegistry.find((entry) => entry.cacheCode === cacheCode);
+  }
+
   private async inspectString(
     key: string,
     ttlSec: number,

+ 19 - 0
apps/box-mgnt-api/src/mgnt-backend/feature/redis-inspector/types/redis-inspector.types.ts

@@ -23,6 +23,8 @@ export interface RedisInspectorPaging {
   cursorNext?: string;
 }
 
+import { RedisCacheCode } from '../redis-cache-registry';
+
 export interface RedisInspectorRecordDetail {
   key: string;
   type?: string;
@@ -39,3 +41,20 @@ export interface RedisInspectorRebuildResult {
   message?: string;
   affected?: number;
 }
+
+export interface RedisInspectorKeyRebuildResult {
+  key: string;
+  cacheCode?: RedisCacheCode;
+  status: 'OK' | 'NOT_SUPPORTED';
+  rebuiltAtSec: number;
+  message?: string;
+  affected?: number;
+}
+
+export interface RedisInspectorRebuildSupportResult {
+  key: string;
+  cacheCode?: RedisCacheCode;
+  supported: boolean;
+  reason?: string;
+  description?: string;
+}