perf: optimize getBusyTimesForLimitChecks with batched parallel queries (#27399)
* perf: optimize getBusyTimesForLimitChecks with batched parallel queries and raw SQL Co-Authored-By: udit@cal.com <udit222001@gmail.com> * test: add unit tests for getBusyTimesForLimitChecks batched query optimization Co-Authored-By: udit@cal.com <udit222001@gmail.com> * refactor: use Prisma findMany instead of raw SQL for better testability The batching (50 user chunks) and parallel execution (Promise.all) provide the main performance optimization. Using findMany instead of $queryRaw maintains the same performance characteristics while being compatible with prismock for testing. Co-Authored-By: udit@cal.com <udit222001@gmail.com> * fix: type error * refactor: add integration test * chore: remove log * chore: remove log * fix: avoid logging PII (userId) in busyTimes debug log Replace JSON.stringify(busyTimes) with count to avoid exposing userId in logs. Addresses Cubic AI review feedback (confidence 9/10). Co-Authored-By: unknown <> * chore: undo --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
unknown
Devin AI
parent
db76980644
commit
3908cda372
@@ -0,0 +1,160 @@
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { getBusyTimesService } from "@calcom/features/di/containers/BusyTimes";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
type CreatedResources = {
|
||||
users: number[];
|
||||
eventTypes: number[];
|
||||
bookings: number[];
|
||||
};
|
||||
|
||||
const createdResources: CreatedResources = {
|
||||
users: [],
|
||||
eventTypes: [],
|
||||
bookings: [],
|
||||
};
|
||||
|
||||
const createTestUser = async (overrides?: { email?: string; username?: string }) => {
|
||||
const timestamp = `${Date.now()}-${Math.random()}`;
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email: overrides?.email ?? `busy-times-${timestamp}@example.com`,
|
||||
username: overrides?.username ?? `busy-times-${timestamp}`,
|
||||
},
|
||||
});
|
||||
createdResources.users.push(user.id);
|
||||
return user;
|
||||
};
|
||||
|
||||
const createTestEventType = async (userId: number) => {
|
||||
const timestamp = `${Date.now()}-${Math.random()}`;
|
||||
const eventType = await prisma.eventType.create({
|
||||
data: {
|
||||
title: "Busy Times Test Event",
|
||||
slug: `busy-times-${timestamp}`,
|
||||
length: 30,
|
||||
userId,
|
||||
users: {
|
||||
connect: { id: userId },
|
||||
},
|
||||
},
|
||||
});
|
||||
createdResources.eventTypes.push(eventType.id);
|
||||
return eventType;
|
||||
};
|
||||
|
||||
const createTestBooking = async (params: {
|
||||
userId: number;
|
||||
eventTypeId: number;
|
||||
uid: string;
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
}) => {
|
||||
const booking = await prisma.booking.create({
|
||||
data: {
|
||||
userId: params.userId,
|
||||
eventTypeId: params.eventTypeId,
|
||||
uid: params.uid,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
startTime: params.startTime,
|
||||
endTime: params.endTime,
|
||||
title: "Busy Times Test Booking",
|
||||
},
|
||||
});
|
||||
createdResources.bookings.push(booking.id);
|
||||
return booking;
|
||||
};
|
||||
|
||||
afterEach(async () => {
|
||||
if (createdResources.bookings.length > 0) {
|
||||
await prisma.booking.deleteMany({
|
||||
where: { id: { in: createdResources.bookings } },
|
||||
});
|
||||
createdResources.bookings = [];
|
||||
}
|
||||
|
||||
if (createdResources.eventTypes.length > 0) {
|
||||
await prisma.eventType.deleteMany({
|
||||
where: { id: { in: createdResources.eventTypes } },
|
||||
});
|
||||
createdResources.eventTypes = [];
|
||||
}
|
||||
|
||||
if (createdResources.users.length > 0) {
|
||||
await prisma.user.deleteMany({
|
||||
where: { id: { in: createdResources.users } },
|
||||
});
|
||||
createdResources.users = [];
|
||||
}
|
||||
});
|
||||
|
||||
describe("getBusyTimesForLimitChecks (integration)", () => {
|
||||
it("returns bookings across batches for large user lists", async () => {
|
||||
const user1 = await createTestUser();
|
||||
const user2 = await createTestUser();
|
||||
const eventType = await createTestEventType(user1.id);
|
||||
|
||||
const dayStart = dayjs().add(1, "day").startOf("day");
|
||||
const dayEnd = dayStart.endOf("day");
|
||||
|
||||
await createTestBooking({
|
||||
userId: user1.id,
|
||||
eventTypeId: eventType.id,
|
||||
uid: `busy-times-${Date.now()}-1`,
|
||||
startTime: dayStart.set("hour", 9).toDate(),
|
||||
endTime: dayStart.set("hour", 10).toDate(),
|
||||
});
|
||||
|
||||
await createTestBooking({
|
||||
userId: user2.id,
|
||||
eventTypeId: eventType.id,
|
||||
uid: `busy-times-${Date.now()}-2`,
|
||||
startTime: dayStart.set("hour", 11).toDate(),
|
||||
endTime: dayStart.set("hour", 12).toDate(),
|
||||
});
|
||||
|
||||
const fillerIds = Array.from({ length: 58 }, (_, index) => 1_000_000 + index);
|
||||
const userIds = [user1.id, ...fillerIds, user2.id];
|
||||
|
||||
const busyTimes = await getBusyTimesService().getBusyTimesForLimitChecks({
|
||||
userIds,
|
||||
eventTypeId: eventType.id,
|
||||
startDate: dayStart.toISOString(),
|
||||
endDate: dayEnd.toISOString(),
|
||||
bookingLimits: { PER_DAY: 5 },
|
||||
});
|
||||
|
||||
expect(busyTimes).toHaveLength(2);
|
||||
expect(busyTimes.map((busyTime) => busyTime.userId).sort()).toEqual([user1.id, user2.id].sort());
|
||||
});
|
||||
|
||||
it("excludes rescheduleUid from results", async () => {
|
||||
const user = await createTestUser();
|
||||
const eventType = await createTestEventType(user.id);
|
||||
|
||||
const dayStart = dayjs().add(2, "day").startOf("day");
|
||||
const dayEnd = dayStart.endOf("day");
|
||||
const rescheduleUid = `busy-times-${Date.now()}-reschedule`;
|
||||
|
||||
await createTestBooking({
|
||||
userId: user.id,
|
||||
eventTypeId: eventType.id,
|
||||
uid: rescheduleUid,
|
||||
startTime: dayStart.set("hour", 9).toDate(),
|
||||
endTime: dayStart.set("hour", 10).toDate(),
|
||||
});
|
||||
|
||||
const busyTimes = await getBusyTimesService().getBusyTimesForLimitChecks({
|
||||
userIds: [user.id],
|
||||
eventTypeId: eventType.id,
|
||||
startDate: dayStart.toISOString(),
|
||||
endDate: dayEnd.toISOString(),
|
||||
rescheduleUid,
|
||||
bookingLimits: { PER_DAY: 5 },
|
||||
});
|
||||
|
||||
expect(busyTimes).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,10 @@
|
||||
import { prisma } from "@calcom/prisma/__mocks__/prisma";
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { getBusyTimesService } from "@calcom/features/di/containers/BusyTimes";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@calcom/prisma", () => ({
|
||||
default: prisma,
|
||||
prisma,
|
||||
}));
|
||||
|
||||
@@ -134,3 +133,252 @@ describe("getBusyTimes", () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBusyTimesForLimitChecks", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const createMockBookingResult = (
|
||||
overrides: Partial<{
|
||||
id: number;
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
eventTypeId: number | null;
|
||||
title: string;
|
||||
userId: number | null;
|
||||
}> = {}
|
||||
) => ({
|
||||
id: 1,
|
||||
startTime: startOfTomorrow.set("hour", 10).toDate(),
|
||||
endTime: startOfTomorrow.set("hour", 11).toDate(),
|
||||
eventTypeId: 1,
|
||||
title: "Test Booking",
|
||||
userId: 1,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
it("should return empty array when no booking or duration limits are provided", async () => {
|
||||
const busyTimesService = getBusyTimesService();
|
||||
const busyTimes = await busyTimesService.getBusyTimesForLimitChecks({
|
||||
userIds: [1, 2, 3],
|
||||
eventTypeId: 1,
|
||||
startDate: startOfTomorrow.format(),
|
||||
endDate: startOfTomorrow.endOf("day").format(),
|
||||
bookingLimits: null,
|
||||
durationLimits: null,
|
||||
});
|
||||
|
||||
expect(busyTimes).toEqual([]);
|
||||
expect(prisma.booking.findMany).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return empty array when userIds is empty", async () => {
|
||||
const busyTimesService = getBusyTimesService();
|
||||
const busyTimes = await busyTimesService.getBusyTimesForLimitChecks({
|
||||
userIds: [],
|
||||
eventTypeId: 1,
|
||||
startDate: startOfTomorrow.format(),
|
||||
endDate: startOfTomorrow.endOf("day").format(),
|
||||
bookingLimits: { PER_DAY: 5 },
|
||||
});
|
||||
|
||||
expect(busyTimes).toEqual([]);
|
||||
});
|
||||
|
||||
it("should fetch bookings for a single user with booking limits", async () => {
|
||||
const mockBooking = createMockBookingResult();
|
||||
vi.mocked(prisma.booking.findMany).mockResolvedValue([mockBooking]);
|
||||
|
||||
const busyTimesService = getBusyTimesService();
|
||||
const busyTimes = await busyTimesService.getBusyTimesForLimitChecks({
|
||||
userIds: [1],
|
||||
eventTypeId: 1,
|
||||
startDate: startOfTomorrow.format(),
|
||||
endDate: startOfTomorrow.endOf("day").format(),
|
||||
bookingLimits: { PER_DAY: 5 },
|
||||
});
|
||||
|
||||
expect(busyTimes).toHaveLength(1);
|
||||
expect(busyTimes[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
start: mockBooking.startTime,
|
||||
end: mockBooking.endTime,
|
||||
title: mockBooking.title,
|
||||
source: `eventType-${mockBooking.eventTypeId}-booking-${mockBooking.id}`,
|
||||
userId: mockBooking.userId,
|
||||
})
|
||||
);
|
||||
expect(prisma.booking.findMany).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should fetch bookings for multiple users with duration limits", async () => {
|
||||
const mockBookings = [
|
||||
createMockBookingResult({ id: 1, userId: 1 }),
|
||||
createMockBookingResult({
|
||||
id: 2,
|
||||
userId: 2,
|
||||
startTime: startOfTomorrow.set("hour", 14).toDate(),
|
||||
endTime: startOfTomorrow.set("hour", 15).toDate(),
|
||||
}),
|
||||
];
|
||||
vi.mocked(prisma.booking.findMany).mockResolvedValue(mockBookings);
|
||||
|
||||
const busyTimesService = getBusyTimesService();
|
||||
const busyTimes = await busyTimesService.getBusyTimesForLimitChecks({
|
||||
userIds: [1, 2],
|
||||
eventTypeId: 1,
|
||||
startDate: startOfTomorrow.format(),
|
||||
endDate: startOfTomorrow.endOf("day").format(),
|
||||
durationLimits: { PER_DAY: 120 },
|
||||
});
|
||||
|
||||
expect(busyTimes).toHaveLength(2);
|
||||
expect(busyTimes[0].userId).toBe(1);
|
||||
expect(busyTimes[1].userId).toBe(2);
|
||||
});
|
||||
|
||||
it("should batch queries when userIds exceeds batch size (50)", async () => {
|
||||
const userIds = Array.from({ length: 75 }, (_, i) => i + 1);
|
||||
const mockBookings = [createMockBookingResult()];
|
||||
vi.mocked(prisma.booking.findMany).mockResolvedValue(mockBookings);
|
||||
|
||||
const busyTimesService = getBusyTimesService();
|
||||
await busyTimesService.getBusyTimesForLimitChecks({
|
||||
userIds,
|
||||
eventTypeId: 1,
|
||||
startDate: startOfTomorrow.format(),
|
||||
endDate: startOfTomorrow.endOf("day").format(),
|
||||
bookingLimits: { PER_DAY: 5 },
|
||||
});
|
||||
|
||||
// Should be called twice: once for first 50 users, once for remaining 25
|
||||
expect(prisma.booking.findMany).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("should batch queries correctly for exactly 100 users (2 full batches)", async () => {
|
||||
const userIds = Array.from({ length: 100 }, (_, i) => i + 1);
|
||||
vi.mocked(prisma.booking.findMany).mockResolvedValue([]);
|
||||
|
||||
const busyTimesService = getBusyTimesService();
|
||||
await busyTimesService.getBusyTimesForLimitChecks({
|
||||
userIds,
|
||||
eventTypeId: 1,
|
||||
startDate: startOfTomorrow.format(),
|
||||
endDate: startOfTomorrow.endOf("day").format(),
|
||||
bookingLimits: { PER_WEEK: 10 },
|
||||
});
|
||||
|
||||
expect(prisma.booking.findMany).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("should batch queries correctly for 150 users (3 batches)", async () => {
|
||||
const userIds = Array.from({ length: 150 }, (_, i) => i + 1);
|
||||
vi.mocked(prisma.booking.findMany).mockResolvedValue([]);
|
||||
|
||||
const busyTimesService = getBusyTimesService();
|
||||
await busyTimesService.getBusyTimesForLimitChecks({
|
||||
userIds,
|
||||
eventTypeId: 1,
|
||||
startDate: startOfTomorrow.format(),
|
||||
endDate: startOfTomorrow.endOf("day").format(),
|
||||
bookingLimits: { PER_MONTH: 20 },
|
||||
});
|
||||
|
||||
expect(prisma.booking.findMany).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it("should merge results from multiple batches correctly", async () => {
|
||||
const userIds = Array.from({ length: 75 }, (_, i) => i + 1);
|
||||
|
||||
// First batch returns 2 bookings, second batch returns 1 booking
|
||||
vi.mocked(prisma.booking.findMany)
|
||||
.mockResolvedValueOnce([
|
||||
createMockBookingResult({ id: 1, userId: 1 }),
|
||||
createMockBookingResult({ id: 2, userId: 25 }),
|
||||
])
|
||||
.mockResolvedValueOnce([createMockBookingResult({ id: 3, userId: 60 })]);
|
||||
|
||||
const busyTimesService = getBusyTimesService();
|
||||
const busyTimes = await busyTimesService.getBusyTimesForLimitChecks({
|
||||
userIds,
|
||||
eventTypeId: 1,
|
||||
startDate: startOfTomorrow.format(),
|
||||
endDate: startOfTomorrow.endOf("day").format(),
|
||||
bookingLimits: { PER_DAY: 5 },
|
||||
});
|
||||
|
||||
expect(busyTimes).toHaveLength(3);
|
||||
expect(busyTimes.map((b) => b.userId)).toEqual([1, 25, 60]);
|
||||
});
|
||||
|
||||
it("should exclude rescheduleUid from results", async () => {
|
||||
const mockBooking = createMockBookingResult();
|
||||
vi.mocked(prisma.booking.findMany).mockResolvedValue([mockBooking]);
|
||||
|
||||
const busyTimesService = getBusyTimesService();
|
||||
await busyTimesService.getBusyTimesForLimitChecks({
|
||||
userIds: [1],
|
||||
eventTypeId: 1,
|
||||
startDate: startOfTomorrow.format(),
|
||||
endDate: startOfTomorrow.endOf("day").format(),
|
||||
rescheduleUid: "existing-booking-uid",
|
||||
bookingLimits: { PER_DAY: 5 },
|
||||
});
|
||||
|
||||
// Verify the query was called (the actual exclusion happens in the Prisma query)
|
||||
expect(prisma.booking.findMany).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should handle both booking and duration limits together", async () => {
|
||||
const mockBooking = createMockBookingResult();
|
||||
vi.mocked(prisma.booking.findMany).mockResolvedValue([mockBooking]);
|
||||
|
||||
const busyTimesService = getBusyTimesService();
|
||||
const busyTimes = await busyTimesService.getBusyTimesForLimitChecks({
|
||||
userIds: [1],
|
||||
eventTypeId: 1,
|
||||
startDate: startOfTomorrow.format(),
|
||||
endDate: startOfTomorrow.endOf("day").format(),
|
||||
bookingLimits: { PER_DAY: 5 },
|
||||
durationLimits: { PER_DAY: 120 },
|
||||
});
|
||||
|
||||
expect(busyTimes).toHaveLength(1);
|
||||
expect(prisma.booking.findMany).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should handle null eventTypeId in booking results", async () => {
|
||||
const mockBooking = createMockBookingResult({ eventTypeId: null });
|
||||
vi.mocked(prisma.booking.findMany).mockResolvedValue([mockBooking]);
|
||||
|
||||
const busyTimesService = getBusyTimesService();
|
||||
const busyTimes = await busyTimesService.getBusyTimesForLimitChecks({
|
||||
userIds: [1],
|
||||
eventTypeId: 1,
|
||||
startDate: startOfTomorrow.format(),
|
||||
endDate: startOfTomorrow.endOf("day").format(),
|
||||
bookingLimits: { PER_DAY: 5 },
|
||||
});
|
||||
|
||||
expect(busyTimes).toHaveLength(1);
|
||||
expect(busyTimes[0].source).toBe(`eventType-null-booking-${mockBooking.id}`);
|
||||
});
|
||||
|
||||
it("should handle null userId in booking results", async () => {
|
||||
const mockBooking = createMockBookingResult({ userId: null });
|
||||
vi.mocked(prisma.booking.findMany).mockResolvedValue([mockBooking]);
|
||||
|
||||
const busyTimesService = getBusyTimesService();
|
||||
const busyTimes = await busyTimesService.getBusyTimesForLimitChecks({
|
||||
userIds: [1],
|
||||
eventTypeId: 1,
|
||||
startDate: startOfTomorrow.format(),
|
||||
endDate: startOfTomorrow.endOf("day").format(),
|
||||
bookingLimits: { PER_DAY: 5 },
|
||||
});
|
||||
|
||||
expect(busyTimes).toHaveLength(1);
|
||||
expect(busyTimes[0].userId).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,13 +11,14 @@ import { getPiiFreeBooking } from "@calcom/lib/piiFreeData";
|
||||
import { withReporting } from "@calcom/lib/sentryWrapper";
|
||||
import { performance } from "@calcom/lib/server/perfObserver";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Booking, EventType } from "@calcom/prisma/client";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import type { SelectedCalendar } from "@calcom/prisma/client";
|
||||
import type { Booking, EventType, Prisma, SelectedCalendar } from "@calcom/prisma/client";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import type { CalendarFetchMode, EventBusyDetails } from "@calcom/types/Calendar";
|
||||
import type { CredentialForCalendarService } from "@calcom/types/Credential";
|
||||
|
||||
const BATCH_SIZE_FOR_LIMIT_CHECKS = 50;
|
||||
const MAX_CONCURRENT_LIMIT_CHECK_BATCHES = 5;
|
||||
|
||||
export interface IBusyTimesService {
|
||||
bookingRepo: BookingRepository;
|
||||
}
|
||||
@@ -323,7 +324,7 @@ export class BusyTimesService {
|
||||
|
||||
performance.mark("getBusyTimesForLimitChecksStart");
|
||||
|
||||
let busyTimes: EventBusyDetails[] = [];
|
||||
const busyTimes: EventBusyDetails[] = [];
|
||||
|
||||
if (!bookingLimits && !durationLimits) {
|
||||
return busyTimes;
|
||||
@@ -343,6 +344,120 @@ export class BusyTimesService {
|
||||
})}`
|
||||
);
|
||||
|
||||
const startTimeDate = limitDateFrom.toDate();
|
||||
const endTimeDate = limitDateTo.toDate();
|
||||
|
||||
const bookings = await this.fetchBookingsForLimitChecksBatched({
|
||||
userIds,
|
||||
eventTypeId,
|
||||
startTimeDate,
|
||||
endTimeDate,
|
||||
rescheduleUid,
|
||||
});
|
||||
|
||||
for (const booking of bookings) {
|
||||
busyTimes.push({
|
||||
start: new Date(booking.startTime),
|
||||
end: new Date(booking.endTime),
|
||||
title: booking.title,
|
||||
source: `eventType-${booking.eventTypeId}-booking-${booking.id}`,
|
||||
userId: booking.userId,
|
||||
});
|
||||
}
|
||||
|
||||
logger.silly(`Fetch limit checks bookings for eventId: ${eventTypeId} ${JSON.stringify(busyTimes)}`);
|
||||
performance.mark("getBusyTimesForLimitChecksEnd");
|
||||
performance.measure(
|
||||
`prisma booking get for limits took $1'`,
|
||||
"getBusyTimesForLimitChecksStart",
|
||||
"getBusyTimesForLimitChecksEnd"
|
||||
);
|
||||
return busyTimes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches bookings for limit checks using batched parallel queries.
|
||||
* This optimization improves performance for teams/orgs with many members by:
|
||||
* 1. Splitting large userIds arrays into smaller batches
|
||||
* 2. Running a capped number of batch queries in parallel
|
||||
*/
|
||||
private async fetchBookingsForLimitChecksBatched(params: {
|
||||
userIds: number[];
|
||||
eventTypeId: number;
|
||||
startTimeDate: Date;
|
||||
endTimeDate: Date;
|
||||
rescheduleUid?: string | null;
|
||||
}): Promise<
|
||||
Array<{
|
||||
id: number;
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
eventTypeId: number | null;
|
||||
title: string;
|
||||
userId: number | null;
|
||||
}>
|
||||
> {
|
||||
const { userIds, eventTypeId, startTimeDate, endTimeDate, rescheduleUid } = params;
|
||||
|
||||
if (userIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const batches: number[][] = [];
|
||||
for (let i = 0; i < userIds.length; i += BATCH_SIZE_FOR_LIMIT_CHECKS) {
|
||||
batches.push(userIds.slice(i, i + BATCH_SIZE_FOR_LIMIT_CHECKS));
|
||||
}
|
||||
|
||||
const results: Array<{
|
||||
id: number;
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
eventTypeId: number | null;
|
||||
title: string;
|
||||
userId: number | null;
|
||||
}> = [];
|
||||
|
||||
for (let i = 0; i < batches.length; i += MAX_CONCURRENT_LIMIT_CHECK_BATCHES) {
|
||||
const currentBatch = batches.slice(i, i + MAX_CONCURRENT_LIMIT_CHECK_BATCHES);
|
||||
const batchResults = await Promise.all(
|
||||
currentBatch.map((batchUserIds) =>
|
||||
this.fetchBookingsForLimitChecksBatch({
|
||||
userIds: batchUserIds,
|
||||
eventTypeId,
|
||||
startTimeDate,
|
||||
endTimeDate,
|
||||
rescheduleUid,
|
||||
})
|
||||
)
|
||||
);
|
||||
results.push(...batchResults.flat());
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches bookings for a single batch of userIds using Prisma's findMany.
|
||||
* Uses batching to improve query planner efficiency for large userIds arrays.
|
||||
*/
|
||||
private async fetchBookingsForLimitChecksBatch(params: {
|
||||
userIds: number[];
|
||||
eventTypeId: number;
|
||||
startTimeDate: Date;
|
||||
endTimeDate: Date;
|
||||
rescheduleUid?: string | null;
|
||||
}): Promise<
|
||||
Array<{
|
||||
id: number;
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
eventTypeId: number | null;
|
||||
title: string;
|
||||
userId: number | null;
|
||||
}>
|
||||
> {
|
||||
const { userIds, eventTypeId, startTimeDate, endTimeDate, rescheduleUid } = params;
|
||||
|
||||
const where: Prisma.BookingWhereInput = {
|
||||
userId: {
|
||||
in: userIds,
|
||||
@@ -351,10 +466,10 @@ export class BusyTimesService {
|
||||
status: BookingStatus.ACCEPTED,
|
||||
// FIXME: bookings that overlap on one side will never be counted
|
||||
startTime: {
|
||||
gte: limitDateFrom.toDate(),
|
||||
gte: startTimeDate,
|
||||
},
|
||||
endTime: {
|
||||
lte: limitDateTo.toDate(),
|
||||
lte: endTimeDate,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -370,31 +485,12 @@ export class BusyTimesService {
|
||||
id: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
eventType: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
eventTypeId: true,
|
||||
title: true,
|
||||
userId: true,
|
||||
},
|
||||
});
|
||||
|
||||
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)}`);
|
||||
performance.mark("getBusyTimesForLimitChecksEnd");
|
||||
performance.measure(
|
||||
`prisma booking get for limits took $1'`,
|
||||
"getBusyTimesForLimitChecksStart",
|
||||
"getBusyTimesForLimitChecksEnd"
|
||||
);
|
||||
return busyTimes;
|
||||
return bookings;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user