Skip to content

Commit 2c8ff89

Browse files
perf: Calendar Cache Improvements (calcom#25502)
* wip * Filter only selectedCalendars where ff is enabled * test: fix and add tests for calendar subscription cache feature Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * test: fix SelectedCalendarRepository tests for new teamIds parameter Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 3bf2a8f commit 2c8ff89

7 files changed

Lines changed: 158 additions & 34 deletions

File tree

packages/features/calendar-subscription/lib/CalendarSubscriptionService.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type { CalendarSyncService } from "@calcom/features/calendar-subscription
1212
import type { FeaturesRepository } from "@calcom/features/flags/features.repository";
1313
import logger from "@calcom/lib/logger";
1414
import type { ISelectedCalendarRepository } from "@calcom/lib/server/repository/SelectedCalendarRepository.interface";
15-
import { SelectedCalendar } from "@calcom/prisma/client";
15+
import type { SelectedCalendar } from "@calcom/prisma/client";
1616

1717
const log = logger.getSubLogger({ prefix: ["CalendarSubscriptionService"] });
1818

@@ -204,9 +204,14 @@ export class CalendarSubscriptionService {
204204
* Subscribe periodically to new calendars
205205
*/
206206
async checkForNewSubscriptions() {
207+
const teamIds = await this.deps.featuresRepository.getTeamsWithFeatureEnabled(
208+
CalendarSubscriptionService.CALENDAR_SUBSCRIPTION_CACHE_FEATURE
209+
);
210+
207211
const rows = await this.deps.selectedCalendarRepository.findNextSubscriptionBatch({
208212
take: 100,
209213
integrations: this.deps.adapterFactory.getProviders(),
214+
teamIds,
210215
});
211216
log.debug("checkForNewSubscriptions", { count: rows.length });
212217
await Promise.allSettled(rows.map(({ id }) => this.subscribe(id)));

packages/features/calendar-subscription/lib/__tests__/CalendarSubscriptionService.test.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ describe("CalendarSubscriptionService", () => {
125125
mockFeaturesRepository = {
126126
checkIfFeatureIsEnabledGlobally: vi.fn().mockResolvedValue(true),
127127
checkIfUserHasFeature: vi.fn().mockResolvedValue(true),
128+
checkIfTeamHasFeature: vi.fn().mockResolvedValue(true),
129+
getTeamsWithFeatureEnabled: vi.fn().mockResolvedValue([1, 2, 3]),
128130
};
129131

130132
mockCalendarCacheEventService = {
@@ -376,12 +378,94 @@ describe("CalendarSubscriptionService", () => {
376378

377379
await service.checkForNewSubscriptions();
378380

381+
expect(mockFeaturesRepository.getTeamsWithFeatureEnabled).toHaveBeenCalledWith(
382+
"calendar-subscription-cache"
383+
);
379384
expect(mockSelectedCalendarRepository.findNextSubscriptionBatch).toHaveBeenCalledWith({
380385
take: 100,
381386
integrations: ["google_calendar", "office365_calendar"],
387+
teamIds: [1, 2, 3],
382388
});
383389
expect(subscribeSpy).toHaveBeenCalledWith(mockSelectedCalendar.id);
384390
});
391+
392+
test("should handle mixed cache scenario where some teams have cache enabled and some do not", async () => {
393+
const calendarWithCache = { ...mockSelectedCalendar, id: "calendar-with-cache", userId: 1 };
394+
const calendarWithCache2 = { ...mockSelectedCalendar, id: "calendar-with-cache-2", userId: 2 };
395+
396+
mockFeaturesRepository.getTeamsWithFeatureEnabled.mockResolvedValue([10, 20]);
397+
398+
mockSelectedCalendarRepository.findNextSubscriptionBatch.mockResolvedValue([
399+
calendarWithCache,
400+
calendarWithCache2,
401+
]);
402+
403+
const subscribeSpy = vi.spyOn(service, "subscribe").mockResolvedValue(undefined);
404+
405+
await service.checkForNewSubscriptions();
406+
407+
expect(mockFeaturesRepository.getTeamsWithFeatureEnabled).toHaveBeenCalledWith(
408+
"calendar-subscription-cache"
409+
);
410+
expect(mockSelectedCalendarRepository.findNextSubscriptionBatch).toHaveBeenCalledWith({
411+
take: 100,
412+
integrations: ["google_calendar", "office365_calendar"],
413+
teamIds: [10, 20],
414+
});
415+
expect(subscribeSpy).toHaveBeenCalledTimes(2);
416+
expect(subscribeSpy).toHaveBeenCalledWith("calendar-with-cache");
417+
expect(subscribeSpy).toHaveBeenCalledWith("calendar-with-cache-2");
418+
});
419+
420+
test("should only fetch calendars for teams with feature enabled, not entire organization hierarchy", async () => {
421+
const teamId = 100;
422+
const parentOrgId = 1;
423+
424+
mockFeaturesRepository.getTeamsWithFeatureEnabled.mockResolvedValue([teamId]);
425+
426+
const calendarForTeamMember = { ...mockSelectedCalendar, id: "team-member-calendar", userId: 5 };
427+
mockSelectedCalendarRepository.findNextSubscriptionBatch.mockResolvedValue([calendarForTeamMember]);
428+
429+
const subscribeSpy = vi.spyOn(service, "subscribe").mockResolvedValue(undefined);
430+
431+
await service.checkForNewSubscriptions();
432+
433+
expect(mockFeaturesRepository.getTeamsWithFeatureEnabled).toHaveBeenCalledWith(
434+
"calendar-subscription-cache"
435+
);
436+
expect(mockSelectedCalendarRepository.findNextSubscriptionBatch).toHaveBeenCalledWith({
437+
take: 100,
438+
integrations: ["google_calendar", "office365_calendar"],
439+
teamIds: [teamId],
440+
});
441+
expect(mockSelectedCalendarRepository.findNextSubscriptionBatch).not.toHaveBeenCalledWith(
442+
expect.objectContaining({
443+
teamIds: expect.arrayContaining([parentOrgId]),
444+
})
445+
);
446+
expect(subscribeSpy).toHaveBeenCalledTimes(1);
447+
expect(subscribeSpy).toHaveBeenCalledWith("team-member-calendar");
448+
});
449+
450+
test("should not process any calendars when no teams have the feature enabled", async () => {
451+
mockFeaturesRepository.getTeamsWithFeatureEnabled.mockResolvedValue([]);
452+
453+
mockSelectedCalendarRepository.findNextSubscriptionBatch.mockResolvedValue([]);
454+
455+
const subscribeSpy = vi.spyOn(service, "subscribe").mockResolvedValue(undefined);
456+
457+
await service.checkForNewSubscriptions();
458+
459+
expect(mockFeaturesRepository.getTeamsWithFeatureEnabled).toHaveBeenCalledWith(
460+
"calendar-subscription-cache"
461+
);
462+
expect(mockSelectedCalendarRepository.findNextSubscriptionBatch).toHaveBeenCalledWith({
463+
take: 100,
464+
integrations: ["google_calendar", "office365_calendar"],
465+
teamIds: [],
466+
});
467+
expect(subscribeSpy).not.toHaveBeenCalled();
468+
});
385469
});
386470

387471
describe("feature flag methods", () => {

packages/features/flags/features.repository.interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ export interface IFeaturesRepository {
44
checkIfFeatureIsEnabledGlobally(slug: keyof AppFlags): Promise<boolean>;
55
checkIfUserHasFeature(userId: number, slug: string): Promise<boolean>;
66
checkIfTeamHasFeature(teamId: number, slug: keyof AppFlags): Promise<boolean>;
7+
getTeamsWithFeatureEnabled(slug: keyof AppFlags): Promise<number[]>;
78
}

packages/features/flags/features.repository.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,4 +351,23 @@ export class FeaturesRepository implements IFeaturesRepository {
351351
throw err;
352352
}
353353
}
354+
355+
async getTeamsWithFeatureEnabled(slug: keyof AppFlags): Promise<number[]> {
356+
try {
357+
// If globally disabled, treat as effectively disabled everywhere
358+
const isGloballyEnabled = await this.checkIfFeatureIsEnabledGlobally(slug);
359+
if (!isGloballyEnabled) return [];
360+
361+
const rows = await this.prismaClient.teamFeatures.findMany({
362+
where: { featureId: slug },
363+
select: { teamId: true },
364+
orderBy: { teamId: "asc" },
365+
});
366+
367+
return rows.map((r) => r.teamId);
368+
} catch (err) {
369+
captureException(err);
370+
throw err;
371+
}
372+
}
354373
}

packages/lib/server/repository/SelectedCalendarRepository.interface.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@ export interface ISelectedCalendarRepository {
2424
*/
2525
findNextSubscriptionBatch({
2626
take,
27+
teamIds,
2728
integrations,
2829
}: {
2930
take: number;
31+
teamIds: number[];
3032
integrations?: string[];
3133
}): Promise<SelectedCalendar[]>;
3234

packages/lib/server/repository/SelectedCalendarRepository.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,24 @@ export class SelectedCalendarRepository implements ISelectedCalendarRepository {
1515
return this.prismaClient.selectedCalendar.findFirst({ where: { channelId } });
1616
}
1717

18-
async findNextSubscriptionBatch({ take, integrations }: { take: number; integrations: string[] }) {
18+
async findNextSubscriptionBatch({
19+
take,
20+
teamIds,
21+
integrations,
22+
}: {
23+
take: number;
24+
teamIds: number[];
25+
integrations: string[];
26+
}) {
1927
return this.prismaClient.selectedCalendar.findMany({
2028
where: {
2129
integration: { in: integrations },
2230
OR: [{ syncSubscribedAt: null }, { channelExpiration: { lte: new Date() } }],
23-
// initially we will run subscription only for teams that have
24-
// the feature flags enabled and it should be removed later
2531
user: {
2632
teams: {
2733
some: {
28-
team: {
29-
features: {
30-
some: {
31-
OR: [
32-
{ featureId: "calendar-subscription-cache" },
33-
{ featureId: "calendar-subscription-sync" },
34-
],
35-
},
36-
},
37-
},
34+
teamId: { in: teamIds },
35+
accepted: true,
3836
},
3937
},
4038
},

packages/lib/server/repository/__tests__/SelectedCalendarRepository.test.ts

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ describe("SelectedCalendarRepository", () => {
116116

117117
const result = await repository.findNextSubscriptionBatch({
118118
take: 10,
119+
teamIds: [1, 2, 3],
119120
integrations: ["google_calendar", "office365_calendar"],
120121
});
121122

@@ -126,16 +127,8 @@ describe("SelectedCalendarRepository", () => {
126127
user: {
127128
teams: {
128129
some: {
129-
team: {
130-
features: {
131-
some: {
132-
OR: [
133-
{ featureId: "calendar-subscription-cache" },
134-
{ featureId: "calendar-subscription-sync" },
135-
],
136-
},
137-
},
138-
},
130+
teamId: { in: [1, 2, 3] },
131+
accepted: true,
139132
},
140133
},
141134
},
@@ -152,6 +145,7 @@ describe("SelectedCalendarRepository", () => {
152145

153146
const result = await repository.findNextSubscriptionBatch({
154147
take: 5,
148+
teamIds: [10, 20],
155149
integrations: [],
156150
});
157151

@@ -162,16 +156,8 @@ describe("SelectedCalendarRepository", () => {
162156
user: {
163157
teams: {
164158
some: {
165-
team: {
166-
features: {
167-
some: {
168-
OR: [
169-
{ featureId: "calendar-subscription-cache" },
170-
{ featureId: "calendar-subscription-sync" },
171-
],
172-
},
173-
},
174-
},
159+
teamId: { in: [10, 20] },
160+
accepted: true,
175161
},
176162
},
177163
},
@@ -181,6 +167,35 @@ describe("SelectedCalendarRepository", () => {
181167

182168
expect(result).toEqual(mockCalendars);
183169
});
170+
171+
test("should handle empty teamIds array", async () => {
172+
const mockCalendars: SelectedCalendar[] = [];
173+
vi.mocked(mockPrismaClient.selectedCalendar.findMany).mockResolvedValue(mockCalendars);
174+
175+
const result = await repository.findNextSubscriptionBatch({
176+
take: 10,
177+
teamIds: [],
178+
integrations: ["google_calendar"],
179+
});
180+
181+
expect(mockPrismaClient.selectedCalendar.findMany).toHaveBeenCalledWith({
182+
where: {
183+
integration: { in: ["google_calendar"] },
184+
OR: [{ syncSubscribedAt: null }, { channelExpiration: { lte: expect.any(Date) } }],
185+
user: {
186+
teams: {
187+
some: {
188+
teamId: { in: [] },
189+
accepted: true,
190+
},
191+
},
192+
},
193+
},
194+
take: 10,
195+
});
196+
197+
expect(result).toEqual(mockCalendars);
198+
});
184199
});
185200

186201
describe("updateSyncStatus", () => {

0 commit comments

Comments
 (0)