Skip to content

Commit 5bdc832

Browse files
authored
feat: separate unique violation retry from deadlock retry (#1674)
* feat: separate unique violation retry from deadlock retry * fix: error import
1 parent e89fe8e commit 5bdc832

6 files changed

Lines changed: 199 additions & 64 deletions

File tree

apps/nestjs-backend/src/features/record/open-api/record-open-api.service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { IThresholdConfig, ThresholdConfig } from '../../../configs/threshold.co
2020
import { EventEmitterService } from '../../../event-emitter/event-emitter.service';
2121
import { Events } from '../../../event-emitter/events';
2222
import type { IClsStore } from '../../../types/cls';
23-
import { retryOnDeadlock } from '../../../utils/retry-on-dead-lock';
23+
import { retryOnDeadlock, retryOnUniqueViolation } from '../../../utils/retry-decorator';
2424
import { AttachmentsStorageService } from '../../attachments/attachments-storage.service';
2525
import { AttachmentsService } from '../../attachments/attachments.service';
2626
import { getPublicFullStorageUrl } from '../../attachments/plugins/utils';
@@ -54,6 +54,7 @@ export class RecordOpenApiService {
5454
) {}
5555

5656
@retryOnDeadlock()
57+
@retryOnUniqueViolation()
5758
async multipleCreateRecords(
5859
tableId: string,
5960
createRecordsRo: ICreateRecordsRo,
@@ -263,6 +264,7 @@ export class RecordOpenApiService {
263264
}
264265

265266
@retryOnDeadlock()
267+
@retryOnUniqueViolation()
266268
async updateRecords(
267269
tableId: string,
268270
updateRecordsRo: IUpdateRecordsRo & {

apps/nestjs-backend/src/utils/retry-on-dead-lock.spec.ts renamed to apps/nestjs-backend/src/utils/retry-decorator.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable sonarjs/no-identical-functions */
22
import { Prisma } from '@prisma/client';
3-
import { retryOnDeadlock } from './retry-on-dead-lock';
3+
import { retryOnDeadlock } from './retry-decorator';
44

55
class TestService {
66
@retryOnDeadlock()
@@ -37,7 +37,7 @@ describe('RetryOnDeadlock Decorator', () => {
3737
thresholdConfig: () => ({
3838
dbDeadlock: {
3939
maxRetries: 3,
40-
initialBackoff: 300,
40+
initialBackoff: 200,
4141
jitter: 1,
4242
},
4343
}),
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { Logger } from '@nestjs/common';
2+
import { HttpErrorCode } from '@teable/core';
3+
import { thresholdConfig } from '../configs/threshold.config';
4+
import { CustomHttpException } from '../custom.exception';
5+
6+
interface IRetryOptions {
7+
maxRetries?: number;
8+
initialBackoff?: number;
9+
jitter?: number;
10+
}
11+
12+
interface IRetryConfig {
13+
errorCodes: string[];
14+
errorMessage: string;
15+
errorCode: HttpErrorCode;
16+
loggerName: string;
17+
}
18+
19+
function createRetryDecorator(config: IRetryConfig) {
20+
const logger = new Logger(config.loggerName);
21+
22+
return function (opt?: IRetryOptions) {
23+
const { dbDeadlock } = thresholdConfig();
24+
const {
25+
maxRetries = dbDeadlock.maxRetries,
26+
initialBackoff = dbDeadlock.initialBackoff,
27+
jitter = dbDeadlock.jitter,
28+
} = opt ?? {};
29+
30+
return function (_target: unknown, _propertyKey: string, descriptor: PropertyDescriptor) {
31+
const originalMethod = descriptor.value;
32+
33+
descriptor.value = async function (...args: unknown[]) {
34+
let retries = 0;
35+
let backoff = initialBackoff + Math.random() * jitter;
36+
37+
while (retries <= maxRetries) {
38+
try {
39+
return await originalMethod.apply(this, args);
40+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
41+
} catch (error: any) {
42+
const { errorCodes, errorMessage, errorCode } = config;
43+
if (
44+
errorCodes.includes(error.code) ||
45+
(error.meta?.code && errorCodes.includes(error.meta.code as string))
46+
) {
47+
if (retries === maxRetries) {
48+
logger.error(`${errorMessage} after ${retries} retries`, error.stack);
49+
throw new CustomHttpException(errorMessage, errorCode);
50+
}
51+
await new Promise((resolve) => setTimeout(resolve, backoff));
52+
backoff *= 1.5 + Math.random() * jitter;
53+
} else {
54+
throw error;
55+
}
56+
}
57+
retries++;
58+
}
59+
};
60+
61+
return descriptor;
62+
};
63+
};
64+
}
65+
66+
export const retryOnDeadlock = createRetryDecorator({
67+
errorCodes: ['40P01', 'P2034'],
68+
errorMessage: 'Database deadlock detected',
69+
errorCode: HttpErrorCode.DATABASE_CONNECTION_UNAVAILABLE,
70+
loggerName: 'DeadlockRetryDecorator',
71+
});
72+
73+
export const retryOnUniqueViolation = createRetryDecorator({
74+
errorCodes: ['23505'],
75+
errorMessage: 'Database unique violation detected',
76+
errorCode: HttpErrorCode.DATABASE_CONNECTION_UNAVAILABLE,
77+
loggerName: 'UniqueViolationRetryDecorator',
78+
});

apps/nestjs-backend/src/utils/retry-on-dead-lock.ts

Lines changed: 0 additions & 59 deletions
This file was deleted.

apps/nestjs-backend/test/dead-lock.e2e-spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import type { INestApplication } from '@nestjs/common';
33
import { DriverClient } from '@teable/core';
44
import { Prisma, PrismaService } from '@teable/db-main-prisma';
5-
import { retryOnDeadlock } from '../src/utils/retry-on-dead-lock';
5+
import { retryOnDeadlock } from '../src/utils/retry-decorator';
66
import { initApp } from './utils/init-app';
77

88
const deadLockTableA = 'dead_lock_a';

apps/nestjs-backend/test/record.e2e-spec.ts

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable sonarjs/no-duplicate-string */
22
import type { INestApplication } from '@nestjs/common';
3-
import type { IFieldRo, ISelectFieldOptions } from '@teable/core';
3+
import type { IFieldRo, IFieldVo, ISelectFieldOptions } from '@teable/core';
44
import { CellFormat, DriverClient, FieldKeyType, FieldType, Relationship } from '@teable/core';
55
import { updateRecords, type ITableFullVo } from '@teable/openapi';
66
import {
@@ -910,4 +910,118 @@ describe('OpenAPI RecordController (e2e)', () => {
910910
expect(records).toBeDefined();
911911
});
912912
});
913+
914+
describe('ops index conflict', () => {
915+
let table: ITableFullVo;
916+
let tableLinkField: IFieldVo;
917+
let linkTable: ITableFullVo;
918+
beforeAll(async () => {
919+
table = await createTable(baseId, {
920+
name: 'table1',
921+
fields: [
922+
{
923+
type: FieldType.SingleLineText,
924+
name: 'field1',
925+
},
926+
],
927+
});
928+
linkTable = await createTable(baseId, {
929+
name: 'linkTable',
930+
fields: [
931+
{
932+
type: FieldType.SingleLineText,
933+
name: 'field1',
934+
},
935+
],
936+
records: [
937+
{
938+
fields: {
939+
field1: 'test1',
940+
},
941+
},
942+
{
943+
fields: {
944+
field1: 'test2',
945+
},
946+
},
947+
{
948+
fields: {
949+
field1: 'test3',
950+
},
951+
},
952+
{
953+
fields: {
954+
field1: 'test4',
955+
},
956+
},
957+
],
958+
});
959+
tableLinkField = await createField(table.id, {
960+
name: 'linkField',
961+
type: FieldType.Link,
962+
options: {
963+
relationship: Relationship.ManyMany,
964+
foreignTableId: linkTable.id,
965+
},
966+
});
967+
});
968+
969+
afterAll(async () => {
970+
await permanentDeleteTable(baseId, table.id);
971+
await permanentDeleteTable(baseId, linkTable.id);
972+
});
973+
974+
it('should create a record with link field', async () => {
975+
await Promise.all([
976+
createRecords(table.id, {
977+
records: [
978+
{
979+
fields: {
980+
[tableLinkField.id]: [{ id: linkTable.records[0].id }],
981+
},
982+
},
983+
{
984+
fields: {
985+
[tableLinkField.id]: [{ id: linkTable.records[1].id }],
986+
},
987+
},
988+
{
989+
fields: {
990+
[tableLinkField.id]: [{ id: linkTable.records[2].id }],
991+
},
992+
},
993+
{
994+
fields: {
995+
[tableLinkField.id]: [{ id: linkTable.records[3].id }],
996+
},
997+
},
998+
],
999+
}),
1000+
createRecords(table.id, {
1001+
records: [
1002+
{
1003+
fields: {
1004+
[tableLinkField.id]: [{ id: linkTable.records[0].id }],
1005+
},
1006+
},
1007+
{
1008+
fields: {
1009+
[tableLinkField.id]: [{ id: linkTable.records[1].id }],
1010+
},
1011+
},
1012+
{
1013+
fields: {
1014+
[tableLinkField.id]: [{ id: linkTable.records[2].id }],
1015+
},
1016+
},
1017+
{
1018+
fields: {
1019+
[tableLinkField.id]: [{ id: linkTable.records[3].id }],
1020+
},
1021+
},
1022+
],
1023+
}),
1024+
]);
1025+
});
1026+
});
9131027
});

0 commit comments

Comments
 (0)