From 9f24517d70a4fda583d1c9e8093c4e4ff999b3a6 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 5 Sep 2024 12:44:34 -0500 Subject: [PATCH 1/2] first pass --- .../src/api/app/bsky/actor/getPreferences.ts | 10 ++++ .../src/api/app/bsky/actor/putPreferences.ts | 6 +++ .../pds/src/caching/redis-simple-store.ts | 51 +++++++++++++++++++ packages/pds/src/config/config.ts | 13 +++++ packages/pds/src/config/env.ts | 6 +++ packages/pds/src/context.ts | 15 ++++++ 6 files changed, 101 insertions(+) create mode 100644 packages/pds/src/caching/redis-simple-store.ts diff --git a/packages/pds/src/api/app/bsky/actor/getPreferences.ts b/packages/pds/src/api/app/bsky/actor/getPreferences.ts index 310495e3282..7af7d3ff5f6 100644 --- a/packages/pds/src/api/app/bsky/actor/getPreferences.ts +++ b/packages/pds/src/api/app/bsky/actor/getPreferences.ts @@ -7,6 +7,16 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.authVerifier.accessStandard(), handler: async ({ auth }) => { const requester = auth.credentials.did + + const cachedPrefs = await ctx.preferencesCache?.get(requester) + if (cachedPrefs) { + const preferences = JSON.parse(cachedPrefs) + return { + encoding: 'application/json', + body: { preferences }, + } + } + const preferences = await ctx.actorStore.read(requester, (store) => store.pref.getPreferences('app.bsky', auth.credentials.scope), ) diff --git a/packages/pds/src/api/app/bsky/actor/putPreferences.ts b/packages/pds/src/api/app/bsky/actor/putPreferences.ts index 5006c2150a5..161d8ce201b 100644 --- a/packages/pds/src/api/app/bsky/actor/putPreferences.ts +++ b/packages/pds/src/api/app/bsky/actor/putPreferences.ts @@ -18,6 +18,7 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError('Preference is missing a $type') } } + await ctx.actorStore.transact(requester, async (actorTxn) => { await actorTxn.pref.putPreferences( checkedPreferences, @@ -25,6 +26,11 @@ export default function (server: Server, ctx: AppContext) { auth.credentials.scope, ) }) + + await ctx.preferencesCache?.set( + requester, + JSON.stringify(checkedPreferences), + ) }, }) } diff --git a/packages/pds/src/caching/redis-simple-store.ts b/packages/pds/src/caching/redis-simple-store.ts new file mode 100644 index 00000000000..202a199f16e --- /dev/null +++ b/packages/pds/src/caching/redis-simple-store.ts @@ -0,0 +1,51 @@ +import { SimpleStore } from '@atproto-labs/simple-store' +import Redis from 'ioredis' + +import { redisLogger } from '../logger' + +export class RedisSimpleStore implements SimpleStore { + /** + * @param redis - Redis client + * @param maxAge - Maximum age of a cached revision in milliseconds + */ + constructor( + protected readonly redis: Redis, + protected readonly maxAge: number, + protected readonly storeName: string, + ) { + // Redis expects the expiration time in seconds + if (!Number.isFinite(this.maxAge) || this.maxAge <= 0) { + throw new TypeError('maxAge must be a positive number') + } + } + + protected key(did: string): string { + return `${this.storeName}:${did}` + } + + async get(did: string): Promise { + try { + const value = await this.redis.get(this.key(did)) + return value || undefined + } catch (err) { + redisLogger.error({ err, did }, `error getting ${this.storeName}`) + return undefined + } + } + + async set(did: string, value: string): Promise { + try { + await this.redis.set(this.key(did), value, 'PX', this.maxAge) + } catch (err) { + redisLogger.error({ err, did, value }, `error setting ${this.storeName}`) + } + } + + async del(did: string): Promise { + try { + await this.redis.del(this.key(did)) + } catch (err) { + redisLogger.error({ err, did }, `error deleting ${this.storeName}`) + } + } +} diff --git a/packages/pds/src/config/config.ts b/packages/pds/src/config/config.ts index efb4f81b6aa..41092909be7 100644 --- a/packages/pds/src/config/config.ts +++ b/packages/pds/src/config/config.ts @@ -242,6 +242,13 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { } : null + const preferencesCacheCfg: ServerConfig['preferencesCache'] = + env.preferencesCacheMaxTTL != null && env.preferencesCacheMaxTTL > 0 + ? { + maxAge: env.preferencesCacheMaxTTL * SECOND, + } + : null + const fetchCfg: ServerConfig['fetch'] = { disableSsrfProtection: env.fetchDisableSsrfProtection ?? false, } @@ -309,6 +316,7 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { rateLimits: rateLimitsCfg, crawlers: crawlersCfg, repoRevCache: repoRevCacheCfg, + preferencesCache: preferencesCacheCfg, fetch: fetchCfg, oauth: oauthCfg, } @@ -332,6 +340,7 @@ export type ServerConfig = { rateLimits: RateLimitsConfig crawlers: string[] repoRevCache: RepoRevCacheConfig | null + preferencesCache: PreferencesCacheConfig | null fetch: FetchConfig oauth: OAuthConfig } @@ -404,6 +413,10 @@ export type RepoRevCacheConfig = { maxAge: number } +export type PreferencesCacheConfig = { + maxAge: number +} + export type FetchConfig = { disableSsrfProtection: boolean } diff --git a/packages/pds/src/config/env.ts b/packages/pds/src/config/env.ts index 6061e721af0..5feff5868f1 100644 --- a/packages/pds/src/config/env.ts +++ b/packages/pds/src/config/env.ts @@ -122,6 +122,9 @@ export const readEnv = (): ServerEnvironment => { // Repo revision cache repoRevCacheMaxAge: envInt('PDS_REPO_REV_CACHE_MAX_AGE'), // Seconds (use 0 to disable) + + // Preferences cache + preferencesCacheMaxTTL: envInt('PDS_REPO_REV_CACHE_MAX_TTL'), // Seconds (use 0 to disable) } } @@ -241,4 +244,7 @@ export type ServerEnvironment = { // Repo revision cache repoRevCacheMaxAge?: number + + // Preferences cache + preferencesCacheMaxTTL?: number } diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index 9f44593977a..6e69d79e785 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -41,6 +41,7 @@ import { getRedisClient } from './redis' import { ActorStore } from './actor-store' import { LocalViewer, LocalViewerCreator } from './read-after-write/viewer' import { RepoRevCacheRedis } from './account-manager/repo-rev-cache-redis' +import { RedisSimpleStore } from './caching/redis-simple-store' export type AppContextOptions = { actorStore: ActorStore @@ -56,6 +57,7 @@ export type AppContextOptions = { backgroundQueue: BackgroundQueue redisScratch?: Redis repoRevCache?: SimpleStore + preferencesCache?: SimpleStore ratelimitCreator?: RateLimiterCreator crawlers: Crawlers appViewAgent?: AtpAgent @@ -83,6 +85,7 @@ export class AppContext { public backgroundQueue: BackgroundQueue public redisScratch?: Redis public repoRevCache?: SimpleStore + public preferencesCache?: SimpleStore public ratelimitCreator?: RateLimiterCreator public crawlers: Crawlers public appViewAgent: AtpAgent | undefined @@ -109,6 +112,7 @@ export class AppContext { this.backgroundQueue = opts.backgroundQueue this.redisScratch = opts.redisScratch this.repoRevCache = opts.repoRevCache + this.preferencesCache = opts.preferencesCache this.ratelimitCreator = opts.ratelimitCreator this.crawlers = opts.crawlers this.appViewAgent = opts.appViewAgent @@ -236,6 +240,16 @@ export class AppContext { : // Note: Single instance PDS that have no redis could use a memory cache undefined + const preferencesCache = + redisScratch && cfg.preferencesCache + ? new RedisSimpleStore( + redisScratch, + cfg.preferencesCache.maxAge, + 'preferences', + ) + : // Note: Single instance PDS that have no redis could use a memory cache + undefined + const accountManager = new AccountManager( backgroundQueue, cfg.db.accountDbLoc, @@ -340,6 +354,7 @@ export class AppContext { backgroundQueue, redisScratch, repoRevCache, + preferencesCache, ratelimitCreator, crawlers, appViewAgent, From b4bd0c55091aa68df226344df4df438d26da7e41 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 5 Sep 2024 12:55:50 -0500 Subject: [PATCH 2/2] tidy --- packages/pds/src/caching/redis-simple-store.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/pds/src/caching/redis-simple-store.ts b/packages/pds/src/caching/redis-simple-store.ts index 202a199f16e..3babb896540 100644 --- a/packages/pds/src/caching/redis-simple-store.ts +++ b/packages/pds/src/caching/redis-simple-store.ts @@ -6,15 +6,15 @@ import { redisLogger } from '../logger' export class RedisSimpleStore implements SimpleStore { /** * @param redis - Redis client - * @param maxAge - Maximum age of a cached revision in milliseconds + * @param maxTTL - Maximum age of a cached revision in milliseconds */ constructor( protected readonly redis: Redis, - protected readonly maxAge: number, + protected readonly maxTTL: number, protected readonly storeName: string, ) { - // Redis expects the expiration time in seconds - if (!Number.isFinite(this.maxAge) || this.maxAge <= 0) { + // Redis expects the expiration time in milliseconds + if (!Number.isFinite(this.maxTTL) || this.maxTTL <= 0) { throw new TypeError('maxAge must be a positive number') } } @@ -35,7 +35,7 @@ export class RedisSimpleStore implements SimpleStore { async set(did: string, value: string): Promise { try { - await this.redis.set(this.key(did), value, 'PX', this.maxAge) + await this.redis.set(this.key(did), value, 'PX', this.maxTTL) } catch (err) { redisLogger.error({ err, did, value }, `error setting ${this.storeName}`) }