[TIL] Cache Handling & Invalidation (feat. Cache Interceptor)

06/23/23

·

3 min read

[TIL] Cache Handling & Invalidation (feat. Cache Interceptor)

Cache Handling (Service Layer → Cache Interceptor)

In the existing code, the service layer checked for cached data and returned it if available, or performed the business logic and cached the return value if not. However, this approach was inefficient in terms of maintenance and code readability since the cache handling was mixed with the business logic in the service layer. To address this, a cache interceptor, called CacheInterceptor, was created to handle caching separately. It was applied in the controller using @UseInterceptors().

  • CacheInterceptor code
@Injectable()
export class CacheInterceptor implements NestInterceptor {
  protected cacheMethods = ['GET'];
  constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}

  async intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Promise<Observable<any>> {
    // Apply caching only for GET requests to retrieve recommended hospitals
    if (this.isRequestGet(context)) {
      const request = context.switchToHttp().getRequest();
      const cacheKey = this.generateCacheKey(request);
      console.log('cacheKey:', cacheKey);
      // Check if cached data exists
      const cachedData = await this.cacheManager.get(cacheKey);
      if (cachedData) {
        console.log('Using cached data.');
        return of(cachedData);
      }

      // If cached data does not exist, handle the request
      return next.handle().pipe(
        tap((data) => {
          // Store the data in the cache
          console.log('Storing data in the cache.');
          this.cacheManager.set(cacheKey, data);
        }),
      );
    }
  }

  private generateCacheKey(request): string {
    // Generate a unique cache key based on the request URL and query parameters
    const reportId = request.params.report_id;
    const radius = request.query.radius;
    const maxCount = request.query.max_count;
    return `${reportId}:${radius}:${maxCount}`;
  }

  private isRequestGet(context: ExecutionContext): boolean {
    const req = context.switchToHttp().getRequest();
    return this.cacheMethods.includes(req.method);
  }
}

Cache Invalidation

When fetching the list of recommended hospitals on the frontend, if a user refreshes the page within 1 minute of the initial request, the cached data is retrieved. However, when a patient applies for or withdraws a hospital transfer, the screen should immediately reflect the appearance or disappearance of a notification window and transfer request cancellation button for the hospital to which the patient applied. However, due to the use of cached data, this immediate update was not possible. To address this issue, the ClearCacheInterceptor was implemented to delete the cached data associated with the specific report_id when a transfer request or cancellation occurs. Since there can be multiple cached data with the same report_id but different radius and max_count query parameters, all cache keys starting with the report_id were cleared.

  • ClearCacheInterceptor code
@Injectable()
export class ClearCacheInterceptor implements NestInterceptor {
  protected clearCacheMethods = ['POST', 'DELETE'];
  constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}

  async intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Promise<Observable<any>> {
    if (this.isRequestPostOrDelete(context)) {
      const request = context.switchToHttp().getRequest();
      // 1. Pass the report_id to the method
      await this.clearCacheKeysStartingWith(request.params.report_id);
    }
    return next.handle();
  }

  private async clearCacheKeysStartingWith(reportId: string): Promise<void> {
    const

 cacheKeys = await this.getCacheKeysStartingWith(reportId);
    // 4. Iterate through the filtered keys and delete them from Redis
    await Promise.all(cacheKeys.map((key) => this.cacheManager.del(key)));
  }

  private async getCacheKeysStartingWith(prefix: string): Promise<string[]> {
    // 2. Retrieve all keys stored in Redis
    const cacheKeys = await this.cacheManager.store.keys('*');
    // 3. Filter keys that start with report_id:
    return cacheKeys.filter((key) => key.startsWith(`${prefix}:`));
  }

  private isRequestPostOrDelete(context: ExecutionContext): boolean {
    const req = context.switchToHttp().getRequest();
    return this.clearCacheMethods.includes(req.method);
  }
}