From cee319570841c2f162ec8aa73821d1a790e50c33 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Tue, 13 Jan 2026 14:23:10 +0530 Subject: [PATCH] fix: optimize routing form mutation to avoid costly name update triggers (#26781) * fix: optimize routing form mutation to avoid costly name update triggers Co-Authored-By: hariom@cal.com * Add load test script for form responses --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../routing-forms/formMutation.handler.ts | 8 +- scripts/seed-load-test-form-reponses.ts | 91 +++++++++++++++++++ 2 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 scripts/seed-load-test-form-reponses.ts diff --git a/packages/trpc/server/routers/apps/routing-forms/formMutation.handler.ts b/packages/trpc/server/routers/apps/routing-forms/formMutation.handler.ts index c114336ad0..cbe6d6dbdd 100644 --- a/packages/trpc/server/routers/apps/routing-forms/formMutation.handler.ts +++ b/packages/trpc/server/routers/apps/routing-forms/formMutation.handler.ts @@ -37,7 +37,7 @@ export const formMutationHandler = async ({ ctx, input }: FormMutationHandlerOpt const existingForm = await prisma.app_RoutingForms_Form.findUnique({ where: { id }, - select: { id: true }, + select: { id: true, name: true }, }); if (existingForm) { // Check PBAC permissions for updating routing forms only @@ -144,6 +144,8 @@ export const formMutationHandler = async ({ ctx, input }: FormMutationHandlerOpt delete settings.sendUpdatesTo; } + const oldFormName = existingForm?.name; + return await prisma.app_RoutingForms_Form.upsert({ where: { id: id, @@ -173,7 +175,9 @@ export const formMutationHandler = async ({ ctx, input }: FormMutationHandlerOpt update: { disabled: disabled, fields, - name: name, + // Don't update name field if the name is the same as the old name to ensure that it doesn't change "name" update triggers which are very costly. + // This is an immediate quick workaround. Correct fix is to decouple trigger related work from the transaction. + name: oldFormName !== name ? name : undefined, description, settings: settings === null ? Prisma.JsonNull : settings, routes: routes === null ? Prisma.JsonNull : routes, diff --git a/scripts/seed-load-test-form-reponses.ts b/scripts/seed-load-test-form-reponses.ts new file mode 100644 index 0000000000..394028e424 --- /dev/null +++ b/scripts/seed-load-test-form-reponses.ts @@ -0,0 +1,91 @@ +/** + * Run this script quickly as `npx dotenv -e .env -- npx tsx scripts/seed-load-test-form-reponses.ts` from the root of the repo. + */ +import { prisma } from "@calcom/prisma"; +import type { App_RoutingForms_Form } from "@calcom/prisma/client"; + +async function createTestForm(): Promise { + const user = await prisma.user.findFirstOrThrow({ + where: { email: "owner1-acme@example.com" }, + }); + if (!user) { + throw new Error("owner1-acme@example.com not found"); + } + const testForm = + (await prisma.app_RoutingForms_Form.findFirst({ + where: { name: "Load Test Form" }, + })) || + (await prisma.app_RoutingForms_Form.create({ + data: { + name: "Load Test Form", + userId: user.id, + fields: [ + { + id: "test-field-1", + type: "text", + label: "Test Field", + required: true, + }, + ], + routes: [ + { + id: "test-route-1", + action: { type: "customPageMessage", value: "Thank you" }, + isFallback: true, + queryValue: { id: "test-route-1", type: "group" }, + }, + ], + }, + })); + + return testForm; +} + +/** + * Creates a form in Acme's owner-1 account and creates 300,000 responses in it. + */ +async function createBulkResponsesAndExport(): Promise { + const testForm = await createTestForm(); + console.log(`Using form: ${testForm.id}`); + + const TOTAL_RESPONSES = 300000; + const BATCH_SIZE = 1000; + const NUM_BATCHES = Math.ceil(TOTAL_RESPONSES / BATCH_SIZE); + + for (let batch = 0; batch < NUM_BATCHES; batch++) { + const batchStart = batch * BATCH_SIZE; + const batchEnd = Math.min((batch + 1) * BATCH_SIZE, TOTAL_RESPONSES); + const batchSize = batchEnd - batchStart; + + console.log(`Creating batch ${batch + 1}/${NUM_BATCHES} (responses ${batchStart + 1}-${batchEnd})...`); + + // Create responses in parallel within each batch + const responsePromises = Array.from({ length: batchSize }, (_, i) => { + const responseIndex = batchStart + i; + return prisma.app_RoutingForms_FormResponse.create({ + data: { + formFillerId: `load-test-filler-${responseIndex}`, + formId: testForm.id, + response: { + "test-field-1": { + label: "Test Field", + value: `Response ${responseIndex}`, + }, + }, + }, + }); + }); + + await Promise.all(responsePromises); + + console.log(`āœ“ Batch ${batch + 1} completed (${batchEnd}/${TOTAL_RESPONSES} total)`); + + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + console.log(`\nāœ“ Successfully created ${TOTAL_RESPONSES} responses`); +} + +createBulkResponsesAndExport() + .catch(console.error) + .finally(() => prisma.$disconnect());