Skip to content

Commit

Permalink
Migrate from Redis to Valkey
Browse files Browse the repository at this point in the history
  • Loading branch information
cskrov committed Feb 5, 2025
1 parent 08a29e3 commit 5027fc8
Show file tree
Hide file tree
Showing 11 changed files with 99 additions and 132 deletions.
3 changes: 1 addition & 2 deletions nais/nais.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ spec:
- application: kabal-e2e-tests
outbound:
rules:
- application: klage-redis-fe
- application: kabal-api
- application: kabal-smart-editor-api
- application: kabal-json-to-pdf
Expand All @@ -62,7 +61,7 @@ spec:
envFrom:
- secret: kabal-session-key
- secret: slack-url
redis:
valkey:
- instance: obo-cache
access: readwrite
- instance: hocuspocus
Expand Down
30 changes: 0 additions & 30 deletions nais/redis.yaml

This file was deleted.

2 changes: 0 additions & 2 deletions server/.env

This file was deleted.

14 changes: 7 additions & 7 deletions server/src/auth/cache/cache-gauge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,22 @@ import { Counter, Gauge, Histogram } from 'prom-client';

const labelNames = ['hit'] as const;

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".',
export const persistentCacheGauge = new Counter({
name: 'obo_persistent_cache',
help: 'Number of requests to the persistent OBO cache. "hit" is the type of hit: "miss", "invalid", "hit" or "expired".',
labelNames,
registers: [proxyRegister],
});

export const redisCacheSizeGauge = new Gauge({
name: 'obo_redis_cache_size',
help: 'Number of OBO tokens in the Redis cache.',
export const persistentCacheSizeGauge = new Gauge({
name: 'obo_persistent_cache_size',
help: 'Number of OBO tokens in the persistent 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".',
help: 'Number of requests to the OBO cache. "hit" is the type of hit: "miss", "persistent", "hit", or "expired".',
labelNames,
registers: [proxyRegister],
});
Expand Down
36 changes: 18 additions & 18 deletions server/src/auth/cache/cache.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import { OboMemoryCache } from '@app/auth/cache/memory-cache';
import { OboRedisCache } from '@app/auth/cache/redis-cache';
import { OboPersistentCache } from '@app/auth/cache/persistent-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');
const VALKEY_URI = optionalEnvString('VALKEY_URI_OBO_CACHE');
const VALKEY_USERNAME = optionalEnvString('VALKEY_USERNAME_OBO_CACHE');
const VALKEY_PASSWORD = optionalEnvString('VALKEY_PASSWORD_OBO_CACHE');

class OboTieredCache {
#oboRedisCache: OboRedisCache;
#oboPersistentCache: OboPersistentCache;
#oboMemoryCache: OboMemoryCache | null = null;
#isReady = false;

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

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

Expand All @@ -36,12 +36,12 @@ class OboTieredCache {
return memoryHit.token;
}

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

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

return redisHit.token;
return persistentHit.token;
}

return null;
Expand All @@ -59,11 +59,11 @@ class OboTieredCache {

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

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

Expand All @@ -89,8 +89,8 @@ class OboSimpleCache {
}
}

const hasRedis = REDIS_URI !== undefined && REDIS_USERNAME !== undefined && REDIS_PASSWORD !== undefined;
const hasValkey = VALKEY_URI !== undefined && VALKEY_USERNAME !== undefined && VALKEY_PASSWORD !== undefined;

export const oboCache = hasRedis ? new OboTieredCache(REDIS_URI, REDIS_USERNAME, REDIS_PASSWORD) : new OboSimpleCache();
export const oboCache = hasValkey ? new OboTieredCache(VALKEY_URI, VALKEY_USERNAME, VALKEY_PASSWORD) : new OboSimpleCache();

export const getCacheKey = (navIdent: string, appName: string) => `${navIdent}-${appName}`;
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import { memoryCacheGauge, redisCacheGauge, redisCacheSizeGauge } from '@app/auth/cache/cache-gauge';
import { memoryCacheGauge, persistentCacheGauge, persistentCacheSizeGauge } from '@app/auth/cache/cache-gauge';
import type { TokenMessage } from '@app/auth/cache/types';
import { getLogger } from '@app/logger';
import { type RedisClientType, createClient } from 'redis';
import { type RedisClientType as ValkeyClientType, createClient } from 'redis';

const log = getLogger('obo-redis-cache');
const log = getLogger('obo-persistent-cache');

export type TokenListener = (message: TokenMessage) => void;

const TOKEN_CHANNEL = 'obo-token';

export class OboRedisCache {
#client: RedisClientType;
#subscribeClient: RedisClientType;
export class OboPersistentCache {
#client: ValkeyClientType;
#subscribeClient: ValkeyClientType;

#listeners: TokenListener[] = [];

constructor(url: string, username: string, password: string) {
this.#client = createClient({ url, username, password, pingInterval: 3_000 });
this.#client.on('error', (error) => log.error({ msg: 'Redis Data Client Error', error }));
this.#client.on('error', (error) => log.error({ msg: 'Valkey Data Client Error', error }));

this.#subscribeClient = this.#client.duplicate();
this.#subscribeClient.on('error', (error) => log.error({ msg: 'Redis Subscribe Client Error', error }));
this.#subscribeClient.on('error', (error) => log.error({ msg: 'Valkey Subscribe Client Error', error }));
}

public async init() {
Expand Down Expand Up @@ -51,7 +51,7 @@ export class OboRedisCache {

#refreshCacheSizeMetric = async () => {
const count = await this.#client.dbSize();
redisCacheSizeGauge.set(count);
persistentCacheSizeGauge.set(count);
};

public async getAll(): Promise<TokenMessage[]> {
Expand Down Expand Up @@ -91,32 +91,32 @@ export class OboRedisCache {
* ttl() gets remaining time to live in seconds.
* Returns -2 if the key does not exist.
* Returns -1 if the key exists but has no associated expire.
* @see https://redis.io/docs/latest/commands/ttl/
* @see https://valkey.io/commands/ttl/
*/
const [token, ttl] = await Promise.all([this.#client.get(key), this.#client.ttl(key)]);

if (token === null || ttl === -2) {
redisCacheGauge.inc({ hit: 'miss' });
persistentCacheGauge.inc({ hit: 'miss' });

return null;
}

if (ttl === -1) {
redisCacheGauge.inc({ hit: 'invalid' });
persistentCacheGauge.inc({ hit: 'invalid' });
this.#client.del(key);
this.#refreshCacheSizeMetric();

return null;
}

if (ttl === 0) {
redisCacheGauge.inc({ hit: 'expired' });
persistentCacheGauge.inc({ hit: 'expired' });

return null;
}

redisCacheGauge.inc({ hit: 'hit' });
memoryCacheGauge.inc({ hit: 'redis' });
persistentCacheGauge.inc({ hit: 'hit' });
memoryCacheGauge.inc({ hit: 'persistent' });

return { token, expiresAt: now() + ttl };
}
Expand Down
4 changes: 2 additions & 2 deletions server/src/plugins/crdt/collaboration-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { getDocument } from '@app/plugins/crdt/api/get-document';
import { setDocument } from '@app/plugins/crdt/api/set-document';
import { type ConnectionContext, isConnectionContext } from '@app/plugins/crdt/context';
import { hasOwn, isObject } from '@app/plugins/crdt/functions';
import { getRedisExtension } from '@app/plugins/crdt/redis';
import { getValkeyExtension } from '@app/plugins/crdt/valkey';
import type { CloseEvent } from '@hocuspocus/common';
import { Server } from '@hocuspocus/server';
import { applyUpdateV2 } from 'yjs';
Expand Down Expand Up @@ -231,7 +231,7 @@ export const collaborationServer = Server.configure({
logContext('Saved document to database', context, 'debug');
},

extensions: isDeployed ? [getRedisExtension()].filter(isNotNull) : [],
extensions: isDeployed ? [getValkeyExtension()].filter(isNotNull) : [],
});

const getCloseEvent = (reason: string, code: number): CloseEvent => ({ reason, code });
Expand Down
26 changes: 0 additions & 26 deletions server/src/plugins/crdt/redis.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
export interface RedisOptions {
export interface ValkeyOptions {
/**
* The URL of the Redis server.
* The URL of the Valkey server.
* @example 'redis://localhost:6379'
*/
readonly url: string;
/**
* The username for the Redis server.
* The username for the Valkey server.
* @example 'username'
*/
readonly username?: string;
/**
* The password for the Redis server.
* The password for the Valkey server.
* @example 'password'
*/
readonly password?: string;
Expand Down
Loading

0 comments on commit 5027fc8

Please sign in to comment.