From 0fa985842573c1c8564cd7ff23f02f5a48585cfd Mon Sep 17 00:00:00 2001 From: bramjanssen Date: Mon, 2 Dec 2024 17:05:52 +0100 Subject: [PATCH] feat: added caching for the jobs controller --- package-lock.json | 84 ++++++++++++++++++++ package.json | 2 + src/app.module.ts | 2 + src/caching/caching.module.ts | 22 +++++ src/caching/services/cache.service.ts | 46 +++++++++++ src/config/schema/config.schema.ts | 16 ++++ src/jobs/controllers/jobs/jobs.controller.ts | 12 ++- src/jobs/jobs.module.ts | 2 + 8 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 src/caching/caching.module.ts create mode 100644 src/caching/services/cache.service.ts diff --git a/package-lock.json b/package-lock.json index fdc15ed..a36f088 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@elastic/elasticsearch": "~7.12.0", "@nestjs/axios": "^1.0.0", + "@nestjs/cache-manager": "^2.3.0", "@nestjs/common": "^9.1.6", "@nestjs/core": "^9.1.6", "@nestjs/elasticsearch": "^9.0.0", @@ -18,6 +19,7 @@ "@nestjs/platform-express": "^9.1.6", "@nestjs/swagger": "^6.1.2", "@nestjs/terminus": "^9.1.4", + "cache-manager": "^5.7.6", "class-transformer": "^0.5.1", "class-validator": "^0.13.2", "commit-analyzer-fail-on-no-release": "^1.0.1", @@ -1864,6 +1866,18 @@ "rxjs": "^6.0.0 || ^7.0.0" } }, + "node_modules/@nestjs/cache-manager": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-2.3.0.tgz", + "integrity": "sha512-pxeBp9w/s99HaW2+pezM1P3fLiWmUEnTUoUMLa9UYViCtjj0E0A19W/vaT5JFACCzFIeNrwH4/16jkpAhQ25Vw==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "cache-manager": "<=5", + "rxjs": "^7.0.0" + } + }, "node_modules/@nestjs/cli": { "version": "9.1.5", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-9.1.5.tgz", @@ -4027,6 +4041,27 @@ "node": ">= 0.8" } }, + "node_modules/cache-manager": { + "version": "5.7.6", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-5.7.6.tgz", + "integrity": "sha512-wBxnBHjDxF1RXpHCBD6HGvKER003Ts7IIm0CHpggliHzN1RZditb7rXoduE1rplc2DEFYKxhLKgFuchXMJje9w==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1", + "lodash.clonedeep": "^4.5.0", + "lru-cache": "^10.2.2", + "promise-coalesce": "^1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/cache-manager/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -5495,6 +5530,12 @@ "node": ">=6" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -12191,6 +12232,15 @@ "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" }, + "node_modules/promise-coalesce": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/promise-coalesce/-/promise-coalesce-1.1.2.tgz", + "integrity": "sha512-zLaJ9b8hnC564fnJH6NFSOGZYYdzrAJn2JUUIwzoQb32fG2QAakpDNM+CZo1km6keXkRXRM+hml1BFAPVnPkxg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=16" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -16149,6 +16199,12 @@ "axios": "1.1.3" } }, + "@nestjs/cache-manager": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-2.3.0.tgz", + "integrity": "sha512-pxeBp9w/s99HaW2+pezM1P3fLiWmUEnTUoUMLa9UYViCtjj0E0A19W/vaT5JFACCzFIeNrwH4/16jkpAhQ25Vw==", + "requires": {} + }, "@nestjs/cli": { "version": "9.1.5", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-9.1.5.tgz", @@ -17754,6 +17810,24 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, + "cache-manager": { + "version": "5.7.6", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-5.7.6.tgz", + "integrity": "sha512-wBxnBHjDxF1RXpHCBD6HGvKER003Ts7IIm0CHpggliHzN1RZditb7rXoduE1rplc2DEFYKxhLKgFuchXMJje9w==", + "requires": { + "eventemitter3": "^5.0.1", + "lodash.clonedeep": "^4.5.0", + "lru-cache": "^10.2.2", + "promise-coalesce": "^1.1.2" + }, + "dependencies": { + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + } + } + }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -18834,6 +18908,11 @@ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" }, + "eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + }, "events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -23602,6 +23681,11 @@ "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" }, + "promise-coalesce": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/promise-coalesce/-/promise-coalesce-1.1.2.tgz", + "integrity": "sha512-zLaJ9b8hnC564fnJH6NFSOGZYYdzrAJn2JUUIwzoQb32fG2QAakpDNM+CZo1km6keXkRXRM+hml1BFAPVnPkxg==" + }, "prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", diff --git a/package.json b/package.json index d19e6b7..a2b3c14 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "dependencies": { "@elastic/elasticsearch": "~7.12.0", "@nestjs/axios": "^1.0.0", + "@nestjs/cache-manager": "^2.3.0", "@nestjs/common": "^9.1.6", "@nestjs/core": "^9.1.6", "@nestjs/elasticsearch": "^9.0.0", @@ -33,6 +34,7 @@ "@nestjs/platform-express": "^9.1.6", "@nestjs/swagger": "^6.1.2", "@nestjs/terminus": "^9.1.4", + "cache-manager": "^5.7.6", "class-transformer": "^0.5.1", "class-validator": "^0.13.2", "commit-analyzer-fail-on-no-release": "^1.0.1", diff --git a/src/app.module.ts b/src/app.module.ts index 67ebd78..786ada1 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,6 +8,7 @@ import { LoggerMiddleware } from './middleware/logger.middleware'; import { ConfigService } from './config/config/config.service'; import { LoggerModule } from 'nestjs-pino'; import { v4 as uuidv4 } from 'uuid'; +import { CachingModule } from './caching/caching.module'; @Module({ imports: [ @@ -41,6 +42,7 @@ import { v4 as uuidv4 } from 'uuid'; }; }, }), + CachingModule, ], }) export class AppModule implements NestModule { diff --git a/src/caching/caching.module.ts b/src/caching/caching.module.ts new file mode 100644 index 0000000..4fa64e4 --- /dev/null +++ b/src/caching/caching.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { CachingService } from './services/cache.service'; +import { ConfigModule } from '../config/config.module'; +import { ConfigService } from '../config/config/config.service'; +import { CacheModule } from '@nestjs/cache-manager'; + +@Module({ + imports: [ + ConfigModule, + CacheModule.registerAsync({ + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ + ttl: configService.get('cache.ttl'), + max: configService.get('cache.max'), + }), + inject: [ConfigService], + }), + ], + providers: [CachingService], + exports: [CachingService], +}) +export class CachingModule {} diff --git a/src/caching/services/cache.service.ts b/src/caching/services/cache.service.ts new file mode 100644 index 0000000..65a0aad --- /dev/null +++ b/src/caching/services/cache.service.ts @@ -0,0 +1,46 @@ +import { Inject, Injectable, Logger } from '@nestjs/common'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Cache } from 'cache-manager'; + +@Injectable() +export class CachingService { + private logger = new Logger('CachingService'); + + constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {} + + /** + * Retrieve an element from the cache. If the element could not be found or an error occured, it will return undefined. + * @param key - Key to use for storing the results + */ + public async checkCache(key: string): Promise { + try { + this.logger.debug(`Checking cache for key: ${key}`); + const result = await this.cacheManager.get(key); + if (result) { + this.logger.debug(`Found result in cache`); + } else { + this.logger.warn('Could not find entry in cache'); + } + return result; + } catch (error) { + this.logger.error(`Could not query cache: ${JSON.stringify(error)}`); + return undefined; + } + } + + /** + * Store a result into the cache of the application + * @param key - Key used to store the value in the cache. + * @param value - Value to store in the cache. + */ + public async store(key: string, value: any) { + try { + this.logger.debug(`Storing body cache for key: ${key}`); + await this.cacheManager.set(key, value); + } catch (error) { + this.logger.error( + `Could not store value in cache cache: ${JSON.stringify(error)}`, + ); + } + } +} diff --git a/src/config/schema/config.schema.ts b/src/config/schema/config.schema.ts index c95812c..38d44a3 100644 --- a/src/config/schema/config.schema.ts +++ b/src/config/schema/config.schema.ts @@ -84,4 +84,20 @@ export const schema: convict.Schema = { arg: 'db_jobs_scroll_timeout', }, }, + cache: { + ttl: { + doc: 'TTL in miliseconds for caching the results', + format: 'Number', + default: 5000, + env: 'CACHE_TTL', + arg: 'cache_ttl', + }, + max: { + doc: 'Maximum number of results to store in cache', + format: 'Number', + default: 100, + env: 'CACHE_MAX_ENTRIES', + arg: 'cache_max_entries', + }, + }, }; diff --git a/src/jobs/controllers/jobs/jobs.controller.ts b/src/jobs/controllers/jobs/jobs.controller.ts index 52c49ac..30ee06b 100644 --- a/src/jobs/controllers/jobs/jobs.controller.ts +++ b/src/jobs/controllers/jobs/jobs.controller.ts @@ -13,11 +13,13 @@ import { import { ApiBody, ApiOperation } from '@nestjs/swagger'; import { Job, PatchJob } from '../../models/job.dto'; import { DatabaseService } from '../../services/database/database.service'; +import { CachingService } from '../../../caching/services/cache.service'; @Controller('jobs') export class JobsController { constructor( private databaseService: DatabaseService, + private cachingService: CachingService, private logger: Logger, ) {} @@ -47,7 +49,15 @@ export class JobsController { }) async queryJobs(@Body() query: any): Promise { try { - return (await this.databaseService.queryJobs(query)) as Job[]; + const cacheKey = `search_result_${btoa(JSON.stringify(query))}`; + let result: Job[] = await this.cachingService.checkCache(cacheKey); + + if (!result) { + result = (await this.databaseService.queryJobs(query)) as Job[]; + await this.cachingService.store(cacheKey, result); + } + + return result; } catch (error: any) { this.logger.error( `Could not query jobs: ${JSON.stringify(error)}`, diff --git a/src/jobs/jobs.module.ts b/src/jobs/jobs.module.ts index e6dee7f..440da7b 100644 --- a/src/jobs/jobs.module.ts +++ b/src/jobs/jobs.module.ts @@ -4,11 +4,13 @@ import { ElasticsearchModule } from '@nestjs/elasticsearch'; import { ConfigModule } from '../config/config.module'; import { ConfigService } from '../config/config/config.service'; import { DatabaseService } from './services/database/database.service'; +import { CachingModule } from '../caching/caching.module'; @Module({ controllers: [JobsController], imports: [ ConfigModule, + CachingModule, ElasticsearchModule.registerAsync({ imports: [ConfigModule], useFactory: async (configService: ConfigService) => ({