From 265d2edc83bc9200d8035c41b605be5f8ca1df97 Mon Sep 17 00:00:00 2001 From: Weiko Date: Tue, 19 May 2026 17:32:36 +0200 Subject: [PATCH] Fix QueryRunnerAlreadyReleasedError in sign-in-up service (#20734) ## Context When signing up on a new workspace, `SignInUpService.signUpOnNewWorkspace` manually drove a transaction with `createQueryRunner` / `startTransaction` / `commitTransaction` / `rollbackTransaction` / `release`. If the underlying Postgres connection dropped mid-transaction (`idle_in_transaction_session_timeout`, server-side termination), the `pg` client's `'error'` event fires. TypeORM's connect-time listener responds by calling `release()` on the `QueryRunner`, which sets `isReleased = true` but deliberately does **not** touch `isTransactionActive`. The `catch` branch then hit: ```ts if (queryRunner.isTransactionActive) { await queryRunner.rollbackTransaction(); // throws QueryRunnerAlreadyReleasedError } throw error; ``` Error from Sentry ```typescript QueryRunnerAlreadyReleasedError: Query runner already released. Cannot run queries anymore. at PostgresQueryRunner.query (.../PostgresQueryRunner.js:177) at PostgresQueryRunner.rollbackTransaction (.../PostgresQueryRunner.js:167) at SignInUpService.signUpOnNewWorkspace (.../sign-in-up.service.js:370) ``` ## Changes Replaced the hand-rolled transaction with this.dataSource.transaction(...). TypeORM's built-in wrapper already does what we need: - starts/commits/rolls back the transaction - wraps rollback in try { ... } catch { /* ignore */ }, so a connection drop no longer masks the real error - releases the QueryRunner unconditionally ## Note Other fix would have been to do this ```typescript if (queryRunner.isTransactionActive && **!queryRunner.isReleased**) { try { await queryRunner.rollbackTransaction(); } catch { ``` --- .../auth/services/sign-in-up.service.ts | 163 +++++++++--------- 1 file changed, 80 insertions(+), 83 deletions(-) diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts index 116d681a2e..d1bccf4780 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts @@ -502,113 +502,110 @@ export class SignInUpService { const workspaceId = v4(); const workspaceCustomApplicationId = v4(); - const queryRunner = this.dataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); try { - const workspaceToCreate = this.workspaceRepository.create({ - id: workspaceId, - subdomain: await this.subdomainManagerService.generateSubdomain( - isWorkEmailFound ? { userEmail: email } : {}, - ), - workspaceCustomApplicationId, - displayName: '', - inviteHash: v4(), - activationStatus: WorkspaceActivationStatus.PENDING_CREATION, - }); + const { user, workspace } = await this.dataSource.transaction( + async (entityManager) => { + const queryRunner = entityManager.queryRunner as QueryRunner; - const workspace = await queryRunner.manager.save( - WorkspaceEntity, - workspaceToCreate, - ); - - const customApplication = - await this.applicationService.createWorkspaceCustomApplication( - { - workspaceId, - applicationId: workspaceCustomApplicationId, - }, - queryRunner, - ); - - if (isWorkEmailFound) { - const logoUrl = `${TWENTY_ICONS_BASE_URL}/${getDomainNameByEmail(email)}`; - const logoFile = - await this.fileCorePictureService.uploadWorkspaceLogoFromUrl({ - imageUrl: logoUrl, - workspaceId, - applicationUniversalIdentifier: - customApplication.universalIdentifier, - queryRunner, + const workspaceToCreate = this.workspaceRepository.create({ + id: workspaceId, + subdomain: await this.subdomainManagerService.generateSubdomain( + isWorkEmailFound ? { userEmail: email } : {}, + ), + workspaceCustomApplicationId, + displayName: '', + inviteHash: v4(), + activationStatus: WorkspaceActivationStatus.PENDING_CREATION, }); - if (isDefined(logoFile)) { - await queryRunner.manager.update( + const workspace = await queryRunner.manager.save( WorkspaceEntity, - { id: workspaceId }, - { logoFileId: logoFile.id }, + workspaceToCreate, ); - } - } - const isExistingUser = userData.type === 'existingUser'; - const user = isExistingUser - ? userData.existingUser - : await this.saveNewUser( - userData.newUserWithPicture, + const customApplication = + await this.applicationService.createWorkspaceCustomApplication( + { + workspaceId, + applicationId: workspaceCustomApplicationId, + }, + queryRunner, + ); + + if (isWorkEmailFound) { + const logoUrl = `${TWENTY_ICONS_BASE_URL}/${getDomainNameByEmail(email)}`; + const logoFile = + await this.fileCorePictureService.uploadWorkspaceLogoFromUrl({ + imageUrl: logoUrl, + workspaceId, + applicationUniversalIdentifier: + customApplication.universalIdentifier, + queryRunner, + }); + + if (isDefined(logoFile)) { + await queryRunner.manager.update( + WorkspaceEntity, + { id: workspaceId }, + { logoFileId: logoFile.id }, + ); + } + } + + const isExistingUser = userData.type === 'existingUser'; + const user = isExistingUser + ? userData.existingUser + : await this.saveNewUser( + userData.newUserWithPicture, + { + canImpersonate: shouldGrantServerAdmin, + canAccessFullAdminPanel: shouldGrantServerAdmin, + }, + queryRunner, + ); + + await this.userWorkspaceService.create( { - canImpersonate: shouldGrantServerAdmin, - canAccessFullAdminPanel: shouldGrantServerAdmin, + userId: user.id, + workspaceId: workspace.id, + isExistingUser, + pictureUrl: isExistingUser + ? undefined + : userData.newUserWithPicture.picture, + applicationUniversalIdentifier: + customApplication.universalIdentifier, }, queryRunner, ); - await this.userWorkspaceService.create( - { - userId: user.id, - workspaceId: workspace.id, - isExistingUser, - pictureUrl: isExistingUser - ? undefined - : userData.newUserWithPicture.picture, - applicationUniversalIdentifier: customApplication.universalIdentifier, - }, - queryRunner, - ); + await this.activateOnboardingForUser( + { + user, + workspace, + shouldShowConnectAccountStep: true, + }, + queryRunner, + ); - await this.activateOnboardingForUser( - { - user, - workspace, - shouldShowConnectAccountStep: true, - }, - queryRunner, - ); + await this.onboardingService.setOnboardingInviteTeamPending( + { + workspaceId: workspace.id, + value: true, + }, + queryRunner, + ); - await this.onboardingService.setOnboardingInviteTeamPending( - { - workspaceId: workspace.id, - value: true, + return { user, workspace }; }, - queryRunner, ); - await queryRunner.commitTransaction(); - void this.auditService .createContext({ workspaceId }) .insertWorkspaceEvent(WORKSPACE_CREATED_EVENT, {}); return { user, workspace }; - } catch (error) { - if (queryRunner.isTransactionActive) { - await queryRunner.rollbackTransaction(); - } - throw error; } finally { - await queryRunner.release(); await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ 'flatApplicationMaps', ]);