From dfc022d8906e5b3cc0746cac244f8533b8a1dadf Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Tue, 27 May 2025 21:34:36 +0100 Subject: [PATCH] Revert "perf: parallelize getBusyTimes calls to improve performance (#21372)" This reverts commit a06b2009ab9fb27caef6264c6d3180edd6232727. --- packages/lib/getBusyTimes.ts | 155 +++++++----------- packages/lib/getUserAvailability.ts | 23 +-- .../server/getBusyTimesFromLimits.ts | 58 +++---- 3 files changed, 87 insertions(+), 149 deletions(-) diff --git a/packages/lib/getBusyTimes.ts b/packages/lib/getBusyTimes.ts index 95050b8204..62107ab264 100644 --- a/packages/lib/getBusyTimes.ts +++ b/packages/lib/getBusyTimes.ts @@ -20,72 +20,6 @@ import type { CredentialForCalendarService } from "@calcom/types/Credential"; import { getDefinedBufferTimes } from "../features/eventtypes/lib/getDefinedBufferTimes"; import { BookingRepository as BookingRepo } from "./server/repository/booking"; -const processBookingsToBusyTimes = ( - bookings: (Pick & { - eventType: Pick | null; - _count?: { - seatsReferences: number; - }; - })[], - rescheduleUid: string | null | undefined, - eventTypeId: number | undefined, - beforeEventBuffer: number | undefined, - afterEventBuffer: number | undefined -) => { - const bookingSeatCountMap: { [x: string]: number } = {}; - return { - busyTimes: bookings.reduce((aggregate: EventBusyDetails[], booking) => { - const { id, startTime, endTime, eventType, title, ...rest } = booking; - - const minutesToBlockBeforeEvent = (eventType?.beforeEventBuffer || 0) + (afterEventBuffer || 0); - const minutesToBlockAfterEvent = (eventType?.afterEventBuffer || 0) + (beforeEventBuffer || 0); - - if (rest._count?.seatsReferences) { - const bookedAt = `${dayjs(startTime).utc().format()}<>${dayjs(endTime).utc().format()}`; - bookingSeatCountMap[bookedAt] = bookingSeatCountMap[bookedAt] || 0; - bookingSeatCountMap[bookedAt]++; - // Seat references on the current event are non-blocking until the event is fully booked. - if ( - // there are still seats available. - bookingSeatCountMap[bookedAt] < (eventType?.seatsPerTimeSlot || 1) && - // and this is the seated event, other event types should be blocked. - eventTypeId === eventType?.id - ) { - // then we ONLY add the before/after buffer times as busy times. - if (minutesToBlockBeforeEvent) { - aggregate.push({ - start: dayjs(startTime).subtract(minutesToBlockBeforeEvent, "minute").toDate(), - end: dayjs(startTime).toDate(), // The event starts after the buffer - }); - } - if (minutesToBlockAfterEvent) { - aggregate.push({ - start: dayjs(endTime).toDate(), // The event ends before the buffer - end: dayjs(endTime).add(minutesToBlockAfterEvent, "minute").toDate(), - }); - } - return aggregate; - } - // if it does get blocked at this point; we remove the bookingSeatCountMap entry - // doing this allows using the map later to remove the ranges from calendar busy times. - delete bookingSeatCountMap[bookedAt]; - } - // rescheduling the same booking to the same time should be possible. Why? - if (rest.uid === rescheduleUid) { - return aggregate; - } - aggregate.push({ - start: dayjs(startTime).subtract(minutesToBlockBeforeEvent, "minute").toDate(), - end: dayjs(endTime).add(minutesToBlockAfterEvent, "minute").toDate(), - title, - source: `eventType-${eventType?.id}-booking-${id}`, - }); - return aggregate; - }, []), - bookingSeatCountMap, - }; -}; - const _getBusyTimes = async (params: { credentials: CredentialForCalendarService[]; userId: number; @@ -173,45 +107,65 @@ const _getBusyTimes = async (params: { // to avoid potential side effects. let bookings = params.currentBookings; - const promises = []; - let bookingsPromise; - if (!bookings) { - bookingsPromise = BookingRepo.findAllExistingBookingsForEventTypeBetween({ + bookings = await BookingRepo.findAllExistingBookingsForEventTypeBetween({ userIdAndEmailMap: new Map([[userId, userEmail]]), eventTypeId, startDate: startTimeAdjustedWithMaxBuffer, endDate: endTimeAdjustedWithMaxBuffer, seatedEvent, }); - promises.push(bookingsPromise); } - let calendarBusyTimesPromise; - if (credentials?.length > 0 && !bypassBusyCalendarTimes) { - calendarBusyTimesPromise = getBusyCalendarTimes( - credentials, - startTime, - endTime, - selectedCalendars, - shouldServeCache - ); - promises.push(calendarBusyTimesPromise); - } + const bookingSeatCountMap: { [x: string]: number } = {}; + const busyTimes = bookings.reduce((aggregate: EventBusyDetails[], booking) => { + const { id, startTime, endTime, eventType, title, ...rest } = booking; - await Promise.all(promises); + const minutesToBlockBeforeEvent = (eventType?.beforeEventBuffer || 0) + (afterEventBuffer || 0); + const minutesToBlockAfterEvent = (eventType?.afterEventBuffer || 0) + (beforeEventBuffer || 0); - if (bookingsPromise) { - bookings = await bookingsPromise; - } - - const { busyTimes, bookingSeatCountMap } = processBookingsToBusyTimes( - bookings || [], - rescheduleUid, - eventTypeId, - beforeEventBuffer, - afterEventBuffer - ); + if (rest._count?.seatsReferences) { + const bookedAt = `${dayjs(startTime).utc().format()}<>${dayjs(endTime).utc().format()}`; + bookingSeatCountMap[bookedAt] = bookingSeatCountMap[bookedAt] || 0; + bookingSeatCountMap[bookedAt]++; + // Seat references on the current event are non-blocking until the event is fully booked. + if ( + // there are still seats available. + bookingSeatCountMap[bookedAt] < (eventType?.seatsPerTimeSlot || 1) && + // and this is the seated event, other event types should be blocked. + eventTypeId === eventType?.id + ) { + // then we ONLY add the before/after buffer times as busy times. + if (minutesToBlockBeforeEvent) { + aggregate.push({ + start: dayjs(startTime).subtract(minutesToBlockBeforeEvent, "minute").toDate(), + end: dayjs(startTime).toDate(), // The event starts after the buffer + }); + } + if (minutesToBlockAfterEvent) { + aggregate.push({ + start: dayjs(endTime).toDate(), // The event ends before the buffer + end: dayjs(endTime).add(minutesToBlockAfterEvent, "minute").toDate(), + }); + } + return aggregate; + } + // if it does get blocked at this point; we remove the bookingSeatCountMap entry + // doing this allows using the map later to remove the ranges from calendar busy times. + delete bookingSeatCountMap[bookedAt]; + } + // rescheduling the same booking to the same time should be possible. Why? + if (rest.uid === rescheduleUid) { + return aggregate; + } + aggregate.push({ + start: dayjs(startTime).subtract(minutesToBlockBeforeEvent, "minute").toDate(), + end: dayjs(endTime).add(minutesToBlockAfterEvent, "minute").toDate(), + title, + source: `eventType-${eventType?.id}-booking-${id}`, + }); + return aggregate; + }, []); logger.debug( `Busy Time from Cal Bookings ${JSON.stringify({ @@ -222,10 +176,15 @@ const _getBusyTimes = async (params: { ); performance.mark("prismaBookingGetEnd"); performance.measure(`prisma booking get took $1'`, "prismaBookingGetStart", "prismaBookingGetEnd"); - - if (credentials?.length > 0 && !bypassBusyCalendarTimes && calendarBusyTimesPromise) { + if (credentials?.length > 0 && !bypassBusyCalendarTimes) { const startConnectedCalendarsGet = performance.now(); - const calendarBusyTimes = await calendarBusyTimesPromise; + const calendarBusyTimes = await getBusyCalendarTimes( + credentials, + startTime, + endTime, + selectedCalendars, + shouldServeCache + ); const endConnectedCalendarsGet = performance.now(); logger.debug( `Connected Calendars get took ${ @@ -245,7 +204,7 @@ const _getBusyTimes = async (params: { }); if (rescheduleUid) { - const originalRescheduleBooking = bookings?.find((booking) => booking.uid === rescheduleUid); + const originalRescheduleBooking = bookings.find((booking) => booking.uid === rescheduleUid); // calendar busy time from original rescheduled booking should not be blocked if (originalRescheduleBooking) { openSeatsDateRanges.push({ diff --git a/packages/lib/getUserAvailability.ts b/packages/lib/getUserAvailability.ts index 6b065c4488..2e5076ed45 100644 --- a/packages/lib/getUserAvailability.ts +++ b/packages/lib/getUserAvailability.ts @@ -349,15 +349,12 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA const durationLimits = parseDurationLimit(eventType?.durationLimits); let busyTimesFromLimits: EventBusyDetails[] = []; - let busyTimesFromTeamLimits: EventBusyDetails[] = []; - - const busyTimesPromises = []; if (initialData?.busyTimesFromLimits && initialData?.eventTypeForLimits) { busyTimesFromLimits = initialData.busyTimesFromLimits.get(user.id) || []; } else if (eventType && (bookingLimits || durationLimits)) { // Fall back to individual query if not available in initialData - const busyTimesFromLimitsPromise = getBusyTimesFromLimits( + busyTimesFromLimits = await getBusyTimesFromLimits( bookingLimits, durationLimits, dateFrom.tz(timeZone), @@ -367,10 +364,7 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA initialData?.busyTimesFromLimitsBookings ?? [], timeZone, initialData?.rescheduleUid ?? undefined - ).then((result) => { - busyTimesFromLimits = result; - }); - busyTimesPromises.push(busyTimesFromLimitsPromise); + ); } const teamForBookingLimits = @@ -380,11 +374,13 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA const teamBookingLimits = parseBookingLimit(teamForBookingLimits?.bookingLimits); + let busyTimesFromTeamLimits: EventBusyDetails[] = []; + if (initialData?.teamBookingLimits && teamForBookingLimits) { busyTimesFromTeamLimits = initialData.teamBookingLimits.get(user.id) || []; } else if (teamForBookingLimits && teamBookingLimits) { // Fall back to individual query if not available in initialData - const busyTimesFromTeamLimitsPromise = getBusyTimesFromTeamLimits( + busyTimesFromTeamLimits = await getBusyTimesFromTeamLimits( user, teamBookingLimits, dateFrom.tz(timeZone), @@ -393,14 +389,7 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA teamForBookingLimits.includeManagedEventsInLimits, timeZone, initialData?.rescheduleUid ?? undefined - ).then((result) => { - busyTimesFromTeamLimits = result; - }); - busyTimesPromises.push(busyTimesFromTeamLimitsPromise); - } - - if (busyTimesPromises.length > 0) { - await Promise.all(busyTimesPromises); + ); } // TODO: only query what we need after applying limits (shrink date range) diff --git a/packages/lib/intervalLimits/server/getBusyTimesFromLimits.ts b/packages/lib/intervalLimits/server/getBusyTimesFromLimits.ts index 85ba9f98eb..f87261f34d 100644 --- a/packages/lib/intervalLimits/server/getBusyTimesFromLimits.ts +++ b/packages/lib/intervalLimits/server/getBusyTimesFromLimits.ts @@ -31,51 +31,41 @@ const _getBusyTimesFromLimits = async ( // shared amongst limiters to prevent processing known busy periods const limitManager = new LimitManager(); - const limitChecks = []; - // run this first, as counting bookings should always run faster.. if (bookingLimits) { performance.mark("bookingLimitsStart"); - limitChecks.push( - getBusyTimesFromBookingLimits({ - bookings, - bookingLimits, - dateFrom, - dateTo, - eventTypeId: eventType.id, - limitManager, - rescheduleUid, - timeZone, - }).then(() => { - performance.mark("bookingLimitsEnd"); - performance.measure(`checking booking limits took $1'`, "bookingLimitsStart", "bookingLimitsEnd"); - }) - ); + await getBusyTimesFromBookingLimits({ + bookings, + bookingLimits, + dateFrom, + dateTo, + eventTypeId: eventType.id, + limitManager, + rescheduleUid, + timeZone, + }); + performance.mark("bookingLimitsEnd"); + performance.measure(`checking booking limits took $1'`, "bookingLimitsStart", "bookingLimitsEnd"); } // ..than adding up durations (especially for the whole year) if (durationLimits) { performance.mark("durationLimitsStart"); - limitChecks.push( - getBusyTimesFromDurationLimits( - bookings, - durationLimits, - dateFrom, - dateTo, - duration, - eventType, - limitManager, - timeZone, - rescheduleUid - ).then(() => { - performance.mark("durationLimitsEnd"); - performance.measure(`checking duration limits took $1'`, "durationLimitsStart", "durationLimitsEnd"); - }) + await getBusyTimesFromDurationLimits( + bookings, + durationLimits, + dateFrom, + dateTo, + duration, + eventType, + limitManager, + timeZone, + rescheduleUid ); + performance.mark("durationLimitsEnd"); + performance.measure(`checking duration limits took $1'`, "durationLimitsStart", "durationLimitsEnd"); } - await Promise.all(limitChecks); - performance.mark("limitsEnd"); performance.measure(`checking all limits took $1'`, "limitsStart", "limitsEnd");