From a64fc62edc94f57fe3b35e82410e9ec8ad5dda9a Mon Sep 17 00:00:00 2001 From: Varun Dhananjaya Date: Tue, 8 Oct 2024 16:16:10 -0400 Subject: [PATCH] [keyserver][lib] replace neynar client cache with redis cache Summary: we're using redis because we have multiple keyserver nodes that will need access to this data in order to minimize the number of neynar client calls instead of caching led channels by the lead user, we cache all channels we get from the fetchFollowed neynar client method by channel ID. we do this in the background since we don't need to block on this operation. Depends on D13636 Test Plan: tested this in a few different ways: 1. commented out the background operation that sets data in the cache. registered a user with j4ck.eth's fid and confirmed that user was made lead of `bromero` community, which they lead on farcaster 2. uncommented the background operation. deleted and registered j4ck.eth again and confirmed that the information was fetched from the cache using redis-cli `monitor`. once again, user was made lead of `bromero` community Reviewers: ashoat Reviewed By: ashoat Subscribers: tomek Differential Revision: https://phab.comm.dev/D13637 --- keyserver/src/updaters/thread-updaters.js | 49 +++++++++++++++++++---- lib/utils/neynar-client.js | 36 +---------------- 2 files changed, 43 insertions(+), 42 deletions(-) diff --git a/keyserver/src/updaters/thread-updaters.js b/keyserver/src/updaters/thread-updaters.js index a73dc7b086..89a93948a1 100644 --- a/keyserver/src/updaters/thread-updaters.js +++ b/keyserver/src/updaters/thread-updaters.js @@ -69,6 +69,7 @@ import { import type { Viewer } from '../session/viewer.js'; import { neynarClient } from '../utils/fc-cache.js'; import { findUserIdentities } from '../utils/identity-utils.js'; +import { redisCache } from '../utils/redis-cache.js'; import RelationshipChangeset from '../utils/relationship-changeset.js'; type UpdateRoleOptions = { @@ -942,13 +943,11 @@ async function fetchUserRoleForThread( return null; } - const ledChannels = - await neynarClient?.fetchLedFarcasterChannels(farcasterID); - - if ( - !ledChannels || - !ledChannels.some(channel => channel.id === communityFarcasterChannelTag) - ) { + const leadsChannel = await userLeadsChannel( + communityFarcasterChannelTag, + farcasterID, + ); + if (!leadsChannel) { return null; } @@ -962,6 +961,42 @@ async function fetchUserRoleForThread( return null; } +async function userLeadsChannel( + communityFarcasterChannelTag: string, + farcasterID: string, +) { + const cachedChannelInfo = await redisCache.getChannelInfo( + communityFarcasterChannelTag, + ); + if (cachedChannelInfo) { + return cachedChannelInfo.lead.fid === parseInt(farcasterID); + } + + // In the background, we fetch and cache followed channels + ignorePromiseRejections( + (async () => { + const followedChannels = + await neynarClient?.fetchFollowedFarcasterChannels(farcasterID); + if (followedChannels) { + await Promise.allSettled( + followedChannels.map(followedChannel => + redisCache.setChannelInfo(followedChannel.id, followedChannel), + ), + ); + } + })(), + ); + + const channelInfo = await neynarClient?.fetchFarcasterChannelByName( + communityFarcasterChannelTag, + ); + if (channelInfo) { + return channelInfo.lead.fid === parseInt(farcasterID); + } + + return false; +} + async function toggleMessagePinForThread( viewer: Viewer, request: ToggleMessagePinRequest, diff --git a/lib/utils/neynar-client.js b/lib/utils/neynar-client.js index 5ab7ce8acc..3fde9ee807 100644 --- a/lib/utils/neynar-client.js +++ b/lib/utils/neynar-client.js @@ -63,16 +63,9 @@ const fetchChannelsLimit = 50; class NeynarClient { apiKey: string; - ledChannelsCache: Map< - string, - { channels: NeynarChannel[], timestamp: number }, - >; - cacheTTL: number; - constructor(apiKey: string, cacheTTL: number = 60000) { + constructor(apiKey: string) { this.apiKey = apiKey; - this.ledChannelsCache = new Map(); - this.cacheTTL = cacheTTL; // Default TTL of 60 seconds } // We're using the term "friend" for a bidirectional follow @@ -160,33 +153,6 @@ class NeynarClient { return this.fetchFollowedFarcasterChannelsWithFilter(fid, () => true); } - cleanExpiredCacheEntries() { - const now = Date.now(); - for (const [fid, { timestamp }] of this.ledChannelsCache.entries()) { - if (now - timestamp >= this.cacheTTL) { - this.ledChannelsCache.delete(fid); - } - } - } - - async fetchLedFarcasterChannels(fid: string): Promise { - this.cleanExpiredCacheEntries(); - - const cachedEntry = this.ledChannelsCache.get(fid); - if (cachedEntry) { - return cachedEntry.channels; - } - - const channels = await this.fetchFollowedFarcasterChannelsWithFilter( - fid, - channel => channel.lead.fid === parseInt(fid), - ); - - this.ledChannelsCache.set(fid, { channels, timestamp: Date.now() }); - - return channels; - } - async fetchFarcasterChannelByName( channelName: string, ): Promise {