From d0e44d450fc6c23f92692b7e96bb4155f492189b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 12 Dec 2023 15:18:32 +0100 Subject: [PATCH] feat(core): Add multi-main setup debug endpoint (no-changelog) (#7991) ## Summary Provide details about your pull request and what it adds, fixes, or changes. Photos and videos are recommended. Adi's idea here to help diagnose: https://n8nio.slack.com/archives/C069KJBJ8HE/p1702300349277609?thread_ts=1702299930.728029&cid=C069KJBJ8HE ... #### How to test the change: 1. ... ## Issues fixed Include links to Github issue or Community forum post or **Linear ticket**: > Important in order to close automatically and provide context to reviewers ... ## Review / Merge checklist - [ ] PR title and summary are descriptive. **Remember, the title automatically goes into the changelog. Use `(no-changelog)` otherwise.** ([conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md)) - [ ] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up ticket created. - [ ] Tests included. > A bug is not considered fixed, unless a test is added to prevent it from happening again. A feature is not complete without tests. > > *(internal)* You can use Slack commands to trigger [e2e tests](https://www.notion.so/n8n/How-to-use-Test-Instances-d65f49dfc51f441ea44367fb6f67eb0a?pvs=4#a39f9e5ba64a48b58a71d81c837e8227) or [deploy test instance](https://www.notion.so/n8n/How-to-use-Test-Instances-d65f49dfc51f441ea44367fb6f67eb0a?pvs=4#f6a177d32bde4b57ae2da0b8e454bfce) or [deploy early access version on Cloud](https://www.notion.so/n8n/Cloudbot-3dbe779836004972b7057bc989526998?pvs=4#fef2d36ab02247e1a0f65a74f6fb534e). --- packages/cli/src/ActiveWorkflowRunner.ts | 4 ++ packages/cli/src/Server.ts | 6 +++ .../cli/src/controllers/debug.controller.ts | 35 ++++++++++++++ .../orchestration/main/MultiMainSetup.ee.ts | 8 ++++ .../test/integration/debug.controller.test.ts | 48 +++++++++++++++++++ packages/cli/test/integration/shared/types.ts | 3 +- .../integration/shared/utils/testServer.ts | 5 ++ 7 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 packages/cli/src/controllers/debug.controller.ts create mode 100644 packages/cli/test/integration/debug.controller.test.ts diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index 8ddc228a20..a665e794f3 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -108,6 +108,10 @@ export class ActiveWorkflowRunner implements IWebhookManager { await this.webhookService.populateCache(); } + async getAllWorkflowActivationErrors() { + return this.activationErrorsService.getAll(); + } + /** * Removes all the currently active workflows from memory. */ diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 8f3c29e6a7..6884e05706 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -120,6 +120,7 @@ import { CollaborationService } from './collaboration/collaboration.service'; import { RoleController } from './controllers/role.controller'; import { BadRequestError } from './errors/response-errors/bad-request.error'; import { NotFoundError } from './errors/response-errors/not-found.error'; +import { MultiMainSetup } from './services/orchestration/main/MultiMainSetup.ee'; import { PasswordUtility } from './services/password.utility'; const exec = promisify(callbackExec); @@ -307,6 +308,11 @@ export class Server extends AbstractServer { Container.get(RoleController), ]; + if (Container.get(MultiMainSetup).isEnabled) { + const { DebugController } = await import('./controllers/debug.controller'); + controllers.push(Container.get(DebugController)); + } + if (isLdapEnabled()) { const { service, sync } = LdapManager.getInstance(); controllers.push(new LdapController(service, sync, internalHooks)); diff --git a/packages/cli/src/controllers/debug.controller.ts b/packages/cli/src/controllers/debug.controller.ts new file mode 100644 index 0000000000..59829ee282 --- /dev/null +++ b/packages/cli/src/controllers/debug.controller.ts @@ -0,0 +1,35 @@ +import { Service } from 'typedi'; +import { Get, RestController } from '@/decorators'; +import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; +import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee'; +import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; +import { In } from 'typeorm'; + +@RestController('/debug') +@Service() +export class DebugController { + constructor( + private readonly multiMainSetup: MultiMainSetup, + private readonly activeWorkflowRunner: ActiveWorkflowRunner, + private readonly workflowRepository: WorkflowRepository, + ) {} + + @Get('/multi-main-setup') + async getMultiMainSetupDetails() { + const leaderKey = await this.multiMainSetup.fetchLeaderKey(); + + const activeWorkflows = await this.workflowRepository.find({ + select: ['id', 'name'], + where: { id: In(this.activeWorkflowRunner.allActiveInMemory()) }, + }); + + const activationErrors = await this.activeWorkflowRunner.getAllWorkflowActivationErrors(); + + return { + instanceId: this.multiMainSetup.instanceId, + leaderKey, + activeWorkflows, + activationErrors, + }; + } +} diff --git a/packages/cli/src/services/orchestration/main/MultiMainSetup.ee.ts b/packages/cli/src/services/orchestration/main/MultiMainSetup.ee.ts index 2a23690906..b032b1e979 100644 --- a/packages/cli/src/services/orchestration/main/MultiMainSetup.ee.ts +++ b/packages/cli/src/services/orchestration/main/MultiMainSetup.ee.ts @@ -28,6 +28,10 @@ export class MultiMainSetup extends SingleMainSetup { return !this.isLeader; } + get instanceId() { + return this.id; + } + setLicensed(newState: boolean) { this.isLicensed = newState; } @@ -140,4 +144,8 @@ export class MultiMainSetup extends SingleMainSetup { payload, }); } + + async fetchLeaderKey() { + return this.redisPublisher.get(this.leaderKey); + } } diff --git a/packages/cli/test/integration/debug.controller.test.ts b/packages/cli/test/integration/debug.controller.test.ts new file mode 100644 index 0000000000..954d17da9c --- /dev/null +++ b/packages/cli/test/integration/debug.controller.test.ts @@ -0,0 +1,48 @@ +import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; +import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; +import { mockInstance } from '../shared/mocking'; +import { randomName } from './shared/random'; +import { generateNanoId } from '@/databases/utils/generators'; +import type { WorkflowEntity } from '@/databases/entities/WorkflowEntity'; +import { setupTestServer } from './shared/utils'; +import type { SuperAgentTest } from 'supertest'; +import { createOwner } from './shared/db/users'; +import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee'; + +describe('DebugController', () => { + const workflowRepository = mockInstance(WorkflowRepository); + const activeWorkflowRunner = mockInstance(ActiveWorkflowRunner); + + let testServer = setupTestServer({ endpointGroups: ['debug'] }); + let ownerAgent: SuperAgentTest; + + beforeAll(async () => { + const owner = await createOwner(); + ownerAgent = testServer.authAgentFor(owner); + testServer.license.enable('feat:multipleMainInstances'); + }); + + describe('GET /debug/multi-main-setup', () => { + test('should return multi-main setup details', async () => { + const workflowId = generateNanoId(); + const activeWorkflows = [{ id: workflowId, name: randomName() }] as WorkflowEntity[]; + const activationErrors = { [workflowId]: 'Failed to activate' }; + const instanceId = 'main-71JdWtq306epIFki'; + + workflowRepository.find.mockResolvedValue(activeWorkflows); + activeWorkflowRunner.allActiveInMemory.mockReturnValue([workflowId]); + activeWorkflowRunner.getAllWorkflowActivationErrors.mockResolvedValue(activationErrors); + jest.spyOn(MultiMainSetup.prototype, 'instanceId', 'get').mockReturnValue(instanceId); + jest.spyOn(MultiMainSetup.prototype, 'fetchLeaderKey').mockResolvedValue('some-leader-key'); + + const response = await ownerAgent.get('/debug/multi-main-setup').expect(200); + + expect(response.body.data).toMatchObject({ + instanceId, + leaderKey: 'some-leader-key', + activeWorkflows, + activationErrors, + }); + }); + }); +}); diff --git a/packages/cli/test/integration/shared/types.ts b/packages/cli/test/integration/shared/types.ts index ff93f11632..6670fc1ff9 100644 --- a/packages/cli/test/integration/shared/types.ts +++ b/packages/cli/test/integration/shared/types.ts @@ -32,7 +32,8 @@ type EndpointGroup = | 'workflowHistory' | 'binaryData' | 'role' - | 'invitations'; + | 'invitations' + | 'debug'; export interface SetupProps { applyAuth?: boolean; diff --git a/packages/cli/test/integration/shared/utils/testServer.ts b/packages/cli/test/integration/shared/utils/testServer.ts index 5a76bec24c..351e0413e2 100644 --- a/packages/cli/test/integration/shared/utils/testServer.ts +++ b/packages/cli/test/integration/shared/utils/testServer.ts @@ -312,6 +312,11 @@ export const setupTestServer = ({ const { RoleController } = await import('@/controllers/role.controller'); registerController(app, config, Container.get(RoleController)); break; + + case 'debug': + const { DebugController } = await import('@/controllers/debug.controller'); + registerController(app, config, Container.get(DebugController)); + break; } } }