fix(core): Detect DB connection aquisition deadlocks (no-changelog) (#9485)

Co-authored-by: Danny Martini <danny@n8n.io>
This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2024-05-22 14:53:23 +02:00 committed by GitHub
parent 2fa46b6faa
commit 3094f1b886
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 53 additions and 66 deletions

View file

@ -102,6 +102,7 @@ jobs:
timeout-minutes: 20
env:
DB_POSTGRESDB_PASSWORD: password
DB_POSTGRESDB_POOL_SIZE: 1 # Detect connection pooling deadlocks
steps:
- uses: actions/checkout@v4.1.1
- run: corepack enable

View file

@ -69,6 +69,7 @@ export async function saveCredential(
const personalProject = await Container.get(ProjectRepository).getPersonalProjectForUserOrFail(
user.id,
transactionManager,
);
Object.assign(newSharedCredential, {

View file

@ -7,7 +7,6 @@ import { SharedWorkflow, type WorkflowSharingRole } from '@db/entities/SharedWor
import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import type { Project } from '@/databases/entities/Project';
import { WorkflowTagMappingRepository } from '@db/repositories/workflowTagMapping.repository';
import { TagRepository } from '@db/repositories/tag.repository';
import { License } from '@/License';
import { WorkflowSharingService } from '@/workflows/workflowSharing.service';
@ -113,9 +112,7 @@ export async function getWorkflowTags(workflowId: string) {
export async function updateTags(workflowId: string, newTags: string[]): Promise<any> {
await Db.transaction(async (transactionManager) => {
const oldTags = await Container.get(WorkflowTagMappingRepository).findBy({
workflowId,
});
const oldTags = await transactionManager.findBy(WorkflowTagMapping, { workflowId });
if (oldTags.length > 0) {
await transactionManager.delete(WorkflowTagMapping, oldTags);
}

View file

@ -3,7 +3,7 @@ import {
ErrorReporterProxy as ErrorReporter,
WorkflowOperationError,
} from 'n8n-workflow';
import { Container, Service } from 'typedi';
import { Service } from 'typedi';
import type { ExecutionStopResult, IWorkflowExecutionDataProcess } from '@/Interfaces';
import { WorkflowRunner } from '@/WorkflowRunner';
import { ExecutionRepository } from '@db/repositories/execution.repository';
@ -137,10 +137,7 @@ export class WaitTracker {
fullExecutionData.waitTill = null;
fullExecutionData.status = 'canceled';
await Container.get(ExecutionRepository).updateExistingExecution(
executionId,
fullExecutionData,
);
await this.executionRepository.updateExistingExecution(executionId, fullExecutionData);
return {
mode: fullExecutionData.mode,

View file

@ -13,9 +13,9 @@ import { BaseCommand } from '../BaseCommand';
import type { ICredentialsEncrypted } from 'n8n-workflow';
import { ApplicationError, jsonParse } from 'n8n-workflow';
import { UM_FIX_INSTRUCTION } from '@/constants';
import { UserRepository } from '@db/repositories/user.repository';
import { ProjectRepository } from '@/databases/repositories/project.repository';
import type { Project } from '@/databases/entities/Project';
import { Project } from '@/databases/entities/Project';
import { User } from '@/databases/entities/User';
export class ImportCredentialsCommand extends BaseCommand {
static description = 'Import credentials';
@ -75,13 +75,13 @@ export class ImportCredentialsCommand extends BaseCommand {
);
}
const project = await this.getProject(flags.userId, flags.projectId);
const credentials = await this.readCredentials(flags.input, flags.separate);
await Db.getConnection().transaction(async (transactionManager) => {
this.transactionManager = transactionManager;
const project = await this.getProject(flags.userId, flags.projectId);
const result = await this.checkRelations(credentials, flags.projectId, flags.userId);
if (!result.success) {
@ -130,19 +130,6 @@ export class ImportCredentialsCommand extends BaseCommand {
}
}
private async getOwnerProject() {
const owner = await Container.get(UserRepository).findOneBy({ role: 'global:owner' });
if (!owner) {
throw new ApplicationError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
}
const project = await Container.get(ProjectRepository).getPersonalProjectForUserOrFail(
owner.id,
);
return project;
}
private async checkRelations(
credentials: ICredentialsEncrypted[],
projectId?: string,
@ -244,7 +231,7 @@ export class ImportCredentialsCommand extends BaseCommand {
});
if (sharedCredential && sharedCredential.project.type === 'personal') {
const user = await Container.get(UserRepository).findOneByOrFail({
const user = await this.transactionManager.findOneByOrFail(User, {
projectRelations: {
role: 'project:personalOwner',
projectId: sharedCredential.projectId,
@ -263,13 +250,20 @@ export class ImportCredentialsCommand extends BaseCommand {
private async getProject(userId?: string, projectId?: string) {
if (projectId) {
return await Container.get(ProjectRepository).findOneByOrFail({ id: projectId });
return await this.transactionManager.findOneByOrFail(Project, { id: projectId });
}
if (userId) {
return await Container.get(ProjectRepository).getPersonalProjectForUserOrFail(userId);
if (!userId) {
const owner = await this.transactionManager.findOneBy(User, { role: 'global:owner' });
if (!owner) {
throw new ApplicationError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
}
userId = owner.id;
}
return await this.getOwnerProject();
return await Container.get(ProjectRepository).getPersonalProjectForUserOrFail(
userId,
this.transactionManager,
);
}
}

View file

@ -160,19 +160,6 @@ export class ImportWorkflowsCommand extends BaseCommand {
this.logger.info(`Successfully imported ${total} ${total === 1 ? 'workflow.' : 'workflows.'}`);
}
private async getOwnerProject() {
const owner = await Container.get(UserRepository).findOneBy({ role: 'global:owner' });
if (!owner) {
throw new ApplicationError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
}
const project = await Container.get(ProjectRepository).getPersonalProjectForUserOrFail(
owner.id,
);
return project;
}
private async getWorkflowOwner(workflow: WorkflowEntity) {
const sharing = await Container.get(SharedWorkflowRepository).findOne({
where: { workflowId: workflow.id, role: 'workflow:owner' },
@ -234,10 +221,14 @@ export class ImportWorkflowsCommand extends BaseCommand {
return await Container.get(ProjectRepository).findOneByOrFail({ id: projectId });
}
if (userId) {
return await Container.get(ProjectRepository).getPersonalProjectForUserOrFail(userId);
if (!userId) {
const owner = await Container.get(UserRepository).findOneBy({ role: 'global:owner' });
if (!owner) {
throw new ApplicationError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
}
userId = owner.id;
}
return await this.getOwnerProject();
return await Container.get(ProjectRepository).getPersonalProjectForUserOrFail(userId);
}
}

View file

@ -262,7 +262,10 @@ export class CredentialsService {
const project =
projectId === undefined
? await this.projectRepository.getPersonalProjectForUserOrFail(user.id)
? await this.projectRepository.getPersonalProjectForUserOrFail(
user.id,
transactionManager,
)
: await this.projectService.getProjectWithScope(
user,
projectId,

View file

@ -17,8 +17,10 @@ export class ProjectRepository extends Repository<Project> {
});
}
async getPersonalProjectForUserOrFail(userId: string) {
return await this.findOneOrFail({
async getPersonalProjectForUserOrFail(userId: string, entityManager?: EntityManager) {
const em = entityManager ?? this.manager;
return await em.findOneOrFail(Project, {
where: { type: 'personal', projectRelations: { userId, role: 'project:personalOwner' } },
});
}

View file

@ -1,12 +1,12 @@
import { Container } from 'typedi';
import type { EntitySubscriberInterface, UpdateEvent } from '@n8n/typeorm';
import { EventSubscriber } from '@n8n/typeorm';
import { User } from '../entities/User';
import Container from 'typedi';
import { ProjectRepository } from '../repositories/project.repository';
import { ApplicationError, ErrorReporterProxy } from 'n8n-workflow';
import { Logger } from '@/Logger';
import { UserRepository } from '../repositories/user.repository';
import { Project } from '../entities/Project';
import { User } from '../entities/User';
import { UserRepository } from '../repositories/user.repository';
@EventSubscriber()
export class UserSubscriber implements EntitySubscriberInterface<User> {
@ -27,14 +27,17 @@ export class UserSubscriber implements EntitySubscriberInterface<User> {
fields.includes('email')
) {
const oldUser = event.databaseEntity;
const name =
const userEntity =
newUserData instanceof User
? newUserData.createPersonalProjectName()
: Container.get(UserRepository).create(newUserData).createPersonalProjectName();
? newUserData
: Container.get(UserRepository).create(newUserData);
const project = await Container.get(ProjectRepository).getPersonalProjectForUser(
oldUser.id,
);
const projectName = userEntity.createPersonalProjectName();
const project = await event.manager.findOneBy(Project, {
type: 'personal',
projectRelations: { userId: oldUser.id },
});
if (!project) {
// Since this is benign we're not throwing the exception. We don't
@ -47,7 +50,7 @@ export class UserSubscriber implements EntitySubscriberInterface<User> {
return;
}
project.name = name;
project.name = projectName;
await event.manager.save(Project, project);
}

View file

@ -1,4 +1,4 @@
import Container, { Service } from 'typedi';
import { Service } from 'typedi';
import { v4 as uuid } from 'uuid';
import { type INode, type INodeCredentialsDetails } from 'n8n-workflow';
@ -8,11 +8,11 @@ import { CredentialsRepository } from '@db/repositories/credentials.repository';
import { TagRepository } from '@db/repositories/tag.repository';
import { SharedWorkflow } from '@db/entities/SharedWorkflow';
import { replaceInvalidCredentials } from '@/WorkflowHelpers';
import { Project } from '@db/entities/Project';
import { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { WorkflowTagMapping } from '@db/entities/WorkflowTagMapping';
import type { TagEntity } from '@db/entities/TagEntity';
import type { ICredentialsDb } from '@/Interfaces';
import { ProjectRepository } from '@/databases/repositories/project.repository';
@Service()
export class ImportService {
@ -59,9 +59,7 @@ export class ImportService {
const upsertResult = await tx.upsert(WorkflowEntity, workflow, ['id']);
const workflowId = upsertResult.identifiers.at(0)?.id as string;
const personalProject = await Container.get(ProjectRepository).findOneByOrFail({
id: projectId,
});
const personalProject = await tx.findOneByOrFail(Project, { id: projectId });
// Create relationship if the workflow was inserted instead of updated.
if (!exists) {