From 847f6ac771316eea270d2e83adac5d8a6483475a Mon Sep 17 00:00:00 2001 From: Omar Ajoue Date: Mon, 27 Nov 2023 15:43:48 +0100 Subject: [PATCH] fix(core): Prevent error messages due to statistics about data loading (#7824) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Statistics collection about the first time a workflow loads data simply attempts an insert to db, and if it fails, we just ignore. This was causing this query to fire against production workflows multiple times, and since we want to insert only and detect whether the insertion failed, performing a select first provides gains both in terms of performance, as it's usually faster than trying an insertion as well as preventing unnecessary noise in logs. Github issue / Community forum post (link here to close automatically): https://community.n8n.io/t/duplicate-key-value-violates-unique-constraint-workflow-statistics-pkey-still-happening/29283 https://github.com/n8n-io/n8n/issues/7256 https://community.n8n.io/t/error-log-arriving-in-postgres/30191 https://github.com/n8n-io/n8n/issues/7256 https://community.n8n.io/t/cant-launch-webhooks-unable-to-find-data-of-execution/31867 --------- Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ --- .../workflowStatistics.repository.ts | 9 +++- packages/cli/src/services/events.service.ts | 2 +- .../repositories/workflowStatistics.test.ts | 53 +++++++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 packages/cli/test/unit/repositories/workflowStatistics.test.ts diff --git a/packages/cli/src/databases/repositories/workflowStatistics.repository.ts b/packages/cli/src/databases/repositories/workflowStatistics.repository.ts index 3ce4ea6d83..5d6a9261da 100644 --- a/packages/cli/src/databases/repositories/workflowStatistics.repository.ts +++ b/packages/cli/src/databases/repositories/workflowStatistics.repository.ts @@ -4,7 +4,7 @@ import config from '@/config'; import type { StatisticsNames } from '../entities/WorkflowStatistics'; import { WorkflowStatistics } from '../entities/WorkflowStatistics'; -type StatisticsInsertResult = 'insert' | 'failed'; +type StatisticsInsertResult = 'insert' | 'failed' | 'alreadyExists'; type StatisticsUpsertResult = StatisticsInsertResult | 'update'; @Service() @@ -21,6 +21,13 @@ export class WorkflowStatisticsRepository extends Repository ): Promise { // Try to insert the data loaded statistic try { + const exists = await this.findOne({ + where: { + workflowId, + name: eventName, + }, + }); + if (exists) return 'alreadyExists'; await this.insert({ workflowId, name: eventName, diff --git a/packages/cli/src/services/events.service.ts b/packages/cli/src/services/events.service.ts index 3f2504366e..64c8a0e499 100644 --- a/packages/cli/src/services/events.service.ts +++ b/packages/cli/src/services/events.service.ts @@ -73,7 +73,7 @@ export class EventsService extends EventEmitter { StatisticsNames.dataLoaded, workflowId, ); - if (insertResult === 'failed') return; + if (insertResult === 'failed' || insertResult === 'alreadyExists') return; // Compile the metrics since this was a new data loaded event const owner = await this.ownershipService.getWorkflowOwnerCached(workflowId); diff --git a/packages/cli/test/unit/repositories/workflowStatistics.test.ts b/packages/cli/test/unit/repositories/workflowStatistics.test.ts new file mode 100644 index 0000000000..7aa0280600 --- /dev/null +++ b/packages/cli/test/unit/repositories/workflowStatistics.test.ts @@ -0,0 +1,53 @@ +import { WorkflowStatisticsRepository } from '@db/repositories/workflowStatistics.repository'; +import { DataSource, EntityManager, InsertResult, QueryFailedError } from 'typeorm'; +import { mockInstance } from '../../shared/mocking'; +import { mock, mockClear } from 'jest-mock-extended'; +import { StatisticsNames, WorkflowStatistics } from '@/databases/entities/WorkflowStatistics'; + +const entityManager = mockInstance(EntityManager); +const dataSource = mockInstance(DataSource, { manager: entityManager }); +dataSource.getMetadata.mockReturnValue(mock()); +Object.assign(entityManager, { connection: dataSource }); +const workflowStatisticsRepository = new WorkflowStatisticsRepository(dataSource); + +describe('insertWorkflowStatistics', () => { + beforeEach(() => { + mockClear(entityManager.insert); + }); + it('Successfully inserts data when it is not yet present', async () => { + entityManager.findOne.mockResolvedValueOnce(null); + entityManager.insert.mockResolvedValueOnce(mockInstance(InsertResult)); + + const insertionResult = await workflowStatisticsRepository.insertWorkflowStatistics( + StatisticsNames.dataLoaded, + 'workflowId', + ); + + expect(insertionResult).toBe('insert'); + }); + + it('Does not insert when data is present', async () => { + entityManager.findOne.mockResolvedValueOnce(mockInstance(WorkflowStatistics)); + const insertionResult = await workflowStatisticsRepository.insertWorkflowStatistics( + StatisticsNames.dataLoaded, + 'workflowId', + ); + + expect(insertionResult).toBe('alreadyExists'); + expect(entityManager.insert).not.toHaveBeenCalled(); + }); + + it('throws an error when insertion fails', async () => { + entityManager.findOne.mockResolvedValueOnce(null); + entityManager.insert.mockImplementation(async () => { + throw new QueryFailedError('Query', [], 'driver error'); + }); + + const insertionResult = await workflowStatisticsRepository.insertWorkflowStatistics( + StatisticsNames.dataLoaded, + 'workflowId', + ); + + expect(insertionResult).toBe('failed'); + }); +});