diff --git a/packages/features/calendar-cache/calendar-cache.repository.ts b/packages/features/calendar-cache/calendar-cache.repository.ts index 6cd1100e5b3f72..738a46a06ae948 100644 --- a/packages/features/calendar-cache/calendar-cache.repository.ts +++ b/packages/features/calendar-cache/calendar-cache.repository.ts @@ -13,6 +13,7 @@ const log = logger.getSubLogger({ prefix: ["CalendarCacheRepository"] }); const MS_PER_DAY = 24 * 60 * 60 * 1000; const ONE_MONTH_IN_MS = 30 * MS_PER_DAY; const CACHING_TIME = ONE_MONTH_IN_MS; +const CACHE_CLEANUP_THRESHOLD = 7 * MS_PER_DAY; function parseKeyForCache(args: FreeBusyArgs): string { // Ensure that calendarIds are unique @@ -108,7 +109,7 @@ export class CalendarCacheRepository implements ICalendarCacheRepository { where: { userId, key, - expiresAt: { gte: new Date(Date.now()) }, + expiresAt: { gt: new Date(Date.now()) }, }, orderBy: { // In case of multiple entries for same key and userId, we prefer the one with highest expiry, which will be the most updated one @@ -123,7 +124,7 @@ export class CalendarCacheRepository implements ICalendarCacheRepository { credentialId, key, }, - expiresAt: { gte: new Date(Date.now()) }, + expiresAt: { gt: new Date(Date.now()) }, }, }); } diff --git a/packages/features/calendar-cache/lib/cacheCleanup.ts b/packages/features/calendar-cache/lib/cacheCleanup.ts new file mode 100644 index 00000000000000..5aaac6ea95bc9a --- /dev/null +++ b/packages/features/calendar-cache/lib/cacheCleanup.ts @@ -0,0 +1,99 @@ +import logger from "@calcom/lib/logger"; +import prisma from "@calcom/prisma"; + +const log = logger.getSubLogger({ prefix: ["CacheCleanup"] }); + +const MS_PER_DAY = 24 * 60 * 60 * 1000; +const CLEANUP_THRESHOLD_DAYS = 7; + +/** + * Removes expired cache entries older than the threshold + * @param thresholdDays - Number of days after expiry to keep entries + * @returns Number of deleted entries + */ +export async function cleanupExpiredCache(thresholdDays: number = CLEANUP_THRESHOLD_DAYS): Promise { + const thresholdDate = new Date(Date.now() - thresholdDays * MS_PER_DAY); + + try { + const result = await prisma.calendarCache.deleteMany({ + where: { + expiresAt: { + lt: thresholdDate, + }, + }, + }); + + log.info(`Cleaned up ${result.count} expired cache entries`); + return result.count; + } catch (error) { + log.error("Failed to cleanup expired cache", error); + throw error; + } +} + +/** + * Removes duplicate cache entries for the same credential and key + * Keeps only the most recent entry + */ +export async function deduplicateCacheEntries(): Promise { + try { + const duplicates = await prisma.calendarCache.groupBy({ + by: ["credentialId", "key"], + having: { + credentialId: { + _count: { + gt: 1, + }, + }, + }, + }); + + let deletedCount = 0; + + for (const duplicate of duplicates) { + const entries = await prisma.calendarCache.findMany({ + where: { + credentialId: duplicate.credentialId, + key: duplicate.key, + }, + orderBy: { + expiresAt: "desc", + }, + }); + + if (entries.length > 1) { + const idsToDelete = entries.slice(1).map((entry) => entry.id); + + const result = await prisma.calendarCache.deleteMany({ + where: { + id: { + in: idsToDelete, + }, + }, + }); + + deletedCount += result.count; + } + } + + log.info(`Deduplicated ${deletedCount} cache entries`); + return deletedCount; + } catch (error) { + log.error("Failed to deduplicate cache entries", error); + throw error; + } +} + +/** + * Performs comprehensive cache maintenance + */ +export async function performCacheMaintenance(): Promise<{ cleaned: number; deduplicated: number }> { + log.info("Starting cache maintenance"); + + const cleaned = await cleanupExpiredCache(); + const deduplicated = await deduplicateCacheEntries(); + + log.info("Cache maintenance completed", { cleaned, deduplicated }); + + return { cleaned, deduplicated }; +} diff --git a/packages/features/calendar-cache/lib/getShouldServeCache.ts b/packages/features/calendar-cache/lib/getShouldServeCache.ts index c98400863cda60..b69d88eed889e7 100644 --- a/packages/features/calendar-cache/lib/getShouldServeCache.ts +++ b/packages/features/calendar-cache/lib/getShouldServeCache.ts @@ -10,7 +10,7 @@ export class CacheService { async getShouldServeCache(shouldServeCache?: boolean | undefined, teamId?: number) { if (typeof shouldServeCache === "boolean") return shouldServeCache; - if (!teamId) return false; + if (!teamId) return true; return await this.dependencies.featuresRepository.checkIfTeamHasFeature(teamId, CalendarSubscriptionService.CALENDAR_SUBSCRIPTION_CACHE_FEATURE); } }