Skip to content

Commit

Permalink
Fix Redis
Browse files Browse the repository at this point in the history
  • Loading branch information
cskrov committed Jul 22, 2024
1 parent 0f439bd commit 0f999d4
Show file tree
Hide file tree
Showing 10 changed files with 241 additions and 84 deletions.
12 changes: 9 additions & 3 deletions server/src/auth/cache/cache-gauge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,27 @@ import { Counter, Gauge, Histogram } from 'prom-client';

const labelNames = ['hit'] as const;

export const cacheRedisGauge = new Counter({
export const redisCacheGauge = new Counter({
name: 'obo_redis_cache',
help: 'Number of requests to the Redis OBO cache. "hit" is the type of hit: "miss", "invalid", "hit" or "expired".',
labelNames,
registers: [proxyRegister],
});

export const cacheGauge = new Counter({
export const redisCacheSizeGauge = new Gauge({
name: 'obo_redis_cache_size',
help: 'Number of OBO tokens in the Redis cache.',
registers: [proxyRegister],
});

export const memoryCacheGauge = new Counter({
name: 'obo_cache',
help: 'Number of requests to the OBO cache. "hit" is the type of hit: "miss", "redis", "hit", or "expired".',
labelNames,
registers: [proxyRegister],
});

export const cacheSizeGauge = new Gauge({
export const memoryCacheSizeGauge = new Gauge({
name: 'obo_cache_size',
help: 'Number of OBO tokens in the cache.',
registers: [proxyRegister],
Expand Down
52 changes: 39 additions & 13 deletions server/src/auth/cache/cache.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,45 @@
import { OboCacheInterface } from '@app/auth/cache/interface';
import { oboMemoryCache } from '@app/auth/cache/memory-cache';
import { OboMemoryCache } from '@app/auth/cache/memory-cache';
import { OboRedisCache } from '@app/auth/cache/redis-cache';
import { optionalEnvString } from '@app/config/env-var';

const REDIS_URI = optionalEnvString('REDIS_URI_OBO_CACHE');
const REDIS_USERNAME = optionalEnvString('REDIS_USERNAME_OBO_CACHE');
const REDIS_PASSWORD = optionalEnvString('REDIS_PASSWORD_OBO_CACHE');

class OboTieredCache implements OboCacheInterface {
private oboRedisCache: OboRedisCache;
class OboTieredCache {
#oboRedisCache: OboRedisCache;
#oboMemoryCache: OboMemoryCache | null = null;
#isReady = false;

constructor(redisUri: string, redisUsername: string, redisPassword: string) {
this.oboRedisCache = new OboRedisCache(redisUri, redisUsername, redisPassword);
this.#oboRedisCache = new OboRedisCache(redisUri, redisUsername, redisPassword);
this.#init();
}

async #init() {
await this.#oboRedisCache.init();
const allTokenMessages = await this.#oboRedisCache.getAll();
const oboMemoryCache = new OboMemoryCache(allTokenMessages);
this.#oboMemoryCache = oboMemoryCache;
this.#oboRedisCache.addTokenListener(({ key, token, expiresAt }) => oboMemoryCache.set(key, token, expiresAt));
this.#isReady = true;
}

public async get(key: string): Promise<string | null> {
const memoryHit = await oboMemoryCache.get(key);
if (this.#oboMemoryCache === null) {
return null;
}

const memoryHit = this.#oboMemoryCache.get(key);

if (memoryHit !== null) {
return memoryHit.token;
}

const redisHit = await this.oboRedisCache.get(key);
const redisHit = await this.#oboRedisCache.get(key);

if (redisHit !== null) {
oboMemoryCache.set(key, redisHit.token, redisHit.expiresAt);
this.#oboMemoryCache.set(key, redisHit.token, redisHit.expiresAt);

return redisHit.token;
}
Expand All @@ -33,19 +48,30 @@ class OboTieredCache implements OboCacheInterface {
}

public async set(key: string, token: string, expiresAt: number): Promise<void> {
await Promise.all([oboMemoryCache.set(key, token, expiresAt), this.oboRedisCache.set(key, token, expiresAt)]);
this.#oboMemoryCache?.set(key, token, expiresAt);
await this.#oboRedisCache.set(key, token, expiresAt);
}

public get isReady(): boolean {
return this.#isReady && this.#oboRedisCache.isReady;
}
}

class OboSimpleCache {
public async get(key: string): Promise<string | null> {
const memoryHit = await oboMemoryCache.get(key);
#oboMemoryCache = new OboMemoryCache([]);

public get(key: string): string | null {
const memoryHit = this.#oboMemoryCache.get(key);

return memoryHit?.token ?? null;
}

public async set(key: string, token: string, expiresAt: number): Promise<void> {
await oboMemoryCache.set(key, token, expiresAt);
public set(key: string, token: string, expiresAt: number): void {
this.#oboMemoryCache.set(key, token, expiresAt);
}

public get isReady(): boolean {
return true;
}
}

Expand Down
9 changes: 0 additions & 9 deletions server/src/auth/cache/interface.ts

This file was deleted.

58 changes: 31 additions & 27 deletions server/src/auth/cache/memory-cache.ts
Original file line number Diff line number Diff line change
@@ -1,71 +1,77 @@
import { cacheGauge, cacheSizeGauge } from '@app/auth/cache/cache-gauge';
import { OboCacheTierInterface } from '@app/auth/cache/interface';
import { memoryCacheGauge, memoryCacheSizeGauge } from '@app/auth/cache/cache-gauge';
import { TokenMessage } from '@app/auth/cache/types';
import { getLogger } from '@app/logger';

const log = getLogger('obo-cache');
const log = getLogger('obo-memory-cache');

type Value = [string, number];

export class OboMemoryCache implements OboCacheTierInterface {
private cache: Map<string, Value> = new Map();
export class OboMemoryCache {
#cache: Map<string, Value>;

constructor(tokenMessages: TokenMessage[]) {
this.#cache = new Map<string, Value>(
tokenMessages.map((tokenMessage) => [tokenMessage.key, [tokenMessage.token, tokenMessage.expiresAt]]),
);

log.info({ msg: `Created OBO memory cache with ${tokenMessages.length} tokens.` });

constructor() {
/**
* Clean OBO token cache every 10 minutes.
* OBO tokens expire after 1 hour.
*/
setInterval(() => this.clean(), 10 * 60 * 1000); // 10 minutes.
setInterval(() => this.#clean(), 10 * 60 * 1_000);
}

public async get(key: string) {
const value = this.cache.get(key);
public get(key: string) {
const value = this.#cache.get(key);

if (value === undefined) {
cacheGauge.inc({ hit: 'miss' });
memoryCacheGauge.inc({ hit: 'miss' });

return null;
}

const [token, expiresAt] = value;

if (expiresAt <= now()) {
cacheGauge.inc({ hit: 'expired' });
this.cache.delete(key);
cacheSizeGauge.set(this.cache.size);
memoryCacheGauge.inc({ hit: 'expired' });
this.#cache.delete(key);
memoryCacheSizeGauge.set(this.#cache.size);

return null;
}

cacheGauge.inc({ hit: 'hit' });
memoryCacheGauge.inc({ hit: 'hit' });

return { token, expiresAt };
}

public async set(key: string, token: string, expiresAt: number) {
this.cache.set(key, [token, expiresAt]);
cacheSizeGauge.set(this.cache.size);
public set(key: string, token: string, expiresAt: number) {
this.#cache.set(key, [token, expiresAt]);
memoryCacheSizeGauge.set(this.#cache.size);
}

private all() {
return Array.from(this.cache.entries());
#all() {
return Array.from(this.#cache.entries());
}

private clean() {
const before = this.cache.size;
#clean() {
const before = this.#cache.size;
const timestamp = now();

const deleted: number = this.all()
const deleted: number = this.#all()
.map(([key, [, expires_at]]) => {
if (expires_at <= timestamp) {
return this.cache.delete(key);
return this.#cache.delete(key);
}

return false;
})
.filter((d) => d).length;

const after = this.cache.size;
cacheSizeGauge.set(after);
const after = this.#cache.size;
memoryCacheSizeGauge.set(after);

if (deleted === 0) {
log.debug({ msg: `Cleaned the OBO token cache. No expired tokens found. Cache had ${before} tokens.` });
Expand All @@ -80,5 +86,3 @@ export class OboMemoryCache implements OboCacheTierInterface {
}

const now = () => Math.ceil(Date.now() / 1_000);

export const oboMemoryCache = new OboMemoryCache();
Loading

0 comments on commit 0f999d4

Please sign in to comment.