From 2084c2ce97940af9213589c88d4966c905dc92d6 Mon Sep 17 00:00:00 2001 From: Nicolas Lepage <19571875+nlepage@users.noreply.github.com> Date: Tue, 3 Dec 2024 10:36:38 +0100 Subject: [PATCH] WIP documentation and logs --- .../caches/learning-content-cache.js | 13 ++- .../learning-content-repository.js | 84 +++++++++++++++---- 2 files changed, 81 insertions(+), 16 deletions(-) diff --git a/api/src/shared/infrastructure/caches/learning-content-cache.js b/api/src/shared/infrastructure/caches/learning-content-cache.js index cb57d595c4d..255c65b80fb 100644 --- a/api/src/shared/infrastructure/caches/learning-content-cache.js +++ b/api/src/shared/infrastructure/caches/learning-content-cache.js @@ -1,4 +1,7 @@ import * as learningContentPubSub from '../caches/learning-content-pubsub.js'; +import { child } from '../utils/logger.js'; + +const logger = child('learningcontent:cache', { event: 'learningcontent' }); export class LearningContentCache { #map; @@ -39,8 +42,14 @@ export class LearningContentCache { async #subscribe() { for await (const message of this.#pubSub.subscribe(this.#name)) { - if (message.type === 'clear') this.#map.clear(); - if (message.type === 'delete') this.#map.delete(message.key); + if (message.type === 'clear') { + logger.debug({ name: this.#name }, 'clearing cache'); + this.#map.clear(); + } + if (message.type === 'delete') { + logger.debug({ name: this.#name, key: message.key }, 'deleting cache key'); + this.#map.delete(message.key); + } } } } diff --git a/api/src/shared/infrastructure/repositories/learning-content-repository.js b/api/src/shared/infrastructure/repositories/learning-content-repository.js index 18b3c4913e7..121f1324b15 100644 --- a/api/src/shared/infrastructure/repositories/learning-content-repository.js +++ b/api/src/shared/infrastructure/repositories/learning-content-repository.js @@ -2,7 +2,18 @@ import Dataloader from 'dataloader'; import { knex } from '../../../../db/knex-database-connection.js'; import { LearningContentCache } from '../caches/learning-content-cache.js'; +import { child } from '../utils/logger.js'; +const logger = child('learningcontent:repository', { event: 'learningcontent' }); + +/** + * @typedef {(knex: import('knex').QueryBuilder) => Promise} QueryBuilderCallback + */ + +/** + * Datasource for learning content repositories. + * This datasource uses a {@link Dataloader} to load and cache entities. + */ export class LearningContentRepository { #tableName; #idType; @@ -10,6 +21,12 @@ export class LearningContentRepository { #findCache; #findCacheMiss; + /** + * @param {{ + * tableName: string + * idType?: 'text' | 'integer' + * }} config + */ constructor({ tableName, idType = 'text' }) { this.#tableName = tableName; this.#idType = idType; @@ -23,21 +40,15 @@ export class LearningContentRepository { this.#findCacheMiss = new Map(); } + /** + * Finds several entities using a request and caches results. + * The request is built using a knex query builder given to {@link callback}. + * {@link cacheKey} must vary according to params given to the query builder. + * @param {string} cacheKey + * @param {QueryBuilderCallback} callback + * @returns {Promise} + */ async find(cacheKey, callback) { - return this.#findDtos(callback, cacheKey); - } - - async load(id) { - if (!id) return null; - return this.#dataloader.load(id); - } - - async loadMany(ids) { - const notNullIds = ids.filter((id) => id); - return this.#dataloader.loadMany(notNullIds); - } - - #findDtos(callback, cacheKey) { let dtos = this.#findCache.get(cacheKey); if (dtos) return dtos; @@ -52,14 +63,51 @@ export class LearningContentRepository { return dtos; } + /** + * Loads one entity by ID. + * @param {string|number} id + * @returns {Promise} + */ + async load(id) { + if (!id) return null; + return this.#dataloader.load(id); + } + + /** + * Loads several entities by ID. + * @param {string[]|number[]} ids + * @returns {Promise} + */ + async loadMany(ids) { + const notNullIds = ids.filter((id) => id); + return this.#dataloader.loadMany(notNullIds); + } + + /** + * Loads entities from database using a request and writes result to cache. + * @param {string} cacheKey + * @param {QueryBuilderCallback} callback + * @returns {Promise} + */ async #loadDtos(callback, cacheKey) { const ids = await callback(knex.pluck(`${this.#tableName}.id`).from(this.#tableName)); const dtos = await this.#dataloader.loadMany(ids); + + logger.debug({ tableName: this.#tableName, cacheKey }, 'caching find result'); this.#findCache.set(cacheKey, dtos); + return dtos; } + /** + * Loads a batch of entities from database by ID. + * Entities are returned in the same order as {@link ids}. + * If an ID is not found, it is null in results. + * @param {string[]|number[]} ids + * @returns {Promise<(object|null)[]>} + */ async #batchLoad(ids) { + logger.debug({ tableName: this.#tableName, count: ids.length }, 'loading from PG'); const dtos = await knex .select(`${this.#tableName}.*`) .from(knex.raw(`unnest(?::${this.#idType}[]) with ordinality as ids(id, idx)`, [ids])) // eslint-disable-line knex/avoid-injections @@ -68,7 +116,15 @@ export class LearningContentRepository { return dtos.map((dto) => (dto.id ? dto : null)); } + /** + * Clears repository’s cache. + * If {@link id} is undefined, all cache is cleared. + * If {@link id} is given, cache is partially cleared. + * @param {string|number|undefined} id + */ clearCache(id) { + logger.debug({ tableName: this.#tableName, id }, 'trigerring cache clear'); + if (id) { this.#dataloader.clear(id); } else {