perf: improve getTotalBookingDuration query (#27383)

* perf: improve getTotalBookingDuration query

* perf: improve getTotalBookingDuration query

* test: add unit tests for getTotalBookingDurationForUsers and yearly duration pre-fetching

Co-Authored-By: udit@cal.com <udit222001@gmail.com>

* fix: integration tests

* fix: update tests

* fix: use vi.stubEnv instead of direct process.env modification for test isolation

Replace direct deletion of process.env variables with vi.stubEnv() to ensure
automatic restoration after tests, preventing flaky tests and side effects
in parallel execution.

Issue identified by Cubic AI (confidence: 9/10)

Co-Authored-By: unknown <>

* chore: add log

* refactor: imports

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Udit Takkar
2026-02-24 22:55:33 +05:30
committed by GitHub
co-authored by unknown Devin AI
parent e3a9f54ba5
commit f883dee084
5 changed files with 542 additions and 6 deletions
@@ -0,0 +1,213 @@
import { afterAll, beforeAll, describe, test, vi } from "vitest";
import { getAvailableSlotsService } from "@calcom/features/di/containers/AvailableSlots";
import { prisma } from "@calcom/prisma";
import type { EventType, Schedule, Team, User } from "@calcom/prisma/client";
import { BookingStatus, MembershipRole, SchedulingType } from "@calcom/prisma/enums";
import { expect } from "./expects";
describe("getSchedule duration limits (integration)", () => {
let user1: User;
let user2: User;
let team: Team;
let schedule1: Schedule;
let schedule2: Schedule;
let eventType: EventType;
const createdBookingIds: number[] = [];
beforeAll(async () => {
const timestamp = Date.now();
vi.stubEnv("UPSTASH_REDIS_REST_URL", undefined);
vi.stubEnv("UPSTASH_REDIS_REST_TOKEN", undefined);
[user1, user2] = await Promise.all([
prisma.user.create({
data: {
username: `dur-limit-user-1-${timestamp}`,
name: "Duration Limit User 1",
email: `dur-limit-user-1-${timestamp}@example.com`,
},
}),
prisma.user.create({
data: {
username: `dur-limit-user-2-${timestamp}`,
name: "Duration Limit User 2",
email: `dur-limit-user-2-${timestamp}@example.com`,
},
}),
]);
[schedule1, schedule2] = await Promise.all([
prisma.schedule.create({
data: {
name: `Schedule 1 ${timestamp}`,
userId: user1.id,
timeZone: "UTC",
},
}),
prisma.schedule.create({
data: {
name: `Schedule 2 ${timestamp}`,
userId: user2.id,
timeZone: "UTC",
},
}),
]);
await prisma.availability.createMany({
data: [
{
scheduleId: schedule1.id,
days: [0, 1, 2, 3, 4, 5, 6],
startTime: new Date("1970-01-01T09:00:00.000Z"),
endTime: new Date("1970-01-01T17:00:00.000Z"),
},
{
scheduleId: schedule2.id,
days: [0, 1, 2, 3, 4, 5, 6],
startTime: new Date("1970-01-01T09:00:00.000Z"),
endTime: new Date("1970-01-01T17:00:00.000Z"),
},
],
});
await Promise.all([
prisma.user.update({
where: { id: user1.id },
data: { defaultScheduleId: schedule1.id },
}),
prisma.user.update({
where: { id: user2.id },
data: { defaultScheduleId: schedule2.id },
}),
]);
team = await prisma.team.create({
data: {
name: `Duration Limit Team ${timestamp}`,
slug: `duration-limit-team-${timestamp}`,
},
});
await prisma.membership.createMany({
data: [
{
userId: user1.id,
teamId: team.id,
role: MembershipRole.ADMIN,
accepted: true,
},
{
userId: user2.id,
teamId: team.id,
role: MembershipRole.MEMBER,
accepted: true,
},
],
});
eventType = await prisma.eventType.create({
data: {
title: "Duration Limit Event",
slug: `duration-limit-event-${timestamp}`,
length: 30,
slotInterval: 30,
teamId: team.id,
userId: user1.id,
schedulingType: SchedulingType.ROUND_ROBIN,
durationLimits: { PER_YEAR: 480 },
users: {
connect: [{ id: user1.id }, { id: user2.id }],
},
hosts: {
createMany: {
data: [
{ userId: user1.id, isFixed: false },
{ userId: user2.id, isFixed: false },
],
},
},
},
});
const [booking1, booking2] = await Promise.all([
prisma.booking.create({
data: {
uid: `dur-limit-booking-1-${timestamp}`,
title: "Duration Limit Booking 1",
status: BookingStatus.ACCEPTED,
userId: user1.id,
eventTypeId: eventType.id,
startTime: new Date("2026-01-10T00:00:00.000Z"),
endTime: new Date("2026-01-10T06:40:00.000Z"), // 400 minutes
},
}),
prisma.booking.create({
data: {
uid: `dur-limit-booking-2-${timestamp}`,
title: "Duration Limit Booking 2",
status: BookingStatus.ACCEPTED,
userId: user2.id,
eventTypeId: eventType.id,
startTime: new Date("2026-01-11T00:00:00.000Z"),
endTime: new Date("2026-01-11T06:40:00.000Z"), // 400 minutes
},
}),
]);
createdBookingIds.push(booking1.id, booking2.id);
});
afterAll(async () => {
vi.useRealTimers();
if (createdBookingIds.length > 0) {
await prisma.booking.deleteMany({
where: { id: { in: createdBookingIds } },
});
}
if (eventType?.id) {
await prisma.eventType.delete({
where: { id: eventType.id },
});
}
await prisma.availability.deleteMany({
where: { scheduleId: { in: [schedule1?.id, schedule2?.id].filter(Boolean) } },
});
await prisma.schedule.deleteMany({
where: { id: { in: [schedule1?.id, schedule2?.id].filter(Boolean) } },
});
await prisma.membership.deleteMany({
where: { teamId: team?.id },
});
if (team?.id) {
await prisma.team.delete({
where: { id: team.id },
});
}
await prisma.user.deleteMany({
where: { id: { in: [user1?.id, user2?.id].filter(Boolean) } },
});
});
test("yearly duration limit is enforced event-type-wide", async () => {
vi.setSystemTime("2026-02-01T00:00:00Z");
const dateString = "2026-02-10";
const availableSlotsService = getAvailableSlotsService();
const scheduleForEvent = await availableSlotsService.getAvailableSlots({
input: {
eventTypeId: eventType.id,
eventTypeSlug: "",
startTime: `${dateString}T00:00:00.000Z`,
endTime: `${dateString}T23:59:59.999Z`,
timeZone: "UTC",
isTeamEvent: true,
orgSlug: null,
},
});
expect(scheduleForEvent).toHaveDateDisabled({ dateString });
});
});
@@ -0,0 +1,61 @@
import type { PrismaClient } from "@calcom/prisma";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { BookingRepository } from "./BookingRepository";
describe("BookingRepository", () => {
let repository: BookingRepository;
let mockPrismaClient: {
$queryRaw: ReturnType<typeof vi.fn>;
};
beforeEach(() => {
vi.clearAllMocks();
mockPrismaClient = {
$queryRaw: vi.fn(),
};
repository = new BookingRepository(mockPrismaClient as unknown as PrismaClient);
});
describe("getTotalBookingDuration", () => {
it("should return total minutes from the database result", async () => {
mockPrismaClient.$queryRaw.mockResolvedValue([{ totalMinutes: 120 }]);
const result = await repository.getTotalBookingDuration({
eventId: 52,
startDate: new Date("2026-01-01"),
endDate: new Date("2026-12-31"),
});
expect(result).toBe(120);
expect(mockPrismaClient.$queryRaw).toHaveBeenCalledTimes(1);
});
it("should return 0 when totalMinutes is null", async () => {
mockPrismaClient.$queryRaw.mockResolvedValue([{ totalMinutes: null }]);
const result = await repository.getTotalBookingDuration({
eventId: 52,
startDate: new Date("2026-01-01"),
endDate: new Date("2026-12-31"),
});
expect(result).toBe(0);
});
it("should call query when rescheduleUid is provided", async () => {
mockPrismaClient.$queryRaw.mockResolvedValue([{ totalMinutes: 90 }]);
const result = await repository.getTotalBookingDuration({
eventId: 52,
startDate: new Date("2026-01-01"),
endDate: new Date("2026-12-31"),
rescheduleUid: "existing-booking-uid",
});
expect(result).toBe(90);
expect(mockPrismaClient.$queryRaw).toHaveBeenCalledTimes(1);
});
});
});
@@ -1497,6 +1497,7 @@ export class BookingRepository implements IBookingRepository {
AND "endTime" <= ${endDate};
`;
}
return totalBookingTime.totalMinutes ?? 0;
}
@@ -0,0 +1,232 @@
import dayjs from "@calcom/dayjs";
import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { IAvailableSlotsService } from "./util";
import { AvailableSlotsService } from "./util";
describe("AvailableSlotsService - _getBusyTimesFromLimitsForUsers", () => {
type BusyTimesFromLimitsForUsers = typeof AvailableSlotsService.prototype._getBusyTimesFromLimitsForUsers;
let service: AvailableSlotsService;
let mockDependencies: {
bookingRepo: {
getTotalBookingDuration: ReturnType<typeof vi.fn>;
};
busyTimesService: {
getStartEndDateforLimitCheck: ReturnType<typeof vi.fn>;
getBusyTimesForLimitChecks: ReturnType<typeof vi.fn>;
};
userAvailabilityService: {
getPeriodStartDatesBetween: ReturnType<typeof vi.fn>;
};
checkBookingLimitsService: {
checkBookingLimit: ReturnType<typeof vi.fn>;
};
};
beforeEach(() => {
vi.clearAllMocks();
mockDependencies = {
bookingRepo: {
getTotalBookingDuration: vi.fn(),
},
busyTimesService: {
getStartEndDateforLimitCheck: vi.fn().mockReturnValue({
limitDateFrom: dayjs("2026-01-01"),
limitDateTo: dayjs("2026-12-31"),
}),
getBusyTimesForLimitChecks: vi.fn().mockResolvedValue([]),
},
userAvailabilityService: {
getPeriodStartDatesBetween: vi.fn().mockReturnValue([dayjs("2026-01-01")]),
},
checkBookingLimitsService: {
checkBookingLimit: vi.fn(),
},
};
service = new AvailableSlotsService(mockDependencies as unknown as IAvailableSlotsService);
});
describe("yearly duration limit pre-fetching optimization", () => {
const users = [
{ id: 29, email: "user29@example.com" },
{ id: 31, email: "user31@example.com" },
];
const eventType = { id: 52, length: 30 };
const dateFrom = dayjs("2026-01-01");
const dateTo = dayjs("2026-12-31");
const timeZone = "UTC";
it("should call getTotalBookingDuration once for yearly duration limits instead of N times", async () => {
const durationLimits: IntervalLimit = { PER_YEAR: 480 };
mockDependencies.bookingRepo.getTotalBookingDuration.mockResolvedValue(180);
await (
service as unknown as { _getBusyTimesFromLimitsForUsers: BusyTimesFromLimitsForUsers }
)._getBusyTimesFromLimitsForUsers(
users,
null,
durationLimits,
dateFrom,
dateTo,
30,
eventType,
timeZone
);
expect(mockDependencies.bookingRepo.getTotalBookingDuration).toHaveBeenCalledTimes(1);
expect(mockDependencies.bookingRepo.getTotalBookingDuration).toHaveBeenCalledWith({
eventId: 52,
startDate: expect.any(Date),
endDate: expect.any(Date),
rescheduleUid: undefined,
});
});
it("should not call getTotalBookingDuration when no PER_YEAR duration limit", async () => {
const durationLimits: IntervalLimit = { PER_DAY: 120 };
await (
service as unknown as { _getBusyTimesFromLimitsForUsers: BusyTimesFromLimitsForUsers }
)._getBusyTimesFromLimitsForUsers(
users,
null,
durationLimits,
dateFrom,
dateTo,
30,
eventType,
timeZone
);
expect(mockDependencies.bookingRepo.getTotalBookingDuration).not.toHaveBeenCalled();
});
it("should not call getTotalBookingDuration when no duration limits at all", async () => {
await (
service as unknown as { _getBusyTimesFromLimitsForUsers: BusyTimesFromLimitsForUsers }
)._getBusyTimesFromLimitsForUsers(users, null, null, dateFrom, dateTo, 30, eventType, timeZone);
expect(mockDependencies.bookingRepo.getTotalBookingDuration).not.toHaveBeenCalled();
});
it("should pass rescheduleUid to getTotalBookingDuration when provided", async () => {
const durationLimits: IntervalLimit = { PER_YEAR: 480 };
const rescheduleUid = "existing-booking-uid";
mockDependencies.bookingRepo.getTotalBookingDuration.mockResolvedValue(0);
await (
service as unknown as { _getBusyTimesFromLimitsForUsers: BusyTimesFromLimitsForUsers }
)._getBusyTimesFromLimitsForUsers(
users,
null,
durationLimits,
dateFrom,
dateTo,
30,
eventType,
timeZone,
rescheduleUid
);
expect(mockDependencies.bookingRepo.getTotalBookingDuration).toHaveBeenCalledWith(
expect.objectContaining({
rescheduleUid: "existing-booking-uid",
})
);
});
it("should call getTotalBookingDuration for each year when date range spans multiple years", async () => {
const durationLimits: IntervalLimit = { PER_YEAR: 480 };
const multiYearDateFrom = dayjs("2025-12-01");
const multiYearDateTo = dayjs("2026-02-28");
mockDependencies.userAvailabilityService.getPeriodStartDatesBetween.mockReturnValue([
dayjs("2025-01-01"),
dayjs("2026-01-01"),
]);
mockDependencies.bookingRepo.getTotalBookingDuration.mockResolvedValue(0);
await (
service as unknown as { _getBusyTimesFromLimitsForUsers: BusyTimesFromLimitsForUsers }
)._getBusyTimesFromLimitsForUsers(
users,
null,
durationLimits,
multiYearDateFrom,
multiYearDateTo,
30,
eventType,
timeZone
);
expect(mockDependencies.bookingRepo.getTotalBookingDuration).toHaveBeenCalledTimes(2);
});
it("should mark user as busy when yearly duration limit is exceeded", async () => {
const durationLimits: IntervalLimit = { PER_YEAR: 100 };
mockDependencies.bookingRepo.getTotalBookingDuration.mockResolvedValue(90);
const result = await (
service as unknown as { _getBusyTimesFromLimitsForUsers: BusyTimesFromLimitsForUsers }
)._getBusyTimesFromLimitsForUsers(
users,
null,
durationLimits,
dateFrom,
dateTo,
30,
eventType,
timeZone
);
expect(result.get(29)).toBeDefined();
expect(result.get(29)?.length).toBeGreaterThan(0);
});
it("should not mark user as busy when yearly duration limit is not exceeded", async () => {
const durationLimits: IntervalLimit = { PER_YEAR: 500 };
mockDependencies.bookingRepo.getTotalBookingDuration.mockResolvedValue(120);
const result = await (
service as unknown as { _getBusyTimesFromLimitsForUsers: BusyTimesFromLimitsForUsers }
)._getBusyTimesFromLimitsForUsers(
users,
null,
durationLimits,
dateFrom,
dateTo,
30,
eventType,
timeZone
);
expect(result.get(29) ?? []).toHaveLength(0);
expect(result.get(31) ?? []).toHaveLength(0);
});
it("should handle users with no bookings (0 duration) correctly", async () => {
const durationLimits: IntervalLimit = { PER_YEAR: 100 };
mockDependencies.bookingRepo.getTotalBookingDuration.mockResolvedValue(0);
const result = await (
service as unknown as { _getBusyTimesFromLimitsForUsers: BusyTimesFromLimitsForUsers }
)._getBusyTimesFromLimitsForUsers(
users,
null,
durationLimits,
dateFrom,
dateTo,
30,
eventType,
timeZone
);
expect(result.get(29) ?? []).toHaveLength(0);
expect(result.get(31) ?? []).toHaveLength(0);
});
});
});
@@ -459,6 +459,28 @@ export class AvailableSlotsService {
}
}
// Pre-fetch yearly duration totals per event type to avoid N+1 queries
const yearlyDurationTotals = new Map<string, number>();
const missingYearlyDurationTotals = new Set<string>();
if (durationLimits?.PER_YEAR) {
const yearPeriodStartDates = this.dependencies.userAvailabilityService.getPeriodStartDatesBetween(
dateFrom,
dateTo,
"year",
timeZone
);
for (const periodStart of yearPeriodStartDates) {
const yearKey = periodStart.format("YYYY");
const totalDurationForYear = await this.dependencies.bookingRepo.getTotalBookingDuration({
eventId: eventType.id,
startDate: periodStart.toDate(),
endDate: periodStart.endOf("year").toDate(),
rescheduleUid,
});
yearlyDurationTotals.set(yearKey, totalDurationForYear);
}
}
for (const user of users) {
const userBookings = busyTimesFromLimitsBookings.filter((booking) => booking.userId === user.id);
const limitManager = new LimitManager();
@@ -545,12 +567,19 @@ export class AvailableSlotsService {
}
if (unit === "year") {
const totalYearlyDuration = await this.dependencies.bookingRepo.getTotalBookingDuration({
eventId: eventType.id,
startDate: periodStart.toDate(),
endDate: periodStart.endOf(unit).toDate(),
rescheduleUid,
});
const yearKey = periodStart.format("YYYY");
if (!yearlyDurationTotals.has(yearKey) && !missingYearlyDurationTotals.has(yearKey)) {
missingYearlyDurationTotals.add(yearKey);
log.warn("[DURATION LIMIT CACHE MISS] Missing pre-fetched yearly duration total", {
eventTypeId: eventType.id,
durationLimitKey: key,
yearKey,
dateFrom: dateFrom.toISOString(),
dateTo: dateTo.toISOString(),
timeZone,
});
}
const totalYearlyDuration = yearlyDurationTotals.get(yearKey) ?? 0;
if (totalYearlyDuration + selectedDuration > limit) {
limitManager.addBusyTime(periodStart, unit, timeZone);
if (