mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
✨ Make it possible to retry node on error
This commit is contained in:
parent
9c0b1a9cec
commit
32204d35d1
|
@ -360,7 +360,34 @@ export class WorkflowExecute {
|
||||||
// is very slow so only do if needed
|
// is very slow so only do if needed
|
||||||
startTime = new Date().getTime();
|
startTime = new Date().getTime();
|
||||||
|
|
||||||
|
let maxTries = 1;
|
||||||
|
if (executionData.node.retryOnFail === true) {
|
||||||
|
// TODO: Remove the hardcoded default-values here and also in NodeSettings.vue
|
||||||
|
maxTries = Math.min(5, Math.max(2, executionData.node.maxTries || 3));
|
||||||
|
}
|
||||||
|
|
||||||
|
let waitBetweenTries = 0;
|
||||||
|
if (executionData.node.retryOnFail === true) {
|
||||||
|
// TODO: Remove the hardcoded default-values here and also in NodeSettings.vue
|
||||||
|
waitBetweenTries = Math.min(5000, Math.max(0, executionData.node.waitBetweenTries || 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let tryIndex = 0; tryIndex < maxTries; tryIndex++) {
|
||||||
try {
|
try {
|
||||||
|
if (tryIndex !== 0) {
|
||||||
|
// Reset executionError from previous error try
|
||||||
|
executionError = undefined;
|
||||||
|
if (waitBetweenTries !== 0) {
|
||||||
|
// TODO: Improve that in the future and check if other nodes can
|
||||||
|
// be executed in the meantime
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve();
|
||||||
|
}, waitBetweenTries);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
runExecutionData.resultData.lastNodeExecuted = executionData.node.name;
|
runExecutionData.resultData.lastNodeExecuted = executionData.node.name;
|
||||||
nodeSuccessData = await workflow.runNode(executionData.node, JSON.parse(JSON.stringify(executionData.data)), runExecutionData, runIndex, this.additionalData, NodeExecuteFunctions, this.mode);
|
nodeSuccessData = await workflow.runNode(executionData.node, JSON.parse(JSON.stringify(executionData.data)), runExecutionData, runIndex, this.additionalData, NodeExecuteFunctions, this.mode);
|
||||||
|
|
||||||
|
@ -368,15 +395,17 @@ export class WorkflowExecute {
|
||||||
// If null gets returned it means that the node did succeed
|
// If null gets returned it means that the node did succeed
|
||||||
// but did not have any data. So the branch should end
|
// but did not have any data. So the branch should end
|
||||||
// (meaning the nodes afterwards should not be processed)
|
// (meaning the nodes afterwards should not be processed)
|
||||||
continue;
|
continue executionLoop;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
executionError = {
|
executionError = {
|
||||||
message: error.message,
|
message: error.message,
|
||||||
stack: error.stack,
|
stack: error.stack,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add the data to return to the user
|
// Add the data to return to the user
|
||||||
// (currently does not get cloned as data does not get changed, maybe later we should do that?!?!)
|
// (currently does not get cloned as data does not get changed, maybe later we should do that?!?!)
|
||||||
|
|
|
@ -24,34 +24,7 @@
|
||||||
</div>
|
</div>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="Node">
|
<el-tab-pane label="Node">
|
||||||
|
<parameter-input-list :parameters="nodeSettings" :hideDelete="true" :nodeValues="nodeValues" path="" @valueChanged="valueChanged" />
|
||||||
<parameter-input-full
|
|
||||||
:parameter="nodeSettingsParameterColor"
|
|
||||||
:value="nodeValues.color"
|
|
||||||
:displayOptions="false"
|
|
||||||
path="color"
|
|
||||||
@valueChanged="valueChanged"
|
|
||||||
/>
|
|
||||||
<div v-if="!isColorDefaultValue" class="color-reset-button-wrapper">
|
|
||||||
<font-awesome-icon icon="redo" @click="resetColor('color')" class="color-reset-button clickable" title="Reset node color" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<parameter-input-full
|
|
||||||
:parameter="nodeSettingsParameterNotes"
|
|
||||||
:value="nodeValues.notes"
|
|
||||||
:displayOptions="false"
|
|
||||||
path="notes"
|
|
||||||
@valueChanged="valueChanged"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<parameter-input-full
|
|
||||||
:parameter="nodeSettingsParameterContinueOnFail"
|
|
||||||
:value="nodeValues.continueOnFail"
|
|
||||||
:displayOptions="false"
|
|
||||||
path="continueOnFail"
|
|
||||||
@valueChanged="valueChanged"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<parameter-input-list :parameters="parametersSetting" :nodeValues="nodeValues" path="parameters" @valueChanged="valueChanged" />
|
<parameter-input-list :parameters="parametersSetting" :nodeValues="nodeValues" path="parameters" @valueChanged="valueChanged" />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
|
@ -160,10 +133,15 @@ export default mixins(
|
||||||
nodeValues: {
|
nodeValues: {
|
||||||
color: '#ff0000',
|
color: '#ff0000',
|
||||||
continueOnFail: false,
|
continueOnFail: false,
|
||||||
|
retryOnFail: false,
|
||||||
|
maxTries: 3,
|
||||||
|
waitBetweenTries: 1000,
|
||||||
notes: '',
|
notes: '',
|
||||||
parameters: {},
|
parameters: {},
|
||||||
} as INodeParameters,
|
} as INodeParameters,
|
||||||
nodeSettingsParameterNotes: {
|
|
||||||
|
nodeSettings: [
|
||||||
|
{
|
||||||
displayName: 'Notes',
|
displayName: 'Notes',
|
||||||
name: 'notes',
|
name: 'notes',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
@ -171,24 +149,72 @@ export default mixins(
|
||||||
rows: 5,
|
rows: 5,
|
||||||
},
|
},
|
||||||
default: '',
|
default: '',
|
||||||
|
noDataExpression: true,
|
||||||
description: 'Notes to save with the node.',
|
description: 'Notes to save with the node.',
|
||||||
} as INodeProperties,
|
},
|
||||||
|
{
|
||||||
nodeSettingsParameterColor: {
|
|
||||||
displayName: 'Node Color',
|
displayName: 'Node Color',
|
||||||
name: 'color',
|
name: 'color',
|
||||||
type: 'color',
|
type: 'color',
|
||||||
default: '',
|
default: '#ff0000',
|
||||||
|
noDataExpression: true,
|
||||||
description: 'The color of the node in the flow.',
|
description: 'The color of the node in the flow.',
|
||||||
} as INodeProperties,
|
},
|
||||||
|
{
|
||||||
nodeSettingsParameterContinueOnFail: {
|
displayName: 'Retry On Fail',
|
||||||
|
name: 'retryOnFail',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
noDataExpression: true,
|
||||||
|
description: 'If activated it will automatically retry the node again multiple times.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Max. Tries',
|
||||||
|
name: 'maxTries',
|
||||||
|
type: 'number',
|
||||||
|
typeOptions: {
|
||||||
|
minValue: 2,
|
||||||
|
maxValue: 5,
|
||||||
|
},
|
||||||
|
default: 3,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
retryOnFail: [
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
noDataExpression: true,
|
||||||
|
description: 'How often it should try to execute the node before it should fail.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Wait Between Tries',
|
||||||
|
name: 'waitBetweenTries',
|
||||||
|
type: 'number',
|
||||||
|
typeOptions: {
|
||||||
|
minValue: 0,
|
||||||
|
maxValue: 5000,
|
||||||
|
},
|
||||||
|
default: 1000,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
retryOnFail: [
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
noDataExpression: true,
|
||||||
|
description: 'How long to wait between ties. Value in ms.',
|
||||||
|
},
|
||||||
|
{
|
||||||
displayName: 'Continue On Fail',
|
displayName: 'Continue On Fail',
|
||||||
name: 'continueOnFail',
|
name: 'continueOnFail',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
description: 'If set and the node fails the workflow will simply continue running.<br />It will then simply pass through the input data so the workflow has<br />to be set up to handle the case that different data gets returned.',
|
noDataExpression: true,
|
||||||
} as INodeProperties,
|
description: 'If activated and the node fails the workflow will simply continue running.<br />It will then simply pass through the input data so the workflow has<br />to be set up to handle the case that different data gets returned.',
|
||||||
|
},
|
||||||
|
] as INodeProperties[],
|
||||||
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -199,14 +225,6 @@ export default mixins(
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
noOp () {},
|
noOp () {},
|
||||||
resetColor () {
|
|
||||||
const activeNode = this.node as INodeUi;
|
|
||||||
const activeNodeType = this.nodeType;
|
|
||||||
if (activeNodeType !== null) {
|
|
||||||
this.setValue('color', activeNodeType.defaults.color as NodeParameterValue);
|
|
||||||
this.valueChanged({ name: 'color', value: activeNodeType.defaults.color } as IUpdateInformation);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setValue (name: string, value: NodeParameterValue) {
|
setValue (name: string, value: NodeParameterValue) {
|
||||||
const nameParts = name.split('.');
|
const nameParts = name.split('.');
|
||||||
let lastNamePart: string | undefined = nameParts.pop();
|
let lastNamePart: string | undefined = nameParts.pop();
|
||||||
|
@ -405,20 +423,50 @@ export default mixins(
|
||||||
if (this.nodeType !== null) {
|
if (this.nodeType !== null) {
|
||||||
this.nodeValid = true;
|
this.nodeValid = true;
|
||||||
|
|
||||||
|
const foundNodeSettings = [];
|
||||||
if (this.node.color) {
|
if (this.node.color) {
|
||||||
|
foundNodeSettings.push('color');
|
||||||
Vue.set(this.nodeValues, 'color', this.node.color);
|
Vue.set(this.nodeValues, 'color', this.node.color);
|
||||||
} else {
|
|
||||||
Vue.set(this.nodeValues, 'color', '#ff0000');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.node.notes) {
|
if (this.node.notes) {
|
||||||
|
foundNodeSettings.push('notes');
|
||||||
Vue.set(this.nodeValues, 'notes', this.node.notes);
|
Vue.set(this.nodeValues, 'notes', this.node.notes);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.node.continueOnFail) {
|
if (this.node.continueOnFail) {
|
||||||
|
foundNodeSettings.push('continueOnFail');
|
||||||
Vue.set(this.nodeValues, 'continueOnFail', this.node.continueOnFail);
|
Vue.set(this.nodeValues, 'continueOnFail', this.node.continueOnFail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.node.retryOnFail) {
|
||||||
|
foundNodeSettings.push('retryOnFail');
|
||||||
|
Vue.set(this.nodeValues, 'retryOnFail', this.node.retryOnFail);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.node.maxTries) {
|
||||||
|
foundNodeSettings.push('maxTries');
|
||||||
|
Vue.set(this.nodeValues, 'maxTries', this.node.maxTries);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.node.waitBetweenTries) {
|
||||||
|
foundNodeSettings.push('waitBetweenTries');
|
||||||
|
Vue.set(this.nodeValues, 'waitBetweenTries', this.node.waitBetweenTries);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default node settings
|
||||||
|
for (const nodeSetting of this.nodeSettings) {
|
||||||
|
if (!foundNodeSettings.includes(nodeSetting.name)) {
|
||||||
|
// Set default value
|
||||||
|
Vue.set(this.nodeValues, nodeSetting.name, nodeSetting.default);
|
||||||
|
}
|
||||||
|
if (nodeSetting.name === 'color') {
|
||||||
|
// For color also apply the default node color to the node settings
|
||||||
|
nodeSetting.default = this.nodeType.defaults.color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Vue.set(this.nodeValues, 'parameters', JSON.parse(JSON.stringify(this.node.parameters)));
|
Vue.set(this.nodeValues, 'parameters', JSON.parse(JSON.stringify(this.node.parameters)));
|
||||||
} else {
|
} else {
|
||||||
this.nodeValid = false;
|
this.nodeValid = false;
|
||||||
|
|
|
@ -77,8 +77,8 @@
|
||||||
<font-awesome-icon icon="cogs" class="reset-icon clickable" title="Parameter Options"/>
|
<font-awesome-icon icon="cogs" class="reset-icon clickable" title="Parameter Options"/>
|
||||||
</span>
|
</span>
|
||||||
<el-dropdown-menu slot="dropdown">
|
<el-dropdown-menu slot="dropdown">
|
||||||
<el-dropdown-item command="addExpression" v-if="!isValueExpression">Add Expression</el-dropdown-item>
|
<el-dropdown-item command="addExpression" v-if="parameter.noDataExpression !== true && !isValueExpression">Add Expression</el-dropdown-item>
|
||||||
<el-dropdown-item command="removeExpression" v-if="isValueExpression">Remove Expression</el-dropdown-item>
|
<el-dropdown-item command="removeExpression" v-if="parameter.noDataExpression !== true && isValueExpression">Remove Expression</el-dropdown-item>
|
||||||
<el-dropdown-item command="resetValue" :disabled="isDefault" divided>Reset Value</el-dropdown-item>
|
<el-dropdown-item command="resetValue" :disabled="isDefault" divided>Reset Value</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
|
@ -325,6 +325,9 @@ export default mixins(
|
||||||
return this.parameter.default === this.value;
|
return this.parameter.default === this.value;
|
||||||
},
|
},
|
||||||
isValueExpression () {
|
isValueExpression () {
|
||||||
|
if (this.parameter.noDataExpression === true) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (typeof this.value === 'string' && this.value.charAt(0) === '=') {
|
if (typeof this.value === 'string' && this.value.charAt(0) === '=') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -215,6 +215,9 @@ export interface INode {
|
||||||
type: string;
|
type: string;
|
||||||
position: [number, number];
|
position: [number, number];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
retryOnFail?: boolean;
|
||||||
|
maxTries?: number;
|
||||||
|
waitBetweenTries?: number;
|
||||||
continueOnFail?: boolean;
|
continueOnFail?: boolean;
|
||||||
parameters: INodeParameters;
|
parameters: INodeParameters;
|
||||||
credentials?: INodeCredentials;
|
credentials?: INodeCredentials;
|
||||||
|
|
Loading…
Reference in a new issue