refactor(backend): Redis -> Memcached
This commit is contained in:
parent
0f2a502521
commit
0928df2cab
9 changed files with 1124 additions and 1152 deletions
|
@ -67,9 +67,9 @@ DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://hostname/realms/teacher/protocol/openid
|
||||||
# If running your stack in docker, this should use the docker service name.
|
# If running your stack in docker, this should use the docker service name.
|
||||||
#DWENGO_LOGGING_LOKI_HOST=http://localhost:3102
|
#DWENGO_LOGGING_LOKI_HOST=http://localhost:3102
|
||||||
|
|
||||||
# The hostname or IP address of the Redis cache.
|
# The hostname or IP address of the caching server, e.g. Memcached.
|
||||||
# If running your stack in docker, this should use the docker service name.
|
# If running your stack in docker, this should use the docker service name.
|
||||||
#DWENGO_CACHE_HOST=localhost
|
#DWENGO_CACHE_HOST=localhost
|
||||||
#DWENGO_CACHE_PORT=6379
|
#DWENGO_CACHE_PORT=11211
|
||||||
#DWENGO_CACHE_TTL=3600
|
#DWENGO_CACHE_TTL=3600
|
||||||
#DWENGO_CACHE_KEY_PREFIX=dwengo
|
#DWENGO_CACHE_KEY_PREFIX=dwengo
|
||||||
|
|
|
@ -37,5 +37,5 @@ DWENGO_LOGGING_LEVEL=info
|
||||||
DWENGO_LOGGING_LOKI_HOST=http://logging:3102
|
DWENGO_LOGGING_LOKI_HOST=http://logging:3102
|
||||||
|
|
||||||
DWENGO_CACHE_HOST=cache
|
DWENGO_CACHE_HOST=cache
|
||||||
#DWENGO_CACHE_PORT=6379
|
#DWENGO_CACHE_PORT=11211
|
||||||
DWENGO_CACHE_TTL=604800
|
DWENGO_CACHE_TTL=604800
|
||||||
|
|
|
@ -37,8 +37,8 @@
|
||||||
"jwks-rsa": "^3.1.0",
|
"jwks-rsa": "^3.1.0",
|
||||||
"loki-logger-ts": "^1.0.2",
|
"loki-logger-ts": "^1.0.2",
|
||||||
"marked": "^15.0.7",
|
"marked": "^15.0.7",
|
||||||
|
"memjs": "^1.3.2",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"redis": "^5.0.1",
|
|
||||||
"response-time": "^2.3.3",
|
"response-time": "^2.3.3",
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "^5.0.1",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
|
@ -50,6 +50,7 @@
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
"@types/memjs": "^1.3.3",
|
||||||
"@types/node": "^22.13.4",
|
"@types/node": "^22.13.4",
|
||||||
"@types/response-time": "^2.3.8",
|
"@types/response-time": "^2.3.8",
|
||||||
"@types/swagger-ui-express": "^4.1.8",
|
"@types/swagger-ui-express": "^4.1.8",
|
||||||
|
|
|
@ -1,42 +1,40 @@
|
||||||
import { createClient, RedisClientType } from 'redis';
|
|
||||||
import { getLogger } from './logging/initalize.js';
|
import { getLogger } from './logging/initalize.js';
|
||||||
import { envVars, getEnvVar } from './util/envVars.js';
|
import { envVars, getEnvVar } from './util/envVars.js';
|
||||||
|
import { Client } from 'memjs';
|
||||||
|
|
||||||
export type CacheClient = RedisClientType;
|
export type CacheClient = Client;
|
||||||
|
|
||||||
let redisClient: CacheClient;
|
let cacheClient: CacheClient;
|
||||||
|
|
||||||
async function initializeClient(): Promise<CacheClient> {
|
async function initializeClient(): Promise<CacheClient> {
|
||||||
if (redisClient !== undefined) {
|
if (cacheClient !== undefined) {
|
||||||
return redisClient;
|
return cacheClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
const redisHost = getEnvVar(envVars.CacheHost);
|
const cachingHost = getEnvVar(envVars.CacheHost);
|
||||||
const redisPort = getEnvVar(envVars.CachePort);
|
const cachingPort = getEnvVar(envVars.CachePort);
|
||||||
const redisUrl = `redis://${redisHost}:${redisPort}`;
|
const cachingUrl = `${cachingHost}:${cachingPort}`;
|
||||||
|
|
||||||
redisClient = createClient({
|
cacheClient = Client.create(cachingUrl);
|
||||||
url: redisUrl
|
|
||||||
});
|
|
||||||
|
|
||||||
redisClient.on('error', (err) => getLogger().error('Redis error:', err));
|
getLogger().info(`Memcached client initialized at ${cachingUrl}`);
|
||||||
await redisClient.connect();
|
|
||||||
|
|
||||||
return redisClient;
|
return cacheClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCacheClient(): Promise<CacheClient> {
|
export async function getCacheClient(): Promise<CacheClient> {
|
||||||
redisClient ||= await initializeClient();
|
cacheClient ||= await initializeClient();
|
||||||
return redisClient;
|
return cacheClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkRedisHealth(): Promise<boolean> {
|
export async function checkCachingHealth(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await redisClient.set('health', 'ok');
|
const client = await getCacheClient();
|
||||||
const reply = await redisClient.get('health');
|
await client.set('health', Buffer.from('ok'), { expires: 60 });
|
||||||
return reply === 'ok';
|
const reply = await cacheClient.get('health');
|
||||||
|
return reply?.value?.toString() === 'ok';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
getLogger().error('Redis Health Check Failed:', error);
|
getLogger().error('Caching Health Check Failed:', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { getLogger, Logger } from '../logging/initalize.js';
|
||||||
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
|
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
|
||||||
import { getCacheClient } from '../caching.js';
|
import { getCacheClient } from '../caching.js';
|
||||||
import { envVars, getEnvVar, getNumericEnvVar } from './envVars.js';
|
import { envVars, getEnvVar, getNumericEnvVar } from './envVars.js';
|
||||||
|
import { createHash } from 'crypto';
|
||||||
|
|
||||||
const logger: Logger = getLogger();
|
const logger: Logger = getLogger();
|
||||||
const runMode: string = getEnvVar(envVars.RunMode);
|
const runMode: string = getEnvVar(envVars.RunMode);
|
||||||
|
@ -29,13 +30,13 @@ export async function fetchRemote<T>(
|
||||||
url: string,
|
url: string,
|
||||||
description: string,
|
description: string,
|
||||||
options?: Options,
|
options?: Options,
|
||||||
cacheTTL?: number
|
cacheTTL?: number,
|
||||||
): Promise<T | null> {
|
): Promise<T | null> {
|
||||||
if (runMode !== 'dev') {
|
if (runMode !== 'dev' && !runMode.includes('test')) {
|
||||||
return fetchWithCache<T>(url, description, options, cacheTTL);
|
return fetchWithCache<T>(url, description, options, cacheTTL);
|
||||||
}
|
}
|
||||||
|
|
||||||
getLogger().info(`🔄 INFO: Bypassing cache for ${description} at "${url}".`);
|
getLogger().debug(`🔄 INFO: Bypassing cache for ${description} at "${url}".`);
|
||||||
return fetchWithLogging(url, description, options);
|
return fetchWithLogging(url, description, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,25 +44,28 @@ async function fetchWithCache<T>(
|
||||||
url: string,
|
url: string,
|
||||||
description: string,
|
description: string,
|
||||||
options?: Options,
|
options?: Options,
|
||||||
cacheTTL?: number
|
cacheTTL?: number,
|
||||||
): Promise<T | null> {
|
): Promise<T | null> {
|
||||||
// Create a unique cache key based on the URL and options
|
// Combine the URL and parameters to create a unique cache key.
|
||||||
const cacheKey = `${prefix}:${url}${options?.params ? JSON.stringify(options.params) : ''}`;
|
// NOTE Using a hash function to keep the key short, since Memcached has a limit on key size
|
||||||
const cacheClient = await getCacheClient();
|
const urlWithParams = `${url}${options?.params ? JSON.stringify(options.params) : ''}`;
|
||||||
|
const hashedUrl = createHash('sha256').update(urlWithParams).digest('hex');
|
||||||
|
const key = `${prefix}:${hashedUrl}`;
|
||||||
|
const client = await getCacheClient();
|
||||||
|
|
||||||
const cachedData = await cacheClient.get(cacheKey);
|
const cachedData = await client.get(key);
|
||||||
if (cachedData !== null && cachedData !== undefined) { // TODO What should this condition actually be?
|
|
||||||
// Cache hit! :)
|
if (cachedData?.value) {
|
||||||
getLogger().debug(`✅ INFO: Cache hit for ${description} at "${url}".`);
|
logger.debug(`✅ INFO: Cache hit for ${description} at "${url}" (key: "${key}")`);
|
||||||
return JSON.parse(cachedData) as T;
|
return JSON.parse(cachedData.value.toString()) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache miss :(
|
|
||||||
logger.debug(`🔄 INFO: Cache miss for ${description} at "${url}". Fetching data...`);
|
logger.debug(`🔄 INFO: Cache miss for ${description} at "${url}". Fetching data...`);
|
||||||
const response = await fetchWithLogging<T>(url, description, options);
|
const response = await fetchWithLogging<T>(url, description, options);
|
||||||
logger.debug(`🔄 INFO: Fetched data for ${description} at "${url}".`);
|
|
||||||
await cacheClient.setEx(cacheKey, cacheTTL || getNumericEnvVar(envVars.CacheTTL), JSON.stringify(response));
|
const ttl = cacheTTL || getNumericEnvVar(envVars.CacheTTL);
|
||||||
logger.debug(`✅ INFO: Cached response for ${description} at "${url}" for ${cacheTTL} seconds.`);
|
await client.set(key, JSON.stringify(response), { expires: ttl });
|
||||||
|
logger.debug(`✅ INFO: Cached response for ${description} at "${url}" for ${ttl} seconds. (key: "${key}")`);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ export const envVars: Record<string, EnvVar> = {
|
||||||
LokiHost: { key: LOGGING_PREFIX + 'LOKI_HOST', defaultValue: 'http://localhost:3102' },
|
LokiHost: { key: LOGGING_PREFIX + 'LOKI_HOST', defaultValue: 'http://localhost:3102' },
|
||||||
|
|
||||||
CacheHost: { key: CACHE_PREFIX + 'HOST', defaultValue: 'localhost' },
|
CacheHost: { key: CACHE_PREFIX + 'HOST', defaultValue: 'localhost' },
|
||||||
CachePort: { key: CACHE_PREFIX + 'PORT', defaultValue: 6379 },
|
CachePort: { key: CACHE_PREFIX + 'PORT', defaultValue: 11211 },
|
||||||
CacheTTL: { key: CACHE_PREFIX + 'TTL', defaultValue: 60 * 60 * 24 }, // 24 hours
|
CacheTTL: { key: CACHE_PREFIX + 'TTL', defaultValue: 60 * 60 * 24 }, // 24 hours
|
||||||
CacheKeyPrefix: { key: CACHE_PREFIX + 'KEY_PREFIX', defaultValue: 'dwengo' },
|
CacheKeyPrefix: { key: CACHE_PREFIX + 'KEY_PREFIX', defaultValue: 'dwengo' },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
@ -143,6 +143,16 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- dwengo_grafana_data:/var/lib/grafana
|
- dwengo_grafana_data:/var/lib/grafana
|
||||||
|
|
||||||
|
caching:
|
||||||
|
image: memcached
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- '11211:11211'
|
||||||
|
command:
|
||||||
|
- --conn-limit=1024
|
||||||
|
- --memory-limit=64
|
||||||
|
- --threads=4
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
dwengo_grafana_data:
|
dwengo_grafana_data:
|
||||||
dwengo_letsencrypt:
|
dwengo_letsencrypt:
|
||||||
|
|
|
@ -87,16 +87,17 @@ services:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
caching:
|
caching:
|
||||||
image: redis:6.2-alpine
|
image: memcached
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- '6379:6379'
|
- '11211:11211'
|
||||||
command: redis-server --save 20 1 --loglevel warning
|
command:
|
||||||
volumes:
|
- --conn-limit=1024
|
||||||
- dwengo_cache:/data
|
- --memory-limit=2048
|
||||||
|
- -I 128m
|
||||||
|
- --threads=4
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
dwengo_grafana_data:
|
dwengo_grafana_data:
|
||||||
dwengo_loki_data:
|
dwengo_loki_data:
|
||||||
dwengo_postgres_data:
|
dwengo_postgres_data:
|
||||||
dwengo_cache:
|
|
||||||
|
|
2166
package-lock.json
generated
2166
package-lock.json
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue