Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { QueryRunnerAlreadyReleasedError } from 'typeorm';

import { computeTwentyORMException } from 'src/engine/twenty-orm/error-handling/compute-twenty-orm-exception';
import {
TwentyORMException,
TwentyORMExceptionCode,
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';

describe('computeTwentyORMException', () => {
it('should map a released query runner error to a retryable QUERY_RUNNER_RELEASED exception', async () => {
const error = new QueryRunnerAlreadyReleasedError();

const result = await computeTwentyORMException(error);

expect(result).toBeInstanceOf(TwentyORMException);
expect((result as TwentyORMException).code).toBe(
TwentyORMExceptionCode.QUERY_RUNNER_RELEASED,
);
});

it('should preserve the original stack and cause when wrapping a released runner error', async () => {
const error = new QueryRunnerAlreadyReleasedError();

const result = await computeTwentyORMException(error);

expect(result.stack).toBe(error.stack);
expect((result as TwentyORMException).cause).toBe(error);
});

it('should return unrelated errors unchanged', async () => {
const error = new Error('some other error');

const result = await computeTwentyORMException(error);

expect(result).toBe(error);
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { msg } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { QueryFailedError } from 'typeorm';
import { QueryFailedError, QueryRunnerAlreadyReleasedError } from 'typeorm';

import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';

Expand All @@ -24,6 +24,21 @@ export const computeTwentyORMException = async (
entityManager?: WorkspaceEntityManager,
internalContext?: WorkspaceInternalContext,
): Promise<Error | TwentyORMException> => {
if (error instanceof QueryRunnerAlreadyReleasedError) {
const releasedRunnerException = new TwentyORMException(
error.message,
TwentyORMExceptionCode.QUERY_RUNNER_RELEASED,
);

// Keep the original stack: it points at the query that ran on the
// released runner, which is the only way to locate where the operation
// escaped its transaction boundary.
releasedRunnerException.stack = error.stack;
releasedRunnerException.cause = error;

return releasedRunnerException;
}

if (error instanceof QueryFailedError) {
if (error.message.includes('Query read timeout')) {
return new TwentyORMException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export enum TwentyORMExceptionCode {
METHOD_NOT_ALLOWED = 'METHOD_NOT_ALLOWED',
ENUM_TYPE_NAME_NOT_FOUND = 'ENUM_TYPE_NAME_NOT_FOUND',
QUERY_READ_TIMEOUT = 'QUERY_READ_TIMEOUT',
QUERY_RUNNER_RELEASED = 'QUERY_RUNNER_RELEASED',
DUPLICATE_ENTRY_DETECTED = 'DUPLICATE_ENTRY_DETECTED',
TOO_MANY_RECORDS_TO_UPDATE = 'TOO_MANY_RECORDS_TO_UPDATE',
INVALID_INPUT = 'INVALID_INPUT',
Expand Down Expand Up @@ -61,6 +62,8 @@ const getTwentyORMExceptionUserFriendlyMessage = (
return msg`This operation is not allowed.`;
case TwentyORMExceptionCode.QUERY_READ_TIMEOUT:
return msg`Query timed out. Please try again.`;
case TwentyORMExceptionCode.QUERY_RUNNER_RELEASED:
return msg`We are experiencing a temporary issue with our database. Please try again later.`;
case TwentyORMExceptionCode.DUPLICATE_ENTRY_DETECTED:
return msg`A duplicate entry was detected.`;
case TwentyORMExceptionCode.TOO_MANY_RECORDS_TO_UPDATE:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,23 @@ export class CalendarEventImportErrorHandlerService {
workspaceId,
);
break;
case TwentyORMExceptionCode.QUERY_RUNNER_RELEASED:
// Transient, so retried like any temporary error - but also reported
// every time (with the original stack preserved upstream) so the
// operation that escapes its transaction boundary can be located.
this.exceptionHandlerService.captureExceptions([exception], {
additionalData: {
calendarChannelId: calendarChannel.id,
syncStep,
},
workspace: { id: workspaceId },
});
await this.handleTemporaryException(
syncStep,
calendarChannel,
workspaceId,
);
break;
case TwentyORMExceptionCode.QUERY_READ_TIMEOUT:
case CalendarEventImportDriverExceptionCode.TEMPORARY_ERROR:
case ConnectedAccountRefreshAccessTokenExceptionCode.TEMPORARY_NETWORK_ERROR:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export class MessageImportExceptionHandlerService {
);
break;
case TwentyORMExceptionCode.QUERY_READ_TIMEOUT:
case TwentyORMExceptionCode.QUERY_RUNNER_RELEASED:
case MessageImportDriverExceptionCode.TEMPORARY_ERROR:
case ConnectedAccountRefreshAccessTokenExceptionCode.TEMPORARY_NETWORK_ERROR:
case MessageNetworkExceptionCode.ECONNABORTED:
Expand Down
Loading