Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(core): Consolidate execution lifecycle hooks even more + additional tests #12898

Merged
merged 10 commits into from
Feb 4, 2025

Conversation

netroy
Copy link
Member

@netroy netroy commented Jan 28, 2025

Summary

This PR

  1. moves all execution related external hooks into lifecycle hooks to make these consistent across all execution modes.
  2. hoists all Container.get calls in execution-lifecycle-hooks.ts to prepare that file for switching to DI
  3. replaces hookFunctionsPreExecute with hookFunctionsSaveProgress and hookFunctionsExternalHooks
  4. calculates saving settings only once for an execution
  5. skips setting up the progress saving hook, when not needed
  6. skips setting up push hooks, when not needed
  7. reduces passing around of pushRef and retryOf
  8. adds additional unit tests for code in and around lifecycle hooks
  9. improves type-safety on retryOf

Review / Merge checklist

  • PR title and summary are descriptive. (conventions)
  • Docs updated or follow-up ticket created.
  • Tests included.
  • PR Labeled with release/backport (if the PR is an urgent fix that needs to be backported)

@n8n-assistant n8n-assistant bot added core Enhancement outside /nodes-base and /editor-ui n8n team Authored by the n8n team labels Jan 28, 2025
Copy link

codecov bot commented Jan 28, 2025

Codecov Report

Attention: Patch coverage is 93.54839% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...c/execution-lifecycle/execution-lifecycle-hooks.ts 93.75% 3 Missing ⚠️
packages/cli/src/workflow-runner.ts 75.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Base automatically changed from refactor-externalHooks to master January 29, 2025 09:33
@netroy netroy force-pushed the external-hooks-in-lifecycle-hooks branch from 429dc92 to d362673 Compare January 30, 2025 14:22
@netroy netroy changed the title refactor(core): Consolidate all external hook calls in execution lifecycle hooks refactor(core): Consolidate execution lifecycle hooks even more + additional tests Jan 30, 2025
@netroy netroy force-pushed the external-hooks-in-lifecycle-hooks branch from d362673 to 81a01dd Compare January 30, 2025 14:56
@netroy netroy requested a review from ivov January 30, 2025 15:19
Copy link
Contributor

@ivov ivov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot going on here, I'll come back later to continue reviewing.

});
this.logger.error('There was an error in the post-execution promise', {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we log this here already?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's only when not using Sentry. I can remove this, but this line is not the same as the one on the default error reporter.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see. Let's keep for now but I've always found it confusing that the error reporter both reports to Sentry and logs out errors. I'd expect all error logging to happen via Logger.

packages/cli/src/execution-lifecycle/to-save-settings.ts Outdated Show resolved Hide resolved
Copy link
Contributor

@ivov ivov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most hooks log like this Executing hook (hookFunctionsPush) so we the log loses the actual hook being executed. Can we add them? e.g. Executing hook workflowExecuteAfter (hookFunctionsPush) The hook is as important as the bundle it belongs to. Also ideally if we can log this more centrally instead of at every hook.

if (executionData.retryOf !== undefined) {
fullExecutionData.retryOf = executionData.retryOf.toString();
}
fullExecutionData.retryOf = executionData.retryOf ?? undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the type ?? undefined is not necessary.

Capture 2025-02-03 at 14 10 51@2x

Same elsewhere.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

retryOf typings are still not 100% reliable, and sometimes this is null because the value came from typeorm.

} as IExecutionResponse);
test('should handle DB errors when updating the execution', async () => {
const error = new Error('Something went wrong');
executionRepository.findSingleExecution.mockResolvedValue({} as IExecutionResponse);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
executionRepository.findSingleExecution.mockResolvedValue({} as IExecutionResponse);
executionRepository.findSingleExecution.mockResolvedValue(mock<IExecutionResponse>());

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

couldn't get it to work because of all the deep proxies.

});

executionRepository.findSingleExecution.mockResolvedValue({} as IExecutionResponse);
test('should populate `.data` when it is missing', async () => {
const fullExecutionData = {} as IExecutionResponse;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const fullExecutionData = {} as IExecutionResponse;
const fullExecutionData = mock<IExecutionResponse>();

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

@@ -392,7 +383,7 @@ export class WorkflowRunner {
data.executionMode,
executionId,
data.workflowData,
{ retryOf: data.retryOf ? data.retryOf.toString() : undefined },
{ retryOf: data.retryOf ?? undefined },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{ retryOf: data.retryOf ?? undefined },
{ retryOf: data.retryOf },

@@ -2284,7 +2284,7 @@ export interface IWorkflowExecutionDataProcess {
executionData?: IRunExecutionData;
runData?: IRunData;
pinData?: IPinData;
retryOf?: string;
retryOf?: string | null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it was string | undefined? If null is what the DB returns, can we transform it to undefined when we retrieve?

Having to remember retryOf ?? undefined disregarding the current type is an accident waiting to happen.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to spend more time on retryOf in a separate PR, to make the types around this more reliable.

@@ -179,7 +177,7 @@ export class ActiveExecutions {
data = this.activeExecutions[id];
returnData.push({
id,
retryOf: data.executionData.retryOf,
retryOf: data.executionData.retryOf ?? undefined,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
retryOf: data.executionData.retryOf ?? undefined,
retryOf: data.executionData.retryOf,

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here.

Comment on lines +198 to +202
await externalHooks.run('workflow.postExecute', [
fullRunData,
this.workflowData,
this.executionId,
]);
Copy link
Contributor

@ivov ivov Feb 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This used to be wrapped in a try-catch that ignored the error. I take it now that's covered when we run the hook, i.e. we won't crash if the external hook fails.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no other external hook (besides n8n.stop) is wrapped in try-catch, and I also clearly remember Jan telling me that if an external hook throws, then we throw that error. If someone has an external hook that breaks because of this, then they need to catch the error in the hook code.
External hooks aren't fire-and-forget. This is also why we await on them and run them sequentially.

Comment on lines +36 to +40
type HooksSetupParameters = {
saveSettings: ExecutionSaveSettings;
pushRef?: string;
retryOf?: string;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to remove this type and use the prop types directly?

Usage hints that these props might not belong together:

  • hookFunctionsPush accepts saveSettings and does not use it
  • hookFunctionsSaveProgress acceptspushRef and retryOf and does not use them
  • hookFunctionsSave uses all three props
  • hookFunctionsSaveWorker accepts saveSettings and does not use it
  • getWorkflowHooksWorkerExecuter inline omits saveSettings so we only pass it the other two
  • getWorkflowHooksWorkerMain inline omits saveSettings so we only pass it the other two

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All this code will be re-written once I convert this file to switch to DI, like I did in #12364

Copy link
Contributor

@ivov ivov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shaping up nicely! Nothing to block on

Copy link
Contributor

github-actions bot commented Feb 4, 2025

⚠️ Some Cypress E2E specs are failing, please fix them before merging

Copy link

cypress bot commented Feb 4, 2025

n8n    Run #9094

Run Properties:  status check passed Passed #9094  •  git commit db229a3a6d: 🌳 🖥️ browsers:node18.12.0-chrome107 🤖 netroy 🗃️ e2e/*
Project n8n
Branch Review external-hooks-in-lifecycle-hooks
Run status status check passed Passed #9094
Run duration 04m 23s
Commit git commit db229a3a6d: 🌳 🖥️ browsers:node18.12.0-chrome107 🤖 netroy 🗃️ e2e/*
Committer कारतोफ्फेलस्क्रिप्ट™
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 5
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 433
View all changes introduced in this branch ↗︎

Copy link
Contributor

github-actions bot commented Feb 4, 2025

✅ All Cypress E2E specs passed

@netroy netroy merged commit 65ec6ae into master Feb 4, 2025
115 checks passed
@netroy netroy deleted the external-hooks-in-lifecycle-hooks branch February 4, 2025 09:17
riascho pushed a commit that referenced this pull request Feb 5, 2025
@janober
Copy link
Member

janober commented Feb 6, 2025

Got released with [email protected]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
core Enhancement outside /nodes-base and /editor-ui n8n team Authored by the n8n team Released
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants