From de624b5c54f777fedde3ca4d8afe18564087f2ad Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Tue, 3 Mar 2026 14:04:03 -0500 Subject: [PATCH] fix: add bigTransactionTimeout and retryOnDeadlock to delete operations deleteRecord and deleteRecords in RecordDeleteService use $tx but with the default Prisma transaction timeout (5s) and no deadlock retry. With FOR UPDATE locking in the computed orchestrator, delete operations can deadlock with concurrent updates touching the same records. Large batch deletes with cascading link changes can also exceed the 5s default timeout. This is inconsistent with updateRecords and multipleCreateRecords which both use bigTransactionTimeout and @retryOnDeadlock. This fix: - Adds @retryOnDeadlock() to deleteRecord and deleteRecords in RecordOpenApiService - Injects ThresholdConfig into RecordDeleteService - Passes bigTransactionTimeout to the $tx call in deleteRecords Co-Authored-By: Claude Opus 4.6 (1M context) --- .../record/open-api/record-open-api.service.ts | 2 ++ .../record/record-modify/record-delete.service.ts | 12 +++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/nestjs-backend/src/features/record/open-api/record-open-api.service.ts b/apps/nestjs-backend/src/features/record/open-api/record-open-api.service.ts index 1fa9e0d28b..e5b9aa4e63 100644 --- a/apps/nestjs-backend/src/features/record/open-api/record-open-api.service.ts +++ b/apps/nestjs-backend/src/features/record/open-api/record-open-api.service.ts @@ -204,10 +204,12 @@ export class RecordOpenApiService { return snapshots[0].data; } + @retryOnDeadlock() async deleteRecord(tableId: string, recordId: string, windowId?: string) { return this.recordModifyService.deleteRecord(tableId, recordId, windowId); } + @retryOnDeadlock() async deleteRecords(tableId: string, recordIds: string[], windowId?: string) { return this.recordModifyService.deleteRecords(tableId, recordIds, windowId); } diff --git a/apps/nestjs-backend/src/features/record/record-modify/record-delete.service.ts b/apps/nestjs-backend/src/features/record/record-modify/record-delete.service.ts index 613817458d..a3352dc970 100644 --- a/apps/nestjs-backend/src/features/record/record-modify/record-delete.service.ts +++ b/apps/nestjs-backend/src/features/record/record-modify/record-delete.service.ts @@ -2,6 +2,8 @@ import { Injectable } from '@nestjs/common'; import { generateOperationId } from '@teable/core'; import { PrismaService } from '@teable/db-main-prisma'; import { ClsService } from 'nestjs-cls'; +import type { IThresholdConfig } from '../../../configs/threshold.config'; +import { ThresholdConfig } from '../../../configs/threshold.config'; import { EventEmitterService } from '../../../event-emitter/event-emitter.service'; import { Events } from '../../../event-emitter/events'; import type { IClsStore } from '../../../types/cls'; @@ -19,7 +21,8 @@ export class RecordDeleteService { private readonly eventEmitterService: EventEmitterService, private readonly computedOrchestrator: ComputedOrchestratorService, private readonly tableDomainQueryService: TableDomainQueryService, - private readonly cls: ClsService + private readonly cls: ClsService, + @ThresholdConfig() private readonly thresholdConfig: IThresholdConfig ) {} async deleteRecord(tableId: string, recordId: string, windowId?: string) { @@ -29,7 +32,8 @@ export class RecordDeleteService { async deleteRecords(tableId: string, recordIds: string[], windowId?: string) { const table = await this.tableDomainQueryService.getTableDomainById(tableId); - const { records: recordsForEvent, orders } = await this.prismaService.$tx(async () => { + const { records: recordsForEvent, orders } = await this.prismaService.$tx( + async () => { // Use a base-table query to ensure link values are derived from junction tables. const recordsForEvent = await this.recordService.getRecordsById( tableId, @@ -71,7 +75,9 @@ export class RecordDeleteService { }); return { records: recordsForEvent, orders }; - }); + }, + { timeout: this.thresholdConfig.bigTransactionTimeout } + ); this.eventEmitterService.emitAsync(Events.OPERATION_RECORDS_DELETE, { operationId: generateOperationId(),