perf: getUserAvailability if dateRange is empty, early return (#26000)
* Early return if date range is empty * test: add tests for empty workingHours early return optimization Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Volnei Munhoz <volnei@cal.com>
This commit is contained in:
co-authored by
joe@cal.com <j.auyeung419@gmail.com>
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Volnei Munhoz
parent
546a1510b2
commit
ec951cacad
@@ -24,12 +24,14 @@ import { expect, expectedSlotsForSchedule } from "./getSchedule/expects";
|
||||
import { setupAndTeardown } from "./getSchedule/setupAndTeardown";
|
||||
import { timeTravelToTheBeginningOfToday } from "./getSchedule/utils";
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
constantsScenarios.set({
|
||||
IS_PRODUCTION: true as any,
|
||||
IS_PRODUCTION: true,
|
||||
WEBAPP_URL: "http://localhost:3000",
|
||||
RESERVED_SUBDOMAINS: ["auth", "docs"] as any,
|
||||
SINGLE_ORG_SLUG: "" as any,
|
||||
});
|
||||
RESERVED_SUBDOMAINS: ["auth", "docs"],
|
||||
SINGLE_ORG_SLUG: "",
|
||||
} as any);
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
describe("getSchedule", () => {
|
||||
const availableSlotsService = getAvailableSlotsService();
|
||||
@@ -3630,4 +3632,251 @@ describe("getSchedule", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Empty working hours - early return optimization", () => {
|
||||
test("returns no slots when workingHours is empty", async () => {
|
||||
vi.setSystemTime("2024-05-21T00:00:13Z");
|
||||
|
||||
const plus1DateString = "2024-05-22";
|
||||
const plus2DateString = "2024-05-23";
|
||||
|
||||
await createBookingScenario({
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 60,
|
||||
length: 60,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
users: [
|
||||
{
|
||||
...TestData.users.example,
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.EmptyAvailability],
|
||||
},
|
||||
],
|
||||
bookings: [],
|
||||
});
|
||||
|
||||
const schedule = await availableSlotsService.getAvailableSlots({
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
orgSlug: null,
|
||||
},
|
||||
});
|
||||
|
||||
expect(schedule).toHaveDateDisabled({
|
||||
dateString: plus2DateString,
|
||||
});
|
||||
});
|
||||
|
||||
test("returns slots only for date override when workingHours is empty but date override exists", async () => {
|
||||
vi.setSystemTime("2024-05-21T00:00:13Z");
|
||||
|
||||
const plus1DateString = "2024-05-22";
|
||||
const plus2DateString = "2024-05-23";
|
||||
|
||||
await createBookingScenario({
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 60,
|
||||
length: 60,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
users: [
|
||||
{
|
||||
...TestData.users.example,
|
||||
id: 101,
|
||||
schedules: [
|
||||
{
|
||||
name: "Empty schedule with date override",
|
||||
availability: [
|
||||
{
|
||||
days: [],
|
||||
startTime: new Date("1970-01-01T09:00:00.000Z"),
|
||||
endTime: new Date("1970-01-01T12:00:00.000Z"),
|
||||
date: plus2DateString,
|
||||
},
|
||||
],
|
||||
timeZone: Timezones["+5:30"],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
bookings: [],
|
||||
});
|
||||
|
||||
const schedule = await availableSlotsService.getAvailableSlots({
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
orgSlug: null,
|
||||
},
|
||||
});
|
||||
|
||||
expect(schedule).toHaveTimeSlots(
|
||||
[`03:30:00.000Z`, `04:30:00.000Z`, `05:30:00.000Z`],
|
||||
{
|
||||
dateString: plus2DateString,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("returns slots only for date overrides when workingHours is empty but multiple date overrides exist", async () => {
|
||||
vi.setSystemTime("2024-05-21T00:00:13Z");
|
||||
|
||||
const plus1DateString = "2024-05-22";
|
||||
const plus2DateString = "2024-05-23";
|
||||
const plus3DateString = "2024-05-24";
|
||||
|
||||
await createBookingScenario({
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 60,
|
||||
length: 60,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
users: [
|
||||
{
|
||||
...TestData.users.example,
|
||||
id: 101,
|
||||
schedules: [
|
||||
{
|
||||
name: "Empty schedule with multiple date overrides",
|
||||
availability: [
|
||||
{
|
||||
days: [],
|
||||
startTime: new Date("1970-01-01T09:00:00.000Z"),
|
||||
endTime: new Date("1970-01-01T11:00:00.000Z"),
|
||||
date: plus2DateString,
|
||||
},
|
||||
{
|
||||
days: [],
|
||||
startTime: new Date("1970-01-01T14:00:00.000Z"),
|
||||
endTime: new Date("1970-01-01T16:00:00.000Z"),
|
||||
date: plus3DateString,
|
||||
},
|
||||
],
|
||||
timeZone: Timezones["+5:30"],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
bookings: [],
|
||||
});
|
||||
|
||||
const schedule = await availableSlotsService.getAvailableSlots({
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
orgSlug: null,
|
||||
},
|
||||
});
|
||||
|
||||
expect(schedule).toHaveTimeSlots([`03:30:00.000Z`, `04:30:00.000Z`], {
|
||||
dateString: plus2DateString,
|
||||
});
|
||||
|
||||
expect(schedule).toHaveTimeSlots([`08:30:00.000Z`, `09:30:00.000Z`], {
|
||||
dateString: plus3DateString,
|
||||
});
|
||||
});
|
||||
|
||||
test("returns no slots for dates without date override when workingHours is empty", async () => {
|
||||
vi.setSystemTime("2024-05-21T00:00:13Z");
|
||||
|
||||
const plus1DateString = "2024-05-22";
|
||||
const plus2DateString = "2024-05-23";
|
||||
const plus3DateString = "2024-05-24";
|
||||
|
||||
await createBookingScenario({
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 60,
|
||||
length: 60,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
users: [
|
||||
{
|
||||
...TestData.users.example,
|
||||
id: 101,
|
||||
schedules: [
|
||||
{
|
||||
name: "Empty schedule with date override only on plus3",
|
||||
availability: [
|
||||
{
|
||||
days: [],
|
||||
startTime: new Date("1970-01-01T09:00:00.000Z"),
|
||||
endTime: new Date("1970-01-01T12:00:00.000Z"),
|
||||
date: plus3DateString,
|
||||
},
|
||||
],
|
||||
timeZone: Timezones["+5:30"],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
bookings: [],
|
||||
});
|
||||
|
||||
const schedule = await availableSlotsService.getAvailableSlots({
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
orgSlug: null,
|
||||
},
|
||||
});
|
||||
|
||||
expect(schedule).toHaveDateDisabled({
|
||||
dateString: plus2DateString,
|
||||
});
|
||||
|
||||
expect(schedule).toHaveTimeSlots(
|
||||
[`03:30:00.000Z`, `04:30:00.000Z`, `05:30:00.000Z`],
|
||||
{
|
||||
dateString: plus3DateString,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -386,6 +386,132 @@ export class UserAvailabilityService {
|
||||
user,
|
||||
});
|
||||
|
||||
if (
|
||||
!(
|
||||
schedule?.availability ||
|
||||
(eventType?.availability.length ? eventType.availability : user.availability)
|
||||
)
|
||||
) {
|
||||
throw new HttpError({ statusCode: 400, message: ErrorCode.AvailabilityNotFoundInSchedule });
|
||||
}
|
||||
|
||||
const availability = (
|
||||
schedule?.availability || (eventType?.availability.length ? eventType.availability : user.availability)
|
||||
).map((a) => ({
|
||||
...a,
|
||||
userId: user.id,
|
||||
}));
|
||||
|
||||
let calendarTimezone: string | null = null;
|
||||
let finalTimezone: string | null = null;
|
||||
|
||||
if (!isTimezoneSet) {
|
||||
calendarTimezone = await this.getTimezoneFromDelegatedCalendars(user);
|
||||
if (calendarTimezone) {
|
||||
finalTimezone = calendarTimezone;
|
||||
}
|
||||
}
|
||||
|
||||
if (!finalTimezone) {
|
||||
finalTimezone = schedule.timeZone;
|
||||
}
|
||||
|
||||
const workingHours = getWorkingHours({ timeZone: finalTimezone }, availability);
|
||||
|
||||
const dateOverrides: TimeRange[] = [];
|
||||
// NOTE: getSchedule is currently calling this function for every user in a team event
|
||||
// but not using these values at all, wasting CPU. Adding this check here temporarily to avoid a larger refactor
|
||||
// since other callers do using this data.
|
||||
if (returnDateOverrides) {
|
||||
const calculateDateOverridesSpan = Sentry.startInactiveSpan({ name: "calculateDateOverrides" });
|
||||
const availabilityWithDates = availability.filter((availability) => !!availability.date);
|
||||
|
||||
for (let i = 0; i < availabilityWithDates.length; i++) {
|
||||
const override = availabilityWithDates[i];
|
||||
const startTime = dayjs.utc(override.startTime);
|
||||
const endTime = dayjs.utc(override.endTime);
|
||||
const overrideStartDate = dayjs.utc(override.date).hour(startTime.hour()).minute(startTime.minute());
|
||||
const overrideEndDate = dayjs.utc(override.date).hour(endTime.hour()).minute(endTime.minute());
|
||||
if (
|
||||
overrideStartDate.isBetween(dateFrom, dateTo, null, "[]") ||
|
||||
overrideEndDate.isBetween(dateFrom, dateTo, null, "[]")
|
||||
) {
|
||||
dateOverrides.push({
|
||||
start: overrideStartDate.toDate(),
|
||||
end: overrideEndDate.toDate(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
calculateDateOverridesSpan.end();
|
||||
}
|
||||
|
||||
const outOfOfficeDays =
|
||||
initialData?.outOfOfficeDays ??
|
||||
(await this.dependencies.oooRepo.findUserOOODays({
|
||||
userId: user.id,
|
||||
dateFrom: dateFrom.toISOString(),
|
||||
dateTo: dateTo.toISOString(),
|
||||
}));
|
||||
|
||||
const datesOutOfOffice: IOutOfOfficeData = this.calculateOutOfOfficeRanges(outOfOfficeDays, availability);
|
||||
|
||||
const holidayBlockedDates = await this.calculateHolidayBlockedDates(
|
||||
user.id,
|
||||
dateFrom.toDate(),
|
||||
dateTo.toDate(),
|
||||
availability
|
||||
);
|
||||
|
||||
for (const [date, holidayData] of Object.entries(holidayBlockedDates)) {
|
||||
if (!datesOutOfOffice[date]) {
|
||||
datesOutOfOffice[date] = holidayData;
|
||||
}
|
||||
}
|
||||
|
||||
const travelSchedules: {
|
||||
startDate: Dayjs;
|
||||
endDate?: Dayjs;
|
||||
timeZone: string;
|
||||
}[] = [];
|
||||
|
||||
if (isDefaultSchedule) {
|
||||
travelSchedules.push(
|
||||
...user.travelSchedules.map((schedule) => {
|
||||
let endDate: Dayjs | undefined;
|
||||
if (schedule.endDate) {
|
||||
endDate = dayjs(schedule.endDate);
|
||||
}
|
||||
return {
|
||||
startDate: dayjs(schedule.startDate),
|
||||
endDate,
|
||||
timeZone: schedule.timeZone,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const { dateRanges, oooExcludedDateRanges } = buildDateRanges({
|
||||
dateFrom,
|
||||
dateTo,
|
||||
availability,
|
||||
timeZone: finalTimezone,
|
||||
travelSchedules,
|
||||
outOfOffice: datesOutOfOffice,
|
||||
});
|
||||
|
||||
if (dateRanges.length === 0)
|
||||
return {
|
||||
busy: [],
|
||||
timeZone: finalTimezone,
|
||||
dateRanges: [],
|
||||
oooExcludedDateRanges: [],
|
||||
workingHours: [],
|
||||
dateOverrides: [],
|
||||
currentSeats: [],
|
||||
datesOutOfOffice: undefined,
|
||||
};
|
||||
|
||||
const bookingLimits =
|
||||
eventType?.bookingLimits &&
|
||||
typeof eventType.bookingLimits === "object" &&
|
||||
@@ -408,20 +534,6 @@ export class UserAvailabilityService {
|
||||
? user.allSelectedCalendars.filter((calendar) => calendar.eventTypeId === eventType.id)
|
||||
: user.userLevelSelectedCalendars;
|
||||
|
||||
let calendarTimezone: string | null = null;
|
||||
let finalTimezone: string | null = null;
|
||||
|
||||
if (!isTimezoneSet) {
|
||||
calendarTimezone = await this.getTimezoneFromDelegatedCalendars(user);
|
||||
if (calendarTimezone) {
|
||||
finalTimezone = calendarTimezone;
|
||||
}
|
||||
}
|
||||
|
||||
if (!finalTimezone) {
|
||||
finalTimezone = schedule.timeZone;
|
||||
}
|
||||
|
||||
let busyTimesFromLimits: EventBusyDetails[] = [];
|
||||
|
||||
if (initialData?.busyTimesFromLimits && initialData?.eventTypeForLimits) {
|
||||
@@ -514,106 +626,13 @@ export class UserAvailabilityService {
|
||||
...busyTimesFromTeamLimits,
|
||||
];
|
||||
|
||||
if (
|
||||
!(
|
||||
schedule?.availability ||
|
||||
(eventType?.availability.length ? eventType.availability : user.availability)
|
||||
)
|
||||
) {
|
||||
throw new HttpError({ statusCode: 400, message: ErrorCode.AvailabilityNotFoundInSchedule });
|
||||
}
|
||||
|
||||
const availability = (
|
||||
schedule?.availability || (eventType?.availability.length ? eventType.availability : user.availability)
|
||||
).map((a) => ({
|
||||
...a,
|
||||
userId: user.id,
|
||||
}));
|
||||
|
||||
const workingHours = getWorkingHours({ timeZone: finalTimezone }, availability);
|
||||
|
||||
const dateOverrides: TimeRange[] = [];
|
||||
// NOTE: getSchedule is currently calling this function for every user in a team event
|
||||
// but not using these values at all, wasting CPU. Adding this check here temporarily to avoid a larger refactor
|
||||
// since other callers do using this data.
|
||||
if (returnDateOverrides) {
|
||||
const calculateDateOverridesSpan = Sentry.startInactiveSpan({ name: "calculateDateOverrides" });
|
||||
const availabilityWithDates = availability.filter((availability) => !!availability.date);
|
||||
|
||||
for (let i = 0; i < availabilityWithDates.length; i++) {
|
||||
const override = availabilityWithDates[i];
|
||||
const startTime = dayjs.utc(override.startTime);
|
||||
const endTime = dayjs.utc(override.endTime);
|
||||
const overrideStartDate = dayjs.utc(override.date).hour(startTime.hour()).minute(startTime.minute());
|
||||
const overrideEndDate = dayjs.utc(override.date).hour(endTime.hour()).minute(endTime.minute());
|
||||
if (
|
||||
overrideStartDate.isBetween(dateFrom, dateTo, null, "[]") ||
|
||||
overrideEndDate.isBetween(dateFrom, dateTo, null, "[]")
|
||||
) {
|
||||
dateOverrides.push({
|
||||
start: overrideStartDate.toDate(),
|
||||
end: overrideEndDate.toDate(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
calculateDateOverridesSpan.end();
|
||||
}
|
||||
|
||||
const outOfOfficeDays =
|
||||
initialData?.outOfOfficeDays ??
|
||||
(await this.dependencies.oooRepo.findUserOOODays({
|
||||
userId: user.id,
|
||||
dateFrom: dateFrom.toISOString(),
|
||||
dateTo: dateTo.toISOString(),
|
||||
}));
|
||||
|
||||
const datesOutOfOffice: IOutOfOfficeData = this.calculateOutOfOfficeRanges(outOfOfficeDays, availability);
|
||||
|
||||
const holidayBlockedDates = await this.calculateHolidayBlockedDates(
|
||||
user.id,
|
||||
dateFrom.toDate(),
|
||||
dateTo.toDate(),
|
||||
availability
|
||||
log.debug(
|
||||
`EventType: ${eventTypeId} | User: ${username} (ID: ${userId}) - usingSchedule: ${safeStringify({
|
||||
chosenSchedule: schedule,
|
||||
eventTypeSchedule: eventType?.schedule,
|
||||
})}`
|
||||
);
|
||||
|
||||
for (const [date, holidayData] of Object.entries(holidayBlockedDates)) {
|
||||
if (!datesOutOfOffice[date]) {
|
||||
datesOutOfOffice[date] = holidayData;
|
||||
}
|
||||
}
|
||||
|
||||
const travelSchedules: {
|
||||
startDate: dayjs.Dayjs;
|
||||
endDate?: dayjs.Dayjs;
|
||||
timeZone: string;
|
||||
}[] = [];
|
||||
|
||||
if (isDefaultSchedule) {
|
||||
travelSchedules.push(
|
||||
...user.travelSchedules.map((schedule) => {
|
||||
let endDate: Dayjs | undefined;
|
||||
if (schedule.endDate) {
|
||||
endDate = dayjs(schedule.endDate);
|
||||
}
|
||||
return {
|
||||
startDate: dayjs(schedule.startDate),
|
||||
endDate,
|
||||
timeZone: schedule.timeZone,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const { dateRanges, oooExcludedDateRanges } = buildDateRanges({
|
||||
dateFrom,
|
||||
dateTo,
|
||||
availability,
|
||||
timeZone: finalTimezone,
|
||||
travelSchedules,
|
||||
outOfOffice: datesOutOfOffice,
|
||||
});
|
||||
|
||||
const formattedBusyTimes = detailedBusyTimes.map((busy) => ({
|
||||
start: dayjs(busy.start),
|
||||
end: dayjs(busy.end),
|
||||
|
||||
Reference in New Issue
Block a user