From e05ff942b10711043a2f1a1874c84ce20c28457e Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Wed, 14 Aug 2024 10:09:37 +0800 Subject: [PATCH] cache features for 600 seconds --- packages/api/src/resolvers/features/index.ts | 11 ++++- .../api/src/resolvers/function_resolvers.ts | 42 +++++++++++-------- packages/api/src/resolvers/user/index.ts | 15 ++++++- packages/api/src/services/features.ts | 24 +++++++++++ packages/api/src/services/library_item.ts | 31 +++++++++++++- packages/api/src/services/user.ts | 21 ++++++++++ 6 files changed, 122 insertions(+), 22 deletions(-) diff --git a/packages/api/src/resolvers/features/index.ts b/packages/api/src/resolvers/features/index.ts index 6bb12c2cb..21999e590 100644 --- a/packages/api/src/resolvers/features/index.ts +++ b/packages/api/src/resolvers/features/index.ts @@ -6,8 +6,10 @@ import { } from '../../generated/graphql' import { getFeatureName, + getFeaturesCache, isOptInFeatureErrorCode, optInFeature, + setFeaturesCache, signFeatureToken, } from '../../services/features' import { authorized } from '../../utils/gql-utils' @@ -34,7 +36,8 @@ export const optInFeatureResolver = authorized< } } - const optedInFeature = await optInFeature(featureName, claims.uid) + const userId = claims.uid + const optedInFeature = await optInFeature(featureName, userId) if (isOptInFeatureErrorCode(optedInFeature)) { return { errorCodes: [optedInFeature], @@ -42,7 +45,11 @@ export const optInFeatureResolver = authorized< } log.info('Opted in to a feature', optedInFeature) - const token = signFeatureToken(optedInFeature, claims.uid) + const cachedFeatures = (await getFeaturesCache(userId)) || [] + const updatedFeatures = [...cachedFeatures, optedInFeature] + await setFeaturesCache(userId, updatedFeatures) + + const token = signFeatureToken(optedInFeature, userId) return { feature: { diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index 485187ecf..3412d9187 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -28,17 +28,19 @@ import { PageType, User, } from '../generated/graphql' -import { redisDataSource } from '../redis_data_source' import { getAISummary } from '../services/ai-summaries' -import { findUserFeatures } from '../services/features' -import { countLibraryItems } from '../services/library_item' -import { Merge } from '../util' import { - isBase64Image, - stringToHash, - validatedDate, - wordsCount, -} from '../utils/helpers' + findUserFeatures, + getFeaturesCache, + setFeaturesCache, +} from '../services/features' +import { + countLibraryItems, + getCachedTotalCount, + setCachedTotalCount, +} from '../services/library_item' +import { Merge } from '../util' +import { isBase64Image, validatedDate, wordsCount } from '../utils/helpers' import { createImageProxyUrl } from '../utils/imageproxy' import { contentConverter } from '../utils/parser' import { @@ -406,7 +408,16 @@ export const functionResolvers = { return undefined } - return findUserFeatures(ctx.claims.uid) + const userId = ctx.claims.uid + const cachedFeatures = await getFeaturesCache(userId) + if (cachedFeatures) { + return cachedFeatures + } + + const features = await findUserFeatures(userId) + await setFeaturesCache(userId, features) + + return features }, picture: (user: UserEntity) => user.profile.pictureUrl, // not implemented yet @@ -607,17 +618,12 @@ export const functionResolvers = { if (pageInfo.searchLibraryItemArgs && ctx.claims) { const args = pageInfo.searchLibraryItemArgs const userId = ctx.claims.uid - // hash the arguments to create a unique cache key - const argsHash = stringToHash(JSON.stringify(args)) - const cacheKey = `countLibraryItems:${userId}:${argsHash}` - const cachedCount = await redisDataSource.redisClient?.get(cacheKey) - if (cachedCount) { - return parseInt(cachedCount, 10) - } + const cachedCount = await getCachedTotalCount(userId, args) + if (cachedCount) return cachedCount const count = await countLibraryItems(args, userId) + await setCachedTotalCount(userId, args, count) - await redisDataSource.redisClient?.set(cacheKey, count, 'EX', 600) return count } diff --git a/packages/api/src/resolvers/user/index.ts b/packages/api/src/resolvers/user/index.ts index 9c496df8c..4dc6b5921 100644 --- a/packages/api/src/resolvers/user/index.ts +++ b/packages/api/src/resolvers/user/index.ts @@ -41,7 +41,7 @@ import { import { userRepository } from '../../repository/user' import { createUser } from '../../services/create_user' import { sendAccountChangeEmail } from '../../services/send_emails' -import { softDeleteUser } from '../../services/user' +import { cacheUser, getCachedUser, softDeleteUser } from '../../services/user' import { Merge } from '../../util' import { authorized } from '../../utils/gql-utils' import { validateUsername } from '../../utils/usernamePolicy' @@ -254,11 +254,19 @@ export const getMeUserResolver: ResolverFn< return undefined } + const userId = claims.uid + const cachedUser = await getCachedUser(userId) + if (cachedUser) { + return cachedUser + } + const user = await userRepository.findById(claims.uid) if (!user) { return undefined } + await cacheUser(user) + return user } catch (error) { return undefined @@ -355,6 +363,11 @@ export const updateEmailResolver = authorized< }) ) + await cacheUser({ + ...user, + email, + }) + return { email } } diff --git a/packages/api/src/services/features.ts b/packages/api/src/services/features.ts index 5e410842f..877701629 100644 --- a/packages/api/src/services/features.ts +++ b/packages/api/src/services/features.ts @@ -6,6 +6,7 @@ import { LibraryItem } from '../entity/library_item' import { Subscription, SubscriptionStatus } from '../entity/subscription' import { env } from '../env' import { OptInFeatureErrorCode } from '../generated/graphql' +import { redisDataSource } from '../redis_data_source' import { authTrx, getRepository } from '../repository' import { logger } from '../utils/logger' @@ -201,3 +202,26 @@ export const userDigestEligible = async (uid: string): Promise => { return subscriptionsCount >= 2 && libraryItemsCount >= 10 } + +const featuresCacheKey = (userId: string) => `features:${userId}` + +export const getFeaturesCache = async (userId: string) => { + const cachedFeatures = await redisDataSource.redisClient?.get( + featuresCacheKey(userId) + ) + if (!cachedFeatures) { + return undefined + } + + return JSON.parse(cachedFeatures) as Feature[] +} + +export const setFeaturesCache = async (userId: string, features: Feature[]) => { + const value = JSON.stringify(features) + return redisDataSource.redisClient?.set( + featuresCacheKey(userId), + value, + 'EX', + 600 + ) +} diff --git a/packages/api/src/services/library_item.ts b/packages/api/src/services/library_item.ts index 5fa422103..84886511c 100644 --- a/packages/api/src/services/library_item.ts +++ b/packages/api/src/services/library_item.ts @@ -30,7 +30,11 @@ import { } from '../repository' import { libraryItemRepository } from '../repository/library_item' import { Merge, PickTuple } from '../util' -import { deepDelete, setRecentlySavedItemInRedis } from '../utils/helpers' +import { + deepDelete, + setRecentlySavedItemInRedis, + stringToHash, +} from '../utils/helpers' import { logger } from '../utils/logger' import { parseSearchQuery } from '../utils/search' import { HighlightEvent } from './highlights' @@ -1711,3 +1715,28 @@ export const filterItemEvents = ( throw new Error('Unexpected state.') } + +const totalCountCacheKey = (userId: string, args: SearchArgs) => { + return `cache:library_items_count:${userId}:${stringToHash( + JSON.stringify(args) + )}` +} + +export const getCachedTotalCount = async (userId: string, args: SearchArgs) => { + const cacheKey = totalCountCacheKey(userId, args) + const cachedCount = await redisDataSource.redisClient?.get(cacheKey) + if (!cachedCount) { + return undefined + } + + return parseInt(cachedCount, 10) +} + +export const setCachedTotalCount = async ( + userId: string, + args: SearchArgs, + count: number +) => { + const cacheKey = totalCountCacheKey(userId, args) + await redisDataSource.redisClient?.set(cacheKey, count, 'EX', 600) +} diff --git a/packages/api/src/services/user.ts b/packages/api/src/services/user.ts index 6b81a7352..3f7ed7d6c 100644 --- a/packages/api/src/services/user.ts +++ b/packages/api/src/services/user.ts @@ -2,6 +2,7 @@ import { Notification } from 'firebase-admin/messaging' import { DeepPartial, FindOptionsWhere, In } from 'typeorm' import { Profile } from '../entity/profile' import { StatusType, User } from '../entity/user' +import { redisDataSource } from '../redis_data_source' import { authTrx, getRepository, queryBuilderToRawSql } from '../repository' import { userRepository } from '../repository/user' import { SetClaimsRole } from '../utils/dictionary' @@ -156,3 +157,23 @@ export const findUserAndPersonalization = async (id: string) => { } ) } + +const userCacheKey = (id: string) => `cache:user:${id}` + +export const getCachedUser = async (id: string) => { + const user = await redisDataSource.redisClient?.get(userCacheKey(id)) + if (!user) { + return undefined + } + + return JSON.parse(user) as User +} + +export const cacheUser = async (user: User) => { + await redisDataSource.redisClient?.set( + userCacheKey(user.id), + JSON.stringify(user), + 'EX', + 600 + ) +}