perf: Load bookings used for limits in bulk (#13474)

This commit is contained in:
Keith Williams
2024-02-06 16:56:05 -07:00
committed by GitHub
parent 903a154dbc
commit 512628e8ab
7 changed files with 97 additions and 46 deletions
+41 -12
View File
@@ -3,13 +3,15 @@ import type { Booking, EventType } from "@prisma/client";
import { getBusyCalendarTimes } from "@calcom/core/CalendarManager";
import dayjs from "@calcom/dayjs";
import { subtract } from "@calcom/lib/date-ranges";
import { intervalLimitKeyToUnit } from "@calcom/lib/intervalLimit";
import logger from "@calcom/lib/logger";
import { getPiiFreeBooking } from "@calcom/lib/piiFreeData";
import { performance } from "@calcom/lib/server/perfObserver";
import prisma from "@calcom/prisma";
import type { Prisma, SelectedCalendar } from "@calcom/prisma/client";
import { BookingStatus } from "@calcom/prisma/enums";
import type { EventBusyDetails } from "@calcom/types/Calendar";
import { stringToDayjs } from "@calcom/prisma/zod-utils";
import type { EventBusyDetails, IntervalLimit } from "@calcom/types/Calendar";
import type { CredentialPayload } from "@calcom/types/Credential";
export async function getBusyTimes(params: {
@@ -261,32 +263,57 @@ export async function getBusyTimes(params: {
}
export async function getBusyTimesForLimitChecks(params: {
userId: number;
userIds: number[];
eventTypeId: number;
startDate: Date;
endDate: Date;
startDate: string;
endDate: string;
rescheduleUid?: string | null;
bookingLimits?: IntervalLimit | null;
durationLimits?: IntervalLimit | null;
}) {
const { userId, eventTypeId, startDate, endDate, rescheduleUid } = params;
const { userIds, eventTypeId, startDate, endDate, rescheduleUid, bookingLimits, durationLimits } = params;
const startTimeAsDayJs = stringToDayjs(startDate);
const endTimeAsDayJs = stringToDayjs(endDate);
performance.mark("getBusyTimesForLimitChecksStart");
let busyTimes: EventBusyDetails[] = [];
if (!bookingLimits && !durationLimits) {
return busyTimes;
}
let limitDateFrom = stringToDayjs(startDate);
let limitDateTo = stringToDayjs(endDate);
// expand date ranges by absolute minimum required to apply limits
// (yearly limits are handled separately for performance)
for (const key of ["PER_MONTH", "PER_WEEK", "PER_DAY"] as Exclude<keyof IntervalLimit, "PER_YEAR">[]) {
if (bookingLimits?.[key] || durationLimits?.[key]) {
const unit = intervalLimitKeyToUnit(key);
limitDateFrom = dayjs.min(limitDateFrom, startTimeAsDayJs.startOf(unit));
limitDateTo = dayjs.max(limitDateTo, endTimeAsDayJs.endOf(unit));
}
}
logger.silly(
`Fetch limit checks bookings in range ${startDate} to ${endDate} for input ${JSON.stringify({
userId,
`Fetch limit checks bookings in range ${limitDateFrom} to ${limitDateTo} for input ${JSON.stringify({
eventTypeId,
status: BookingStatus.ACCEPTED,
})}`
);
performance.mark("getBusyTimesForLimitChecksStart");
const where: Prisma.BookingWhereInput = {
userId,
userId: {
in: userIds,
},
eventTypeId,
status: BookingStatus.ACCEPTED,
// FIXME: bookings that overlap on one side will never be counted
startTime: {
gte: startDate,
gte: limitDateFrom.toDate(),
},
endTime: {
lte: endDate,
lte: limitDateTo.toDate(),
},
};
@@ -308,14 +335,16 @@ export async function getBusyTimesForLimitChecks(params: {
},
},
title: true,
userId: true,
},
});
const busyTimes = bookings.map(({ id, startTime, endTime, eventType, title }) => ({
busyTimes = bookings.map(({ id, startTime, endTime, eventType, title, userId }) => ({
start: dayjs(startTime).toDate(),
end: dayjs(endTime).toDate(),
title,
source: `eventType-${eventType?.id}-booking-${id}`,
userId,
}));
logger.silly(`Fetch limit checks bookings for eventId: ${eventTypeId} ${JSON.stringify(busyTimes)}`);
+7 -30
View File
@@ -17,7 +17,7 @@ import { getTotalBookingDuration } from "@calcom/lib/server/queries";
import prisma, { availabilityUserSelect } from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import { EventTypeMetaDataSchema, stringToDayjs } from "@calcom/prisma/zod-utils";
import { EventTypeMetaDataSchema, stringToDayjsZod } from "@calcom/prisma/zod-utils";
import type {
EventBusyDate,
EventBusyDetails,
@@ -25,14 +25,14 @@ import type {
IntervalLimitUnit,
} from "@calcom/types/Calendar";
import { getBusyTimes, getBusyTimesForLimitChecks } from "./getBusyTimes";
import { getBusyTimes } from "./getBusyTimes";
import monitorCallbackAsync, { monitorCallbackSync } from "./sentryWrapper";
const log = logger.getSubLogger({ prefix: ["getUserAvailability"] });
const availabilitySchema = z
.object({
dateFrom: stringToDayjs,
dateTo: stringToDayjs,
dateFrom: stringToDayjsZod,
dateTo: stringToDayjsZod,
eventTypeId: z.number().optional(),
username: z.string().optional(),
userId: z.number().optional(),
@@ -176,6 +176,7 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
seatsReferences: number;
};
})[];
busyTimesFromLimitsBookings: EventBusyDetails[];
}
) {
const { username, userId, dateFrom, dateTo, eventTypeId, afterEventBuffer, beforeEventBuffer, duration } =
@@ -219,8 +220,7 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
dateTo,
duration,
eventType,
user.id,
initialData?.rescheduleUid
initialData?.busyTimesFromLimitsBookings ?? []
)
: [];
@@ -431,36 +431,13 @@ const _getBusyTimesFromLimits = async (
dateTo: Dayjs,
duration: number | undefined,
eventType: NonNullable<EventType>,
userId: number,
rescheduleUid?: string | null
bookings: EventBusyDetails[]
) => {
performance.mark("limitsStart");
// shared amongst limiters to prevent processing known busy periods
const limitManager = new LimitManager();
let limitDateFrom = dayjs(dateFrom);
let limitDateTo = dayjs(dateTo);
// expand date ranges by absolute minimum required to apply limits
// (yearly limits are handled separately for performance)
for (const key of ["PER_MONTH", "PER_WEEK", "PER_DAY"] as Exclude<keyof IntervalLimit, "PER_YEAR">[]) {
if (bookingLimits?.[key] || durationLimits?.[key]) {
const unit = intervalLimitKeyToUnit(key);
limitDateFrom = dayjs.min(limitDateFrom, dateFrom.startOf(unit));
limitDateTo = dayjs.max(limitDateTo, dateTo.endOf(unit));
}
}
// fetch only the data we need to check limits
const bookings = await getBusyTimesForLimitChecks({
userId,
eventTypeId: eventType.id,
startDate: limitDateFrom.toDate(),
endDate: limitDateTo.toDate(),
rescheduleUid: rescheduleUid,
});
// run this first, as counting bookings should always run faster..
if (bookingLimits) {
performance.mark("bookingLimitsStart");
@@ -23,6 +23,7 @@ import type { EventTypeAppsList } from "@calcom/app-store/utils";
import { getAppFromSlug } from "@calcom/app-store/utils";
import EventManager from "@calcom/core/EventManager";
import { getEventName } from "@calcom/core/event";
import { getBusyTimesForLimitChecks } from "@calcom/core/getBusyTimes";
import { getUserAvailability } from "@calcom/core/getUserAvailability";
import dayjs from "@calcom/dayjs";
import { scheduleMandatoryReminder } from "@calcom/ee/workflows/lib/reminders/scheduleMandatoryReminder";
@@ -53,6 +54,7 @@ import { getFullName } from "@calcom/features/form-builder/utils";
import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import { cancelScheduledJobs, scheduleTrigger } from "@calcom/features/webhooks/lib/scheduleTrigger";
import { parseBookingLimit, parseDurationLimit } from "@calcom/lib";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents";
@@ -454,6 +456,22 @@ export async function ensureAvailableUsers(
)
: undefined;
const bookingLimits = parseBookingLimit(eventType?.bookingLimits);
const durationLimits = parseDurationLimit(eventType?.durationLimits);
let busyTimesFromLimitsBookingsAllUsers: Awaited<ReturnType<typeof getBusyTimesForLimitChecks>> = [];
if (eventType && (bookingLimits || durationLimits)) {
busyTimesFromLimitsBookingsAllUsers = await getBusyTimesForLimitChecks({
userIds: eventType.users.map((u) => u.id),
eventTypeId: eventType.id,
startDate: input.dateFrom,
endDate: input.dateTo,
rescheduleUid: input.originalRescheduledBooking?.uid ?? null,
bookingLimits,
durationLimits,
});
}
/** Let's start checking for availability */
for (const user of eventType.users) {
const { dateRanges, busy: bufferedBusyTimes } = await getUserAvailability(
@@ -467,6 +485,7 @@ export async function ensureAvailableUsers(
user,
eventType,
rescheduleUid: input.originalRescheduledBooking?.uid ?? null,
busyTimesFromLimitsBookings: busyTimesFromLimitsBookingsAllUsers.filter((b) => b.userId === user.id),
}
);
+4 -2
View File
@@ -206,11 +206,13 @@ export const stringOrNumber = z.union([
z.number().int(),
]);
export const stringToDayjs = z.string().transform((val) => {
export const stringToDayjs = (val: string) => {
const matches = val.match(/([+-]\d{2}:\d{2})$/);
const timezone = matches ? matches[1] : "+00:00";
return dayjs(val).utcOffset(timezone);
});
};
export const stringToDayjsZod = z.string().transform(stringToDayjs);
export const bookingCreateBodySchema = z.object({
end: z.string().optional(),
@@ -3,12 +3,14 @@ import { countBy } from "lodash";
import { v4 as uuid } from "uuid";
import { getAggregatedAvailability } from "@calcom/core/getAggregatedAvailability";
import { getBusyTimesForLimitChecks } from "@calcom/core/getBusyTimes";
import type { CurrentSeats } from "@calcom/core/getUserAvailability";
import { getUserAvailability } from "@calcom/core/getUserAvailability";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import { getSlugOrRequestedSlug, orgDomainConfig } from "@calcom/ee/organizations/lib/orgDomains";
import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled";
import { parseBookingLimit, parseDurationLimit } from "@calcom/lib";
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import isTimeOutOfBounds from "@calcom/lib/isOutOfBounds";
import logger from "@calcom/lib/logger";
@@ -336,6 +338,8 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions) {
},
};
const allUserIds = usersWithCredentials.map((user) => user.id);
const currentBookingsAllUsers = await prisma.booking.findMany({
where: {
OR: [
@@ -343,7 +347,7 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions) {
{
...sharedQuery,
userId: {
in: usersWithCredentials.map((user) => user.id),
in: allUserIds,
},
},
// The current user has a different booking at this time he/she attends
@@ -386,6 +390,22 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions) {
},
});
const bookingLimits = parseBookingLimit(eventType?.bookingLimits);
const durationLimits = parseDurationLimit(eventType?.durationLimits);
let busyTimesFromLimitsBookingsAllUsers: Awaited<ReturnType<typeof getBusyTimesForLimitChecks>> = [];
if (eventType && (bookingLimits || durationLimits)) {
busyTimesFromLimitsBookingsAllUsers = await getBusyTimesForLimitChecks({
userIds: allUserIds,
eventTypeId: eventType.id,
startDate: startTime.format(),
endDate: endTime.format(),
rescheduleUid: input.rescheduleUid,
bookingLimits,
durationLimits,
});
}
/* We get all users working hours and busy slots */
const userAvailability = await Promise.all(
usersWithCredentials.map(async (currentUser) => {
@@ -418,6 +438,9 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions) {
const { attendees: _attendees, ...bookingWithoutAttendees } = bookings;
return bookingWithoutAttendees;
}),
busyTimesFromLimitsBookings: busyTimesFromLimitsBookingsAllUsers.filter(
(b) => b.userId === currentUser.id
),
}
);
if (!currentSeats && _currentSeats) currentSeats = _currentSeats;
@@ -46,7 +46,7 @@ export const getMemberAvailabilityHandler = async ({ ctx, input }: GetMemberAvai
dateFrom: input.dateFrom,
dateTo: input.dateTo,
},
{ user: member.user }
{ user: member.user, busyTimesFromLimitsBookings: [] }
);
};
+1
View File
@@ -55,6 +55,7 @@ export type EventBusyDate = {
export type EventBusyDetails = EventBusyDate & {
title?: string;
source?: string | null;
userId?: number | null;
};
export type CalendarServiceType = typeof Calendar;