diff --git a/packages/pds/src/account-manager/repo-rev-cache-redis.ts b/packages/pds/src/account-manager/repo-rev-cache-redis.ts deleted file mode 100644 index cffa84ee29b..00000000000 --- a/packages/pds/src/account-manager/repo-rev-cache-redis.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { SimpleStore } from '@atproto-labs/simple-store' -import Redis from 'ioredis' - -import { redisLogger } from '../logger' - -const key = (did: string) => `latestRev:${did}` - -export class RepoRevCacheRedis implements SimpleStore { - /** - * @param redis - Redis client - * @param maxTTL - Maximum age of a cached revision in milliseconds - */ - constructor( - protected readonly redis: Redis, - protected readonly maxTTL: number, - ) { - // Redis expects the expiration time in milliseconds - if (!Number.isFinite(this.maxTTL) || this.maxTTL <= 0) { - throw new TypeError('maxTTL must be a positive number') - } - } - - async get(did: string): Promise { - try { - const rev = await this.redis.get(key(did)) - return rev || undefined - } catch (err) { - redisLogger.error({ err, did }, 'error getting latestRev') - return undefined - } - } - - async set(did: string, rev: string): Promise { - try { - await this.redis.set(key(did), rev, 'PX', this.maxTTL) - } catch (err) { - redisLogger.error({ err, did, rev }, 'error setting latestRev') - } - } - - async del(did: string): Promise { - try { - await this.redis.del(key(did)) - } catch (err) { - redisLogger.error({ err, did }, 'error deleting latestRev') - } - } -} 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..3babb896540 --- /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 maxTTL - Maximum age of a cached revision in milliseconds + */ + constructor( + protected readonly redis: Redis, + protected readonly maxTTL: number, + protected readonly storeName: string, + ) { + // Redis expects the expiration time in milliseconds + if (!Number.isFinite(this.maxTTL) || this.maxTTL <= 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.maxTTL) + } 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 3a787288e90..f3eec019bee 100644 --- a/packages/pds/src/config/config.ts +++ b/packages/pds/src/config/config.ts @@ -235,10 +235,10 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { const crawlersCfg: ServerConfig['crawlers'] = env.crawlers ?? [] - const repoRevCacheCfg: ServerConfig['repoRevCache'] = - env.repoRevCacheMaxTTL != null && env.repoRevCacheMaxTTL > 0 - ? { maxTTL: env.repoRevCacheMaxTTL } - : null + const cachingCfg: ServerConfig['caching'] = { + repoRevMaxTTL: env.repoRevCacheMaxTTL, + preferencesMaxTTL: env.preferencesCacheMaxTTL, + } const fetchCfg: ServerConfig['fetch'] = { disableSsrfProtection: env.fetchDisableSsrfProtection ?? false, @@ -306,7 +306,7 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { redis: redisCfg, rateLimits: rateLimitsCfg, crawlers: crawlersCfg, - repoRevCache: repoRevCacheCfg, + caching: cachingCfg, fetch: fetchCfg, oauth: oauthCfg, } @@ -329,7 +329,7 @@ export type ServerConfig = { redis: RedisScratchConfig | null rateLimits: RateLimitsConfig crawlers: string[] - repoRevCache: RepoRevCacheConfig | null + caching: CachingConfig fetch: FetchConfig oauth: OAuthConfig } @@ -398,8 +398,9 @@ export type EntrywayConfig = { plcRotationKey: string } -export type RepoRevCacheConfig = { - maxTTL: number +export type CachingConfig = { + repoRevMaxTTL?: number + preferencesMaxTTL?: number } export type FetchConfig = { diff --git a/packages/pds/src/config/env.ts b/packages/pds/src/config/env.ts index a7ed63be82a..dde62598fdb 100644 --- a/packages/pds/src/config/env.ts +++ b/packages/pds/src/config/env.ts @@ -120,8 +120,9 @@ export const readEnv = (): ServerEnvironment => { // fetch fetchDisableSsrfProtection: envBool('PDS_DISABLE_SSRF_PROTECTION'), - // Repo revision cache + // caching repoRevCacheMaxTTL: envInt('PDS_REPO_REV_CACHE_MAX_TTL'), // milliseconds (use 0 to disable) + preferencesCacheMaxTTL: envInt('PDS_REPO_REV_CACHE_MAX_TTL'), // milliseconds (use 0 to disable) } } @@ -239,6 +240,7 @@ export type ServerEnvironment = { // fetch fetchDisableSsrfProtection?: boolean - // Repo revision cache + // Caching repoRevCacheMaxTTL?: number + preferencesCacheMaxTTL?: number } diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index 1d9cfe8135f..155e8706971 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -40,7 +40,7 @@ import { DiskBlobStore } from './disk-blobstore' 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 +56,7 @@ export type AppContextOptions = { backgroundQueue: BackgroundQueue redisScratch?: Redis repoRevCache?: SimpleStore + preferencesCache?: SimpleStore ratelimitCreator?: RateLimiterCreator crawlers: Crawlers appViewAgent?: AtpAgent @@ -83,6 +84,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 +111,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 @@ -231,8 +234,22 @@ export class AppContext { : null const repoRevCache = - redisScratch && cfg.repoRevCache - ? new RepoRevCacheRedis(redisScratch, cfg.repoRevCache.maxTTL) + redisScratch && cfg.caching.repoRevMaxTTL + ? new RedisSimpleStore( + redisScratch, + cfg.caching.repoRevMaxTTL, + 'latestRev', + ) + : // Note: Single instance PDS that have no redis could use a memory cache + undefined + + const preferencesCache = + redisScratch && cfg.caching.preferencesMaxTTL + ? new RedisSimpleStore( + redisScratch, + cfg.caching.preferencesMaxTTL, + 'preferences', + ) : // Note: Single instance PDS that have no redis could use a memory cache undefined @@ -340,6 +357,7 @@ export class AppContext { backgroundQueue, redisScratch, repoRevCache, + preferencesCache, ratelimitCreator, crawlers, appViewAgent,