-
Notifications
You must be signed in to change notification settings - Fork 8.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: allows forcing/skipping calendar cache serving #18224
Changes from 1 commit
704ae48
07ae45a
653f536
80e144f
d273512
3427d8b
20f1a40
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ import { MeetLocationType } from "@calcom/app-store/locations"; | |
import dayjs from "@calcom/dayjs"; | ||
import { CalendarCache } from "@calcom/features/calendar-cache/calendar-cache"; | ||
import type { FreeBusyArgs } from "@calcom/features/calendar-cache/calendar-cache.repository.interface"; | ||
import { getTimeMax, getTimeMin } from "@calcom/features/calendar-cache/lib/datesForCache"; | ||
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser"; | ||
import type CalendarService from "@calcom/lib/CalendarService"; | ||
import { | ||
|
@@ -510,16 +511,22 @@ export default class GoogleCalendarService implements Calendar { | |
return apiResponse.json; | ||
} | ||
|
||
async getCacheOrFetchAvailability(args: FreeBusyArgs): Promise<EventBusyDate[] | null> { | ||
const { timeMin, timeMax, items } = args; | ||
let freeBusyResult: calendar_v3.Schema$FreeBusyResponse = {}; | ||
async getFreeBusyResult( | ||
args: FreeBusyArgs, | ||
shouldServeCache?: boolean | ||
): Promise<calendar_v3.Schema$FreeBusyResponse> { | ||
if (shouldServeCache === false) return await this.fetchAvailability(args); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the parameter is explicitly set to false we skip cache altogether |
||
const calendarCache = await CalendarCache.init(null); | ||
const cached = await calendarCache.getCachedAvailability(this.credential.id, args); | ||
if (cached) { | ||
freeBusyResult = cached.value as unknown as calendar_v3.Schema$FreeBusyResponse; | ||
} else { | ||
freeBusyResult = await this.fetchAvailability({ timeMin, timeMax, items }); | ||
} | ||
if (cached) return cached.value as unknown as calendar_v3.Schema$FreeBusyResponse; | ||
return await this.fetchAvailability(args); | ||
} | ||
|
||
async getCacheOrFetchAvailability( | ||
args: FreeBusyArgs, | ||
shouldServeCache?: boolean | ||
): Promise<EventBusyDate[] | null> { | ||
const freeBusyResult = await this.getFreeBusyResult(args, shouldServeCache); | ||
if (!freeBusyResult.calendars) return null; | ||
|
||
const result = Object.values(freeBusyResult.calendars).reduce((c, i) => { | ||
|
@@ -537,7 +544,8 @@ export default class GoogleCalendarService implements Calendar { | |
async getAvailability( | ||
dateFrom: string, | ||
dateTo: string, | ||
selectedCalendars: IntegrationCalendar[] | ||
selectedCalendars: IntegrationCalendar[], | ||
shouldServeCache?: boolean | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not a fan to drilling but I couldn't find a cleaner way to pass this setting around. |
||
): Promise<EventBusyDate[]> { | ||
this.log.debug("Getting availability", safeStringify({ dateFrom, dateTo, selectedCalendars })); | ||
const calendar = await this.authedCalendar(); | ||
|
@@ -563,11 +571,14 @@ export default class GoogleCalendarService implements Calendar { | |
|
||
// /freebusy from google api only allows a date range of 90 days | ||
if (diff <= 90) { | ||
const freeBusyData = await this.getCacheOrFetchAvailability({ | ||
timeMin: dateFrom, | ||
timeMax: dateTo, | ||
items: calsIds.map((id) => ({ id })), | ||
}); | ||
const freeBusyData = await this.getCacheOrFetchAvailability( | ||
{ | ||
timeMin: dateFrom, | ||
timeMax: dateTo, | ||
items: calsIds.map((id) => ({ id })), | ||
}, | ||
shouldServeCache | ||
); | ||
if (!freeBusyData) throw new Error("No response from google calendar"); | ||
|
||
return freeBusyData; | ||
|
@@ -583,11 +594,14 @@ export default class GoogleCalendarService implements Calendar { | |
if (endDate.isAfter(originalEndDate)) endDate = originalEndDate; | ||
|
||
busyData.push( | ||
...((await this.getCacheOrFetchAvailability({ | ||
timeMin: startDate.format(), | ||
timeMax: endDate.format(), | ||
items: calsIds.map((id) => ({ id })), | ||
})) || []) | ||
...((await this.getCacheOrFetchAvailability( | ||
{ | ||
timeMin: startDate.format(), | ||
timeMax: endDate.format(), | ||
items: calsIds.map((id) => ({ id })), | ||
}, | ||
shouldServeCache | ||
)) || []) | ||
); | ||
|
||
startDate = endDate.add(1, "minutes"); | ||
|
@@ -672,12 +686,7 @@ export default class GoogleCalendarService implements Calendar { | |
} | ||
async unwatchCalendar({ calendarId }: { calendarId: string }) { | ||
const credentialId = this.credential.id; | ||
const sc = await prisma.selectedCalendar.findFirst({ | ||
where: { | ||
credentialId, | ||
externalId: calendarId, | ||
}, | ||
}); | ||
const sc = await SelectedCalendarRepository.findByExternalId(credentialId, calendarId); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved to repo |
||
// Delete the calendar cache to force a fresh cache | ||
await prisma.calendarCache.deleteMany({ where: { credentialId } }); | ||
const calendar = await this.authedCalendar(); | ||
|
@@ -699,6 +708,12 @@ export default class GoogleCalendarService implements Calendar { | |
googleChannelResourceUri: null, | ||
googleChannelExpiration: null, | ||
}); | ||
// Populate the cache back for the remaining calendars, if any | ||
const remainingCalendars = | ||
sc?.credential?.selectedCalendars.filter((sc) => sc.externalId !== calendarId) || []; | ||
if (remainingCalendars.length > 0) { | ||
await this.fetchAvailabilityAndSetCache(remainingCalendars); | ||
} | ||
Comment on lines
+711
to
+716
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This fixes a concern that @hariombalhara exposed |
||
} | ||
|
||
async setAvailabilityInCache(args: FreeBusyArgs, data: calendar_v3.Schema$FreeBusyResponse): Promise<void> { | ||
|
@@ -707,12 +722,11 @@ export default class GoogleCalendarService implements Calendar { | |
} | ||
|
||
async fetchAvailabilityAndSetCache(selectedCalendars: IntegrationCalendar[]) { | ||
const date = new Date(); | ||
const parsedArgs = { | ||
/** Expand the start date to the start of the month */ | ||
timeMin: new Date(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0).toISOString(), | ||
timeMin: getTimeMax(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved to util to reuse when saving and fetching |
||
/** Expand the end date to the end of the month */ | ||
timeMax: new Date(date.getFullYear(), date.getMonth() + 1, 0, 0, 0, 0, 0).toISOString(), | ||
timeMax: getTimeMin(), | ||
items: selectedCalendars.map((sc) => ({ id: sc.externalId })), | ||
}; | ||
const data = await this.fetchAvailability(parsedArgs); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,7 +8,6 @@ import { getUid } from "@calcom/lib/CalEventParser"; | |
import logger from "@calcom/lib/logger"; | ||
import { getPiiFreeCalendarEvent, getPiiFreeCredential } from "@calcom/lib/piiFreeData"; | ||
import { safeStringify } from "@calcom/lib/safeStringify"; | ||
import { performance } from "@calcom/lib/server/perfObserver"; | ||
import type { | ||
CalendarEvent, | ||
EventBusyDate, | ||
|
@@ -136,51 +135,6 @@ const cleanIntegrationKeys = ( | |
return rest; | ||
}; | ||
|
||
// here I will fetch the page json file. | ||
export const getCachedResults = async ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dead code |
||
withCredentials: CredentialPayload[], | ||
dateFrom: string, | ||
dateTo: string, | ||
selectedCalendars: SelectedCalendar[] | ||
): Promise<EventBusyDate[][]> => { | ||
const calendarCredentials = withCredentials.filter((credential) => credential.type.endsWith("_calendar")); | ||
const calendars = await Promise.all(calendarCredentials.map((credential) => getCalendar(credential))); | ||
performance.mark("getBusyCalendarTimesStart"); | ||
const results = calendars.map(async (c, i) => { | ||
/** Filter out nulls */ | ||
if (!c) return []; | ||
/** We rely on the index so we can match credentials with calendars */ | ||
const { type, appId } = calendarCredentials[i]; | ||
/** We just pass the calendars that matched the credential type, | ||
* TODO: Migrate credential type or appId | ||
*/ | ||
const passedSelectedCalendars = selectedCalendars.filter((sc) => sc.integration === type); | ||
if (!passedSelectedCalendars.length) return []; | ||
/** We extract external Ids so we don't cache too much */ | ||
const selectedCalendarIds = passedSelectedCalendars.map((sc) => sc.externalId); | ||
/** If we don't then we actually fetch external calendars (which can be very slow) */ | ||
performance.mark("eventBusyDatesStart"); | ||
const eventBusyDates = await c.getAvailability(dateFrom, dateTo, passedSelectedCalendars); | ||
performance.mark("eventBusyDatesEnd"); | ||
performance.measure( | ||
`[getAvailability for ${selectedCalendarIds.join(", ")}][$1]'`, | ||
"eventBusyDatesStart", | ||
"eventBusyDatesEnd" | ||
); | ||
|
||
return eventBusyDates.map((a: object) => ({ ...a, source: `${appId}` })); | ||
}); | ||
const awaitedResults = await Promise.all(results); | ||
performance.mark("getBusyCalendarTimesEnd"); | ||
performance.measure( | ||
`getBusyCalendarTimes took $1 for creds ${calendarCredentials.map((cred) => cred.id)}`, | ||
"getBusyCalendarTimesStart", | ||
"getBusyCalendarTimesEnd" | ||
); | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
return awaitedResults as any; | ||
}; | ||
|
||
/** | ||
* Get months between given dates | ||
* @returns ["2023-04", "2024-05"] | ||
|
@@ -203,7 +157,8 @@ export const getBusyCalendarTimes = async ( | |
withCredentials: CredentialPayload[], | ||
dateFrom: string, | ||
dateTo: string, | ||
selectedCalendars: SelectedCalendar[] | ||
selectedCalendars: SelectedCalendar[], | ||
shouldServeCache?: boolean | ||
) => { | ||
let results: EventBusyDate[][] = []; | ||
// const months = getMonths(dateFrom, dateTo); | ||
|
@@ -212,7 +167,13 @@ export const getBusyCalendarTimes = async ( | |
const startDate = dayjs(dateFrom).subtract(11, "hours").format(); | ||
// Add 14 hours from the start date to avoid problems in UTC+ time zones. | ||
const endDate = dayjs(dateTo).endOf("month").add(14, "hours").format(); | ||
results = await getCalendarsEvents(withCredentials, startDate, endDate, selectedCalendars); | ||
results = await getCalendarsEvents( | ||
withCredentials, | ||
startDate, | ||
endDate, | ||
selectedCalendars, | ||
shouldServeCache | ||
); | ||
} catch (e) { | ||
log.warn(safeStringify(e)); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,6 +44,7 @@ export async function getBusyTimes(params: { | |
})[] | ||
| null; | ||
bypassBusyCalendarTimes: boolean; | ||
shouldServeCache?: boolean; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this nullable? Seems like we should either just pass true or false. |
||
}) { | ||
const { | ||
credentials, | ||
|
@@ -60,6 +61,7 @@ export async function getBusyTimes(params: { | |
rescheduleUid, | ||
duration, | ||
bypassBusyCalendarTimes = false, | ||
shouldServeCache, | ||
} = params; | ||
|
||
logger.silly( | ||
|
@@ -243,7 +245,8 @@ export async function getBusyTimes(params: { | |
credentials, | ||
startTime, | ||
endTime, | ||
selectedCalendars | ||
selectedCalendars, | ||
shouldServeCache | ||
); | ||
const endConnectedCalendarsGet = performance.now(); | ||
logger.debug( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,7 +11,8 @@ const getCalendarsEvents = async ( | |
withCredentials: CredentialPayload[], | ||
dateFrom: string, | ||
dateTo: string, | ||
selectedCalendars: SelectedCalendar[] | ||
selectedCalendars: SelectedCalendar[], | ||
shouldServeCache?: boolean | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this nullable? Seems like we should either just pass true or false. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Undefined will fallback to the DB default. If we force a boolean value the fallback will never execute. |
||
): Promise<EventBusyDate[][]> => { | ||
const calendarCredentials = withCredentials | ||
.filter((credential) => credential.type.endsWith("_calendar")) | ||
|
@@ -45,7 +46,12 @@ const getCalendarsEvents = async ( | |
selectedCalendars: passedSelectedCalendars.map(getPiiFreeSelectedCalendar), | ||
}) | ||
); | ||
const eventBusyDates = await c.getAvailability(dateFrom, dateTo, passedSelectedCalendars); | ||
const eventBusyDates = await c.getAvailability( | ||
dateFrom, | ||
dateTo, | ||
passedSelectedCalendars, | ||
shouldServeCache | ||
); | ||
performance.mark("eventBusyDatesEnd"); | ||
performance.measure( | ||
`[getAvailability for ${selectedCalendarIds.join(", ")}][$1]'`, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
/** Expand the start date to the beginning of the current month */ | ||
export const getTimeMin = (timeMin?: string) => { | ||
const date = timeMin ? new Date(timeMin) : new Date(); | ||
date.setDate(1); | ||
date.setHours(0, 0, 0, 0); | ||
return date.toISOString(); | ||
}; | ||
|
||
/** Expand the end date to the end of the next month */ | ||
export function getTimeMax(timeMax?: string) { | ||
const date = timeMax ? new Date(timeMax) : new Date(); | ||
date.setMonth(date.getMonth() + 2); | ||
date.setDate(0); | ||
date.setHours(0, 0, 0, 0); | ||
return date.toISOString(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ | |
**/ | ||
export type AppFlags = { | ||
"calendar-cache": boolean; | ||
"calendar-cache-serve": boolean; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Whether to serve the cache by default or not. Override-able via URL params. @keithwillcode |
||
emails: boolean; | ||
insights: boolean; | ||
teams: boolean; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extracted to take advantage of early returns