perf: Load bookings used for limits in bulk (#13474)
This commit is contained in:
@@ -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)}`);
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -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: [] }
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Vendored
+1
@@ -55,6 +55,7 @@ export type EventBusyDate = {
|
||||
export type EventBusyDetails = EventBusyDate & {
|
||||
title?: string;
|
||||
source?: string | null;
|
||||
userId?: number | null;
|
||||
};
|
||||
|
||||
export type CalendarServiceType = typeof Calendar;
|
||||
|
||||
Reference in New Issue
Block a user