mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
fix(editor): Fix race condition for updating node and workflow execution status (#14353)
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
@@ -279,7 +279,7 @@ describe('NodeExecuteButton', () => {
|
|||||||
it('stops execution when clicking button while workflow is running', async () => {
|
it('stops execution when clicking button while workflow is running', async () => {
|
||||||
uiStore.isActionActive.workflowRunning = true;
|
uiStore.isActionActive.workflowRunning = true;
|
||||||
nodeTypesStore.isTriggerNode = () => true;
|
nodeTypesStore.isTriggerNode = () => true;
|
||||||
workflowsStore.activeExecutionId = 'test-execution-id';
|
workflowsStore.setActiveExecutionId('test-execution-id');
|
||||||
workflowsStore.isNodeExecuting.mockReturnValue(true);
|
workflowsStore.isNodeExecuting.mockReturnValue(true);
|
||||||
workflowsStore.getNodeByName.mockReturnValue(
|
workflowsStore.getNodeByName.mockReturnValue(
|
||||||
mockNode({ name: 'test-node', type: SET_NODE_TYPE }),
|
mockNode({ name: 'test-node', type: SET_NODE_TYPE }),
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export function useCanvasNode() {
|
|||||||
const executionStatus = computed(() => data.value.execution.status);
|
const executionStatus = computed(() => data.value.execution.status);
|
||||||
const executionWaiting = computed(() => data.value.execution.waiting);
|
const executionWaiting = computed(() => data.value.execution.waiting);
|
||||||
const executionRunning = computed(() => data.value.execution.running);
|
const executionRunning = computed(() => data.value.execution.running);
|
||||||
const executionRunningThrottled = refThrottled(executionRunning, 50);
|
const executionRunningThrottled = refThrottled(executionRunning, 300);
|
||||||
|
|
||||||
const runDataOutputMap = computed(() => data.value.runData.outputMap);
|
const runDataOutputMap = computed(() => data.value.runData.outputMap);
|
||||||
const runDataIterations = computed(() => data.value.runData.iterations);
|
const runDataIterations = computed(() => data.value.runData.iterations);
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ describe('usePushConnection()', () => {
|
|||||||
const workflowId = 'abc';
|
const workflowId = 'abc';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
workflowsStore.activeExecutionId = executionId;
|
workflowsStore.setActiveExecutionId(executionId);
|
||||||
uiStore.isActionActive.workflowRunning = true;
|
uiStore.isActionActive.workflowRunning = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -239,13 +239,16 @@ describe('usePushConnection()', () => {
|
|||||||
it("enqueues messages if we don't have the active execution id yet", async () => {
|
it("enqueues messages if we don't have the active execution id yet", async () => {
|
||||||
uiStore.isActionActive.workflowRunning = true;
|
uiStore.isActionActive.workflowRunning = true;
|
||||||
const event: PushMessage = {
|
const event: PushMessage = {
|
||||||
type: 'executionStarted',
|
type: 'nodeExecuteAfter',
|
||||||
data: {
|
data: {
|
||||||
executionId: '1',
|
executionId: '1',
|
||||||
mode: 'manual',
|
nodeName: 'Node',
|
||||||
startedAt: new Date(),
|
data: {
|
||||||
workflowId: '1',
|
executionIndex: 0,
|
||||||
flattedRunData: stringify({}),
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
source: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -281,7 +284,7 @@ describe('usePushConnection()', () => {
|
|||||||
workflowId: '1',
|
workflowId: '1',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
workflowsStore.activeExecutionId = event.data.executionId;
|
workflowsStore.setActiveExecutionId(event.data.executionId);
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
const result = await pushConnection.pushMessageReceived(event);
|
const result = await pushConnection.pushMessageReceived(event);
|
||||||
|
|||||||
@@ -151,6 +151,12 @@ export function usePushConnection({ router }: { router: ReturnType<typeof useRou
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (receivedData.type === 'executionStarted') {
|
||||||
|
if (!workflowsStore.activeExecutionId) {
|
||||||
|
workflowsStore.setActiveExecutionId(receivedData.data.executionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
receivedData.type === 'nodeExecuteAfter' ||
|
receivedData.type === 'nodeExecuteAfter' ||
|
||||||
receivedData.type === 'nodeExecuteBefore' ||
|
receivedData.type === 'nodeExecuteBefore' ||
|
||||||
@@ -228,8 +234,7 @@ export function usePushConnection({ router }: { router: ReturnType<typeof useRou
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { executionId } = receivedData.data;
|
const { executionId } = receivedData.data;
|
||||||
const { activeExecutionId } = workflowsStore;
|
if (executionId !== workflowsStore.activeExecutionId) {
|
||||||
if (executionId !== activeExecutionId) {
|
|
||||||
// The workflow which did finish execution did either not get started
|
// The workflow which did finish execution did either not get started
|
||||||
// by this session or we do not have the execution id yet.
|
// by this session or we do not have the execution id yet.
|
||||||
if (isRetry !== true) {
|
if (isRetry !== true) {
|
||||||
@@ -322,7 +327,7 @@ export function usePushConnection({ router }: { router: ReturnType<typeof useRou
|
|||||||
runDataExecutedErrorMessage = i18n.baseText(
|
runDataExecutedErrorMessage = i18n.baseText(
|
||||||
'executionsList.showMessage.stopExecution.message',
|
'executionsList.showMessage.stopExecution.message',
|
||||||
{
|
{
|
||||||
interpolate: { activeExecutionId },
|
interpolate: { activeExecutionId: workflowsStore.activeExecutionId },
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -523,6 +528,8 @@ export function usePushConnection({ router }: { router: ReturnType<typeof useRou
|
|||||||
iRunExecutionData.resultData.runData[lastNodeExecuted][0].data!.main[0]!.length;
|
iRunExecutionData.resultData.runData[lastNodeExecuted][0].data!.main[0]!.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
workflowsStore.setActiveExecutionId(null);
|
||||||
|
|
||||||
void useExternalHooks().run('pushConnection.executionFinished', {
|
void useExternalHooks().run('pushConnection.executionFinished', {
|
||||||
itemsCount,
|
itemsCount,
|
||||||
nodeName: iRunExecutionData.resultData.lastNodeExecuted,
|
nodeName: iRunExecutionData.resultData.lastNodeExecuted,
|
||||||
@@ -578,7 +585,7 @@ export function usePushConnection({ router }: { router: ReturnType<typeof useRou
|
|||||||
|
|
||||||
if (pushData.workflowId === workflowsStore.workflowId) {
|
if (pushData.workflowId === workflowsStore.workflowId) {
|
||||||
workflowsStore.executionWaitingForWebhook = false;
|
workflowsStore.executionWaitingForWebhook = false;
|
||||||
workflowsStore.activeExecutionId = pushData.executionId;
|
workflowsStore.setActiveExecutionId(pushData.executionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void processWaitingPushMessages();
|
void processWaitingPushMessages();
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ import { setActivePinia } from 'pinia';
|
|||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import type router from 'vue-router';
|
import type router from 'vue-router';
|
||||||
import {
|
import { ExpressionError, NodeConnectionTypes } from 'n8n-workflow';
|
||||||
ExpressionError,
|
import type {
|
||||||
type IPinData,
|
IPinData,
|
||||||
type IRunData,
|
IRunData,
|
||||||
type Workflow,
|
Workflow,
|
||||||
type IExecuteData,
|
IExecuteData,
|
||||||
type ITaskData,
|
ITaskData,
|
||||||
NodeConnectionTypes,
|
INodeConnections,
|
||||||
type INodeConnections,
|
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
||||||
@@ -26,20 +25,23 @@ import { usePushConnectionStore } from '@/stores/pushConnection.store';
|
|||||||
import { createTestNode, createTestWorkflow } from '@/__tests__/mocks';
|
import { createTestNode, createTestWorkflow } from '@/__tests__/mocks';
|
||||||
import { waitFor } from '@testing-library/vue';
|
import { waitFor } from '@testing-library/vue';
|
||||||
|
|
||||||
vi.mock('@/stores/workflows.store', () => ({
|
vi.mock('@/stores/workflows.store', async () => {
|
||||||
useWorkflowsStore: vi.fn().mockReturnValue({
|
const storeState: Partial<ReturnType<typeof useWorkflowsStore>> & {
|
||||||
|
activeExecutionId: string | null;
|
||||||
|
} = {
|
||||||
allNodes: [],
|
allNodes: [],
|
||||||
runWorkflow: vi.fn(),
|
runWorkflow: vi.fn(),
|
||||||
subWorkflowExecutionError: null,
|
subWorkflowExecutionError: null,
|
||||||
getWorkflowRunData: null,
|
getWorkflowRunData: null,
|
||||||
|
workflowExecutionData: null,
|
||||||
setWorkflowExecutionData: vi.fn(),
|
setWorkflowExecutionData: vi.fn(),
|
||||||
activeExecutionId: null,
|
activeExecutionId: null,
|
||||||
|
previousExecutionId: null,
|
||||||
nodesIssuesExist: false,
|
nodesIssuesExist: false,
|
||||||
executionWaitingForWebhook: false,
|
executionWaitingForWebhook: false,
|
||||||
getCurrentWorkflow: vi.fn().mockReturnValue({ id: '123' }),
|
getCurrentWorkflow: vi.fn().mockReturnValue({ id: '123' }),
|
||||||
getNodeByName: vi.fn(),
|
getNodeByName: vi.fn(),
|
||||||
getExecution: vi.fn(),
|
getExecution: vi.fn(),
|
||||||
nodeIssuesExit: vi.fn(),
|
|
||||||
checkIfNodeHasChatParent: vi.fn(),
|
checkIfNodeHasChatParent: vi.fn(),
|
||||||
getParametersLastUpdate: vi.fn(),
|
getParametersLastUpdate: vi.fn(),
|
||||||
getPinnedDataLastUpdate: vi.fn(),
|
getPinnedDataLastUpdate: vi.fn(),
|
||||||
@@ -47,8 +49,15 @@ vi.mock('@/stores/workflows.store', () => ({
|
|||||||
incomingConnectionsByNodeName: vi.fn(),
|
incomingConnectionsByNodeName: vi.fn(),
|
||||||
outgoingConnectionsByNodeName: vi.fn(),
|
outgoingConnectionsByNodeName: vi.fn(),
|
||||||
markExecutionAsStopped: vi.fn(),
|
markExecutionAsStopped: vi.fn(),
|
||||||
}),
|
setActiveExecutionId: vi.fn((id: string | null) => {
|
||||||
}));
|
storeState.activeExecutionId = id;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
useWorkflowsStore: vi.fn().mockReturnValue(storeState),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock('@/stores/pushConnection.store', () => ({
|
vi.mock('@/stores/pushConnection.store', () => ({
|
||||||
usePushConnectionStore: vi.fn().mockReturnValue({
|
usePushConnectionStore: vi.fn().mockReturnValue({
|
||||||
@@ -151,6 +160,7 @@ describe('useRunWorkflow({ router })', () => {
|
|||||||
|
|
||||||
const mockResponse = { executionId: '123', waitingForWebhook: false };
|
const mockResponse = { executionId: '123', waitingForWebhook: false };
|
||||||
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockResponse);
|
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockResponse);
|
||||||
|
vi.mocked(workflowsStore).setActiveExecutionId('123');
|
||||||
|
|
||||||
const response = await runWorkflowApi({} as IStartRunData);
|
const response = await runWorkflowApi({} as IStartRunData);
|
||||||
|
|
||||||
@@ -692,7 +702,7 @@ describe('useRunWorkflow({ router })', () => {
|
|||||||
|
|
||||||
workflowsStore.workflowExecutionData = executionData;
|
workflowsStore.workflowExecutionData = executionData;
|
||||||
workflowsStore.activeWorkflows = ['test-wf-id'];
|
workflowsStore.activeWorkflows = ['test-wf-id'];
|
||||||
workflowsStore.activeExecutionId = 'test-exec-id';
|
workflowsStore.setActiveExecutionId('test-exec-id');
|
||||||
|
|
||||||
// Exercise - don't wait for returned promise to resolve
|
// Exercise - don't wait for returned promise to resolve
|
||||||
void runWorkflowComposable.stopCurrentExecution();
|
void runWorkflowComposable.stopCurrentExecution();
|
||||||
|
|||||||
@@ -78,8 +78,11 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.executionId !== undefined) {
|
if (
|
||||||
workflowsStore.activeExecutionId = response.executionId;
|
response.executionId !== undefined &&
|
||||||
|
workflowsStore.previousExecutionId !== response.executionId
|
||||||
|
) {
|
||||||
|
workflowsStore.setActiveExecutionId(response.executionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.waitingForWebhook === true && useWorkflowsStore().nodesIssuesExist) {
|
if (response.waitingForWebhook === true && useWorkflowsStore().nodesIssuesExist) {
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
|||||||
const chatSessionCredType = ref<ICredentialType | undefined>();
|
const chatSessionCredType = ref<ICredentialType | undefined>();
|
||||||
const chatSessionError = ref<ChatRequest.ErrorContext | undefined>();
|
const chatSessionError = ref<ChatRequest.ErrorContext | undefined>();
|
||||||
const currentSessionId = ref<string | undefined>();
|
const currentSessionId = ref<string | undefined>();
|
||||||
const currentSessionActiveExecutionId = ref<string | undefined>();
|
const currentSessionActiveExecutionId = ref<string | null>(null);
|
||||||
const currentSessionWorkflowId = ref<string | undefined>();
|
const currentSessionWorkflowId = ref<string | undefined>();
|
||||||
const lastUnread = ref<ChatUI.AssistantMessage | undefined>();
|
const lastUnread = ref<ChatUI.AssistantMessage | undefined>();
|
||||||
const nodeExecutionStatus = ref<NodeExecutionStatus>('not_executed');
|
const nodeExecutionStatus = ref<NodeExecutionStatus>('not_executed');
|
||||||
@@ -125,7 +125,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
|||||||
currentSessionId.value = undefined;
|
currentSessionId.value = undefined;
|
||||||
chatSessionError.value = undefined;
|
chatSessionError.value = undefined;
|
||||||
lastUnread.value = undefined;
|
lastUnread.value = undefined;
|
||||||
currentSessionActiveExecutionId.value = undefined;
|
currentSessionActiveExecutionId.value = null;
|
||||||
suggestions.value = {};
|
suggestions.value = {};
|
||||||
nodeExecutionStatus.value = 'not_executed';
|
nodeExecutionStatus.value = 'not_executed';
|
||||||
chatSessionCredType.value = undefined;
|
chatSessionCredType.value = undefined;
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
|||||||
const workflowExecutionData = ref<IExecutionResponse | null>(null);
|
const workflowExecutionData = ref<IExecutionResponse | null>(null);
|
||||||
const workflowExecutionPairedItemMappings = ref<Record<string, Set<string>>>({});
|
const workflowExecutionPairedItemMappings = ref<Record<string, Set<string>>>({});
|
||||||
const activeExecutionId = ref<string | null>(null);
|
const activeExecutionId = ref<string | null>(null);
|
||||||
|
const previousExecutionId = ref<string | null>(null);
|
||||||
const subWorkflowExecutionError = ref<Error | null>(null);
|
const subWorkflowExecutionError = ref<Error | null>(null);
|
||||||
const executionWaitingForWebhook = ref(false);
|
const executionWaitingForWebhook = ref(false);
|
||||||
const workflowsById = ref<Record<string, IWorkflowDb>>({});
|
const workflowsById = ref<Record<string, IWorkflowDb>>({});
|
||||||
@@ -289,6 +290,11 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
|||||||
Workflow.getConnectionsByDestination(workflow.value.connections),
|
Workflow.getConnectionsByDestination(workflow.value.connections),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function setActiveExecutionId(id: string | null) {
|
||||||
|
previousExecutionId.value = activeExecutionId.value;
|
||||||
|
activeExecutionId.value = id;
|
||||||
|
}
|
||||||
|
|
||||||
function getWorkflowResultDataByNodeName(nodeName: string): ITaskData[] | null {
|
function getWorkflowResultDataByNodeName(nodeName: string): ITaskData[] | null {
|
||||||
if (getWorkflowRunData.value === null) {
|
if (getWorkflowRunData.value === null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -615,7 +621,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
|||||||
setWorkflowSettings({ ...defaults.settings });
|
setWorkflowSettings({ ...defaults.settings });
|
||||||
setWorkflowTagIds([]);
|
setWorkflowTagIds([]);
|
||||||
|
|
||||||
activeExecutionId.value = null;
|
setActiveExecutionId(null);
|
||||||
executingNode.value.length = 0;
|
executingNode.value.length = 0;
|
||||||
executionWaitingForWebhook.value = false;
|
executionWaitingForWebhook.value = false;
|
||||||
}
|
}
|
||||||
@@ -1687,7 +1693,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function markExecutionAsStopped() {
|
function markExecutionAsStopped() {
|
||||||
activeExecutionId.value = null;
|
setActiveExecutionId(null);
|
||||||
clearNodeExecutionQueue();
|
clearNodeExecutionQueue();
|
||||||
executionWaitingForWebhook.value = false;
|
executionWaitingForWebhook.value = false;
|
||||||
uiStore.removeActiveAction('workflowRunning');
|
uiStore.removeActiveAction('workflowRunning');
|
||||||
@@ -1711,7 +1717,9 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
|||||||
currentWorkflowExecutions,
|
currentWorkflowExecutions,
|
||||||
workflowExecutionData,
|
workflowExecutionData,
|
||||||
workflowExecutionPairedItemMappings,
|
workflowExecutionPairedItemMappings,
|
||||||
activeExecutionId,
|
activeExecutionId: computed(() => activeExecutionId.value),
|
||||||
|
previousExecutionId: computed(() => previousExecutionId.value),
|
||||||
|
setActiveExecutionId,
|
||||||
subWorkflowExecutionError,
|
subWorkflowExecutionError,
|
||||||
executionWaitingForWebhook,
|
executionWaitingForWebhook,
|
||||||
executingNode,
|
executingNode,
|
||||||
|
|||||||
Reference in New Issue
Block a user