Revert "perf: parallelize getBusyTimes calls to improve performance (#21372)"

This reverts commit a06b2009ab.
This commit is contained in:
Alex van Andel
2025-05-27 23:30:30 +01:00
parent 68cb0cf197
commit dfc022d890
3 changed files with 87 additions and 149 deletions
+57 -98
View File
@@ -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<Booking, "id" | "uid" | "userId" | "startTime" | "endTime" | "title"> & {
eventType: Pick<EventType, "id" | "beforeEventBuffer" | "afterEventBuffer" | "seatsPerTimeSlot"> | 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({
+6 -17
View File
@@ -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)
@@ -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");