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

feat(core): Add basic execution concurrency limits in main mode #8458

Closed
wants to merge 2 commits into from

Conversation

netroy
Copy link
Member

@netroy netroy commented Jan 26, 2024

This PR implements a very rudimentary concurrency limit in main mode, to be a bit more consistent with queue mode, and also to prevent the application from thrashing the event-loop when under high load.

Ticket: N8N-7052

Review / Merge checklist

  • PR title and summary are descriptive
  • Docs updated or follow-up ticket created.
  • Tests included

@n8n-assistant n8n-assistant bot added core Enhancement outside /nodes-base and /editor-ui n8n team Authored by the n8n team labels Jan 26, 2024
@RicardoE105 RicardoE105 marked this pull request as ready for review January 27, 2024 12:33
@netroy netroy marked this pull request as draft January 28, 2024 05:27
@netroy netroy force-pushed the rudimentary-concurrency branch 2 times, most recently from 0b27aed to 0502e77 Compare January 29, 2024 20:19
@netroy netroy changed the base branch from master to refactor-WorkflowRunner January 29, 2024 21:59
@netroy netroy force-pushed the refactor-WorkflowRunner branch 3 times, most recently from 5686ad5 to f350b14 Compare January 30, 2024 12:07
@netroy netroy force-pushed the rudimentary-concurrency branch 2 times, most recently from 7b546d0 to 1e1ea50 Compare February 6, 2024 15:52
Base automatically changed from refactor-WorkflowRunner to master February 6, 2024 17:09
@netroy netroy marked this pull request as ready for review February 6, 2024 19:26
@netroy netroy changed the title feat(core): Make execution concurrency limits consistent across all execution modes feat(core): Add basic execution concurrency limits in main mode Feb 6, 2024
@netroy netroy requested a review from krynble February 6, 2024 19:27
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.

Nice work, this is going to be so useful!

Quick pass, not tested yet.

executionStatus: ExecutionStatus,
) {
// Wait here in-case execution concurrency limit is reached
await this.getQueue(executionData.executionMode).enqueue(executionId);
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd've expected all this to be encapsulated? As in, a ConcurrencyService that contains the two queues and is smart enough to identify manual and production executions without being told what kind they are, e.g. this.concurrencyService.enqueue(execution)

@@ -21,6 +24,11 @@ import { Logger } from '@/Logger';

@Service()
export class ActiveExecutions {
private queues = {
manual: new ConcurrencyQueue(config.getEnv('executions.manualConcurrency')),
others: new ConcurrencyQueue(config.getEnv('executions.concurrency')),
Copy link
Contributor

Choose a reason for hiding this comment

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

Or is there a reason to keep it intentionally vague?

Suggested change
others: new ConcurrencyQueue(config.getEnv('executions.concurrency')),
production: new ConcurrencyQueue(config.getEnv('executions.concurrency')),

Comment on lines 204 to 208
getRunningExecutionIds() {
const executions = Object.entries(this.activeExecutions);
return executions.filter(([, value]) => value.status === 'running').map(([id]) => id);
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Are we using this?

@@ -0,0 +1,30 @@
export class ConcurrencyQueue {
private waiting: Array<[string, () => void]> = [];
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. readonly
  2. waiting is an execution status, is there a better term? paused? rateLimited? throttled? onHold?
  3. This is a nice use case for labeled tuples: Array<[executionId: string, resolveFn: () => void]> = [];


constructor(private capacity: number) {}

async enqueue(id: string) {
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
async enqueue(id: string) {
async enqueue(executionId: string) {

});

describe('ConcurrencyQueue', () => {
test('should limit concurrency', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

We're testing state at various Ts but we're not testing that the concurrency limit is being enforced?

@@ -518,6 +518,7 @@
"executionDetails.confirmMessage.headline": "Delete Execution?",
"executionDetails.confirmMessage.message": "Are you sure that you want to delete the current execution?",
"executionDetails.deleteExecution": "Delete this execution",
"executionDetails.enqueuedMessage": "Execution hasn't started yet.",
Copy link
Contributor

Choose a reason for hiding this comment

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

As a user, I'd like to know why the execution hasn't started - if there's a problem or if this is behaving as intended, especially if I'm unaware of the concurrency capacity and I might be seeing this message for a while. Execution was rate-limited? Might need product input.

@@ -558,6 +559,7 @@
"executionsList.confirmMessage.message": "Are you sure that you want to delete the {numSelected} selected execution(s)?",
"executionsList.clearSelection": "Clear selection",
"executionsList.error": "Failed",
"executionsList.enqueued": "Enqueued",
Copy link
Contributor

Choose a reason for hiding this comment

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

A user not on queue mode might be confused by the reference to an implementation detail. Rate limited? Might need product input.

@@ -162,22 +200,18 @@ export class WorkflowRunner {
this.activeExecutions.attachResponsePromise(executionId, responsePromise);
}

if (this.executionsMode === 'queue' && data.executionMode !== 'manual') {
const runInMainProcess = this.executionsMode !== 'queue' || data.executionMode === 'manual';
if (!runInMainProcess) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's flip for readability?

Comment on lines +7 to +9
afterAll(() => {
jest.useRealTimers();
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this needed? If we don't do this, fake timers will bleed over to other suites?

@netroy
Copy link
Member Author

netroy commented Feb 8, 2024

jumping back on this now.

@ivov
Copy link
Contributor

ivov commented Feb 9, 2024

More ideas:

  • Prevent users from setting any concurrency limit lower than 1. As an exception, consider supporting -1 for unlimited concurrency, with a warning.
  • Add Public API endpoints to read queue size and update the production concurrency limit, so cloud can dynamically adjust it for heavy users in lower plans.
  • Priority queuing for selected workflows?
  • Warn on startup if concurrency limit is too high for system specs.
  • Add a timestamp to the queued item and on resuming warn if the time elapsed between enqueueing and resuming exceeded a threshold.

@netroy netroy force-pushed the rudimentary-concurrency branch 2 times, most recently from 767c1ce to f4baf87 Compare February 20, 2024 14:40
@netroy netroy force-pushed the rudimentary-concurrency branch 2 times, most recently from 5e191f7 to 3406f40 Compare February 28, 2024 16:40
@netroy
Copy link
Member Author

netroy commented Jun 3, 2024

closing in favor of #9453

@netroy netroy closed this Jun 3, 2024
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
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants