Skip to content

Commit

Permalink
refactor(api): replaces old learning content cache by new one
Browse files Browse the repository at this point in the history
  • Loading branch information
nlepage committed Dec 2, 2024
1 parent 75913f3 commit 1ae3594
Show file tree
Hide file tree
Showing 14 changed files with 120 additions and 274 deletions.
2 changes: 1 addition & 1 deletion api/scripts/refresh-cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ try {
await usecases.refreshLearningContentCache();
logger.info('Learning Content refreshed');
} catch (e) {
logger.error('Error while reloading cache', e);
logger.error(e, 'Error while reloading cache');
} finally {
await learningContentCache.quit();
await disconnect();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { withTransaction } from '../../../shared/domain/DomainTransaction.js';
export const createLearningContentRelease = withTransaction(
/** @param {import('./dependencies.js').Dependencies} */
async function createLearningContentRelease({
LearningContentCache,
lcmsClient,
frameworkRepository,
areaRepository,
competenceRepository,
Expand All @@ -15,7 +15,7 @@ export const createLearningContentRelease = withTransaction(
tutorialRepository,
missionRepository,
}) {
const learningContent = await LearningContentCache.instance.update();
const learningContent = await lcmsClient.createRelease();

await frameworkRepository.saveMany(learningContent.frameworks);
await areaRepository.saveMany(learningContent.areas);
Expand Down
4 changes: 2 additions & 2 deletions api/src/learning-content/domain/usecases/dependencies.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LearningContentCache } from '../../../shared/infrastructure/caches/learning-content-cache.js';
import { lcmsClient } from '../../../shared/infrastructure/lcms-client.js';
import { areaRepository } from '../../infrastructure/repositories/area-repository.js';
import { challengeRepository } from '../../infrastructure/repositories/challenge-repository.js';
import { competenceRepository } from '../../infrastructure/repositories/competence-repository.js';
Expand All @@ -25,7 +25,7 @@ export const dependencies = {
missionRepository,
lcmsRefreshCacheJobRepository,
lcmsCreateReleaseJobRepository,
LearningContentCache,
lcmsClient,
};

/** @typedef {typeof dependencies} Dependencies */
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { withTransaction } from '../../../shared/domain/DomainTransaction.js';
export const refreshLearningContentCache = withTransaction(
/** @param {import('./dependencies.js').Dependencies} */
async function refreshLearningContentCache({
LearningContentCache,
lcmsClient,
frameworkRepository,
areaRepository,
competenceRepository,
Expand All @@ -15,7 +15,7 @@ export const refreshLearningContentCache = withTransaction(
tutorialRepository,
missionRepository,
}) {
const learningContent = await LearningContentCache.instance.reset();
const learningContent = await lcmsClient.getLatestRelease();

await frameworkRepository.saveMany(learningContent.frameworks);
await areaRepository.saveMany(learningContent.areas);
Expand Down
7 changes: 3 additions & 4 deletions api/src/shared/domain/usecases/init-learning-content-cache.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
const initLearningContentCache = async function ({ LearningContentCache }) {
await LearningContentCache.instance.get();
};
export { initLearningContentCache };
export async function initLearningContentCache() {
// FIXME
}
86 changes: 35 additions & 51 deletions api/src/shared/infrastructure/caches/learning-content-cache.js
Original file line number Diff line number Diff line change
@@ -1,69 +1,53 @@
import { config } from '../../config.js';
import { lcmsClient } from '../lcms-client.js';
import { DistributedCache } from './DistributedCache.js';
import { InMemoryCache } from './InMemoryCache.js';
import { LayeredCache } from './LayeredCache.js';
import { RedisCache } from './RedisCache.js';

const LEARNING_CONTENT_CHANNEL = 'Learning content';
const LEARNING_CONTENT_CACHE_KEY = 'LearningContent';
import * as learningContentPubSub from '../caches/learning-content-pubsub.js';

export class LearningContentCache {
constructor(redisUrl) {
if (redisUrl) {
const distributedCache = new DistributedCache(new InMemoryCache(), redisUrl, LEARNING_CONTENT_CHANNEL);
const redisCache = new RedisCache(redisUrl);

this._underlyingCache = new LayeredCache(distributedCache, redisCache);
} else {
this._underlyingCache = new InMemoryCache();
}
this.generator = () => lcmsClient.getLatestRelease();
}

get() {
return this._underlyingCache.get(LEARNING_CONTENT_CACHE_KEY, this.generator);
}
#map;
#pubSub;
#name;

async reset() {
const object = await this.generator();
return this._underlyingCache.set(LEARNING_CONTENT_CACHE_KEY, object);
}
/**
* @param {{
* name: string
* pubSub: import('../caches/learning-content-pubsub.js').LearningContentPubSub
* map: Map
* }} config
* @returns
*/
constructor({ name, pubSub = learningContentPubSub.getPubSub(), map = new Map() }) {
this.#name = name;
this.#pubSub = pubSub;
this.#map = map;

async update() {
const newLearningContent = await lcmsClient.createRelease();
return this._underlyingCache.set(LEARNING_CONTENT_CACHE_KEY, newLearningContent);
this.#subscribe();
}

patch(patch) {
return this._underlyingCache.patch(LEARNING_CONTENT_CACHE_KEY, patch);
get(key) {
return this.#map.get(key);
}

flushAll() {
return this._underlyingCache.flushAll();
set(key, value) {
return this.#map.set(key, value);
}

quit() {
return this._underlyingCache.quit();
delete(key) {
return this.#pubSub.publish(this.#name, { type: 'delete', key });
}

/** @type {LearningContentCache} */
static _instance = null;

static defaultInstance() {
return new LearningContentCache(config.caching.redisUrl);
clear() {
return this.#pubSub.publish(this.#name, { type: 'clear' });
}

static get instance() {
if (!this._instance) {
this._instance = this.defaultInstance();
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(this.message.key);
}
return this._instance;
}

static set instance(_instance) {
this._instance = _instance;
console.log('POUET');
}
}

export const learningContentCache = LearningContentCache.instance;
export const learningContentCache = {
async quit() {
return learningContentPubSub.quit();
},
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Dataloader from 'dataloader';

import { knex } from '../../../../db/knex-database-connection.js';
import * as learningContentPubSub from '../caches/learning-content-pubsub.js';
import { LearningContentCache } from '../caches/learning-content-cache.js';

export class LearningContentRepository {
#tableName;
Expand All @@ -10,21 +10,15 @@ export class LearningContentRepository {
#findCache;
#findCacheMiss;

constructor({ tableName, idType = 'text', pubSub = learningContentPubSub.getPubSub() }) {
constructor({ tableName, idType = 'text' }) {
this.#tableName = tableName;
this.#idType = idType;

this.#dataloader = new Dataloader((ids) => this.#batchLoad(ids), {
cacheMap: new LearningContentCache({
name: `${tableName}:entities`,
pubSub,
}),
cacheMap: new LearningContentCache({ name: `${tableName}:entities` }),
});

this.#findCache = new LearningContentCache({
name: `${tableName}:results`,
pubSub,
});
this.#findCache = new LearningContentCache({ name: `${tableName}:results` });

this.#findCacheMiss = new Map();
}
Expand Down Expand Up @@ -81,44 +75,3 @@ export class LearningContentRepository {
this.#findCache.clear();
}
}

class LearningContentCache {
#map = new Map();
#pubSub;
#name;

/**
* @param {{
* pubSub: import('../caches/learning-content-pubsub.js').LearningContentPubSub
* name: string
* }} config
* @returns
*/
constructor({ pubSub, name }) {
this.#pubSub = pubSub;
this.#name = name;

(async () => {
for await (const message of pubSub.subscribe(name)) {
if (message.type === 'clear') this.#map.clear();
if (message.type === 'delete') this.#map.delete(this.message.key);
}
})();
}

get(key) {
return this.#map.get(key);
}

set(key, value) {
return this.#map.set(key, value);
}

delete(key) {
return this.#pubSub.publish(this.#name, { type: 'delete', key });
}

clear() {
return this.#pubSub.publish(this.#name, { type: 'clear' });
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import Redis from 'ioredis';

import { PIX_ADMIN } from '../../../../src/authorization/domain/constants.js';
import { LearningContentCache } from '../../../../src/shared/infrastructure/caches/learning-content-cache.js';
import {
createServer,
databaseBuilder,
Expand Down Expand Up @@ -59,16 +56,7 @@ describe('Acceptance | Controller | lcms-controller', function () {
});

describe('nominal case', function () {
beforeEach(function () {
LearningContentCache.instance = new LearningContentCache(process.env.TEST_REDIS_URL);
});

afterEach(async function () {
await LearningContentCache.instance._underlyingCache.flushAll();
LearningContentCache.instance = null;
});

it('should store patches in Redis and patch the DB for an assign operation', async function () {
it('should patch the DB for an assign operation', async function () {
// given
await mockLearningContent({
frameworks: [
Expand All @@ -95,18 +83,14 @@ describe('Acceptance | Controller | lcms-controller', function () {

// then
expect(response.statusCode).to.equal(204);
const redis = new Redis(process.env.TEST_REDIS_URL);
expect(await redis.lrange('cache:LearningContent:patches', 0, -1)).to.deep.equal([
JSON.stringify({ operation: 'assign', path: `frameworks[0]`, value: payload }),
]);
const frameworksInDB = await knex.select('*').from('learningcontent.frameworks').orderBy('name');
expect(frameworksInDB).to.deep.equal([
{ id: 'frameworkId', name: 'new name' },
{ id: 'frameworkId_other', name: 'other name' },
]);
});

it('should store patches in Redis and patch the DB for a push operation', async function () {
it('should patch the DB for a push operation', async function () {
// given
await mockLearningContent({
frameworks: [
Expand All @@ -133,10 +117,6 @@ describe('Acceptance | Controller | lcms-controller', function () {

// then
expect(response.statusCode).to.equal(204);
const redis = new Redis(process.env.TEST_REDIS_URL);
expect(await redis.lrange('cache:LearningContent:patches', 0, -1)).to.deep.equal([
JSON.stringify({ operation: 'push', path: `frameworks`, value: payload }),
]);
const frameworksInDB = await knex.select('*').from('learningcontent.frameworks').orderBy('name');
expect(frameworksInDB).to.deep.equal([
{ id: 'frameworkId1', name: 'name 1' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,19 @@ describe('Learning Content | Unit | UseCase | create-learning-content-release',
const tutorials = Symbol('tutorials');
const missions = Symbol('missions');

const LearningContentCache = {
instance: {
update: sinon.stub().resolves({
frameworks,
areas,
competences,
thematics,
tubes,
skills,
challenges,
courses,
tutorials,
missions,
}),
},
const lcmsClient = {
createRelease: sinon.stub().resolves({
frameworks,
areas,
competences,
thematics,
tubes,
skills,
challenges,
courses,
tutorials,
missions,
}),
};

const frameworkRepository = {
Expand Down Expand Up @@ -73,7 +71,7 @@ describe('Learning Content | Unit | UseCase | create-learning-content-release',

// when
await createLearningContentRelease({
LearningContentCache,
lcmsClient,
frameworkRepository,
areaRepository,
competenceRepository,
Expand All @@ -87,7 +85,7 @@ describe('Learning Content | Unit | UseCase | create-learning-content-release',
});

// then
expect(LearningContentCache.instance.update).to.have.been.calledOnce;
expect(lcmsClient.createRelease).to.have.been.calledOnce;
expect(frameworkRepository.saveMany).to.have.been.calledOnceWithExactly(frameworks);
expect(areaRepository.saveMany).to.have.been.calledOnceWithExactly(areas);
expect(competenceRepository.saveMany).to.have.been.calledOnceWithExactly(competences);
Expand Down
Loading

0 comments on commit 1ae3594

Please sign in to comment.