diff --git a/libraries/botbuilder-dialogs-adaptive-testing/tests/conditionals.test.js b/libraries/botbuilder-dialogs-adaptive-testing/tests/conditionals.test.js index 9d5bcdd9c0..8cb99ca21f 100644 --- a/libraries/botbuilder-dialogs-adaptive-testing/tests/conditionals.test.js +++ b/libraries/botbuilder-dialogs-adaptive-testing/tests/conditionals.test.js @@ -44,4 +44,12 @@ describe('ConditionalsTests', function () { it('OnRepromptDialog', async function () { await TestUtils.runTestScript(resourceExplorer, 'ConditionalsTests_OnRepromptDialog'); }); + + it('OnError loop limit', async function () { + await TestUtils.runTestScript(resourceExplorer, 'ConditionalsTests_OnErrorLoop'); + }); + + it('OnError default loop limit', async function () { + await TestUtils.runTestScript(resourceExplorer, 'ConditionalsTests_OnErrorLoopDefaultLimit'); + }); }); diff --git a/libraries/botbuilder-dialogs-adaptive-testing/tests/resources/ConditionalsTests/ConditionalsTests_OnErrorLoop.test.dialog b/libraries/botbuilder-dialogs-adaptive-testing/tests/resources/ConditionalsTests/ConditionalsTests_OnErrorLoop.test.dialog new file mode 100644 index 0000000000..59293c4517 --- /dev/null +++ b/libraries/botbuilder-dialogs-adaptive-testing/tests/resources/ConditionalsTests/ConditionalsTests_OnErrorLoop.test.dialog @@ -0,0 +1,65 @@ +{ + "$schema": "../../../tests.schema", + "$kind": "Microsoft.Test.Script", + "dialog": { + "$kind": "Microsoft.AdaptiveDialog", + "id": "ErrorLoop", + "autoEndDialog": true, + "defaultResultProperty": "dialog.result", + "triggers": [ + { + "$kind": "Microsoft.OnBeginDialog", + "actions": [ + { + "$kind": "Microsoft.SendActivity", + "activity": "Throw Exception in BeginDialog." + }, + { + "$kind": "Microsoft.ThrowException", + "errorValue": "Exception in BeginDialog." + } + ] + }, + { + "$kind": "Microsoft.OnError", + "actions": [ + { + "$kind": "Microsoft.SendActivity", + "activity": "Throw Exception in OnError." + }, + { + "$kind": "Microsoft.ThrowException", + "errorValue": "Exception in OnError." + } + ], + "executionLimit": 3 + } + ] + }, + "script": [ + { + "$kind": "Microsoft.Test.UserSays", + "text": "hi" + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Throw Exception in BeginDialog." + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Throw Exception in OnError." + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Throw Exception in OnError." + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Throw Exception in OnError." + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Exception in OnError." + } + ] +} \ No newline at end of file diff --git a/libraries/botbuilder-dialogs-adaptive-testing/tests/resources/ConditionalsTests/ConditionalsTests_OnErrorLoopDefaultLimit.test.dialog b/libraries/botbuilder-dialogs-adaptive-testing/tests/resources/ConditionalsTests/ConditionalsTests_OnErrorLoopDefaultLimit.test.dialog new file mode 100644 index 0000000000..cad53c9b68 --- /dev/null +++ b/libraries/botbuilder-dialogs-adaptive-testing/tests/resources/ConditionalsTests/ConditionalsTests_OnErrorLoopDefaultLimit.test.dialog @@ -0,0 +1,92 @@ +{ + "$schema": "../../../tests.schema", + "$kind": "Microsoft.Test.Script", + "dialog": { + "$kind": "Microsoft.AdaptiveDialog", + "id": "ErrorLoop", + "autoEndDialog": true, + "defaultResultProperty": "dialog.result", + "triggers": [ + { + "$kind": "Microsoft.OnBeginDialog", + "actions": [ + { + "$kind": "Microsoft.SendActivity", + "activity": "Throw Exception in BeginDialog." + }, + { + "$kind": "Microsoft.ThrowException", + "errorValue": "Exception in BeginDialog." + } + ] + }, + { + "$kind": "Microsoft.OnError", + "actions": [ + { + "$kind": "Microsoft.SendActivity", + "activity": "Throw Exception in OnError." + }, + { + "$kind": "Microsoft.ThrowException", + "errorValue": "Exception in OnError." + } + ] + } + ] + }, + "script": [ + { + "$kind": "Microsoft.Test.UserSays", + "text": "hi" + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Throw Exception in BeginDialog." + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Throw Exception in OnError." + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Throw Exception in OnError." + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Throw Exception in OnError." + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Throw Exception in OnError." + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Throw Exception in OnError." + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Throw Exception in OnError." + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Throw Exception in OnError." + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Throw Exception in OnError." + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Throw Exception in OnError." + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Throw Exception in OnError." + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Exception in OnError." + } + ] +} \ No newline at end of file diff --git a/libraries/botbuilder-dialogs-adaptive/etc/botbuilder-dialogs-adaptive.api.md b/libraries/botbuilder-dialogs-adaptive/etc/botbuilder-dialogs-adaptive.api.md index ae1372d70b..a2bd84e53d 100644 --- a/libraries/botbuilder-dialogs-adaptive/etc/botbuilder-dialogs-adaptive.api.md +++ b/libraries/botbuilder-dialogs-adaptive/etc/botbuilder-dialogs-adaptive.api.md @@ -1794,6 +1794,10 @@ export class OnError extends OnDialogEvent { // (undocumented) static $kind: string; constructor(actions?: Dialog[], condition?: string); + // (undocumented) + currentExecutionLimit: () => number; + execute(actionContext: ActionContext): Promise; + executionLimit: NumberExpression; onCreateChangeList(actionContext: ActionContext, dialogOptions?: any): ActionChangeList; } diff --git a/libraries/botbuilder-dialogs-adaptive/schemas/TriggerConditions/Microsoft.OnError.schema b/libraries/botbuilder-dialogs-adaptive/schemas/TriggerConditions/Microsoft.OnError.schema index ddce686825..950722d963 100644 --- a/libraries/botbuilder-dialogs-adaptive/schemas/TriggerConditions/Microsoft.OnError.schema +++ b/libraries/botbuilder-dialogs-adaptive/schemas/TriggerConditions/Microsoft.OnError.schema @@ -3,5 +3,16 @@ "$role": [ "implements(Microsoft.ITrigger)", "extends(Microsoft.OnCondition)" ], "title": "On error", "description": "Action to perform when an 'Error' dialog event occurs.", - "type": "object" + "type": "object", + "properties": { + "executionLimit": { + "$ref": "schema:#/definitions/numberExpression", + "title": "Execution limit", + "description": "Number of executions allowed for this trigger. Used to break loops in case of errors inside the trigger.", + "examples": [ + 3, + "=f(x)" + ] + } + } } diff --git a/libraries/botbuilder-dialogs-adaptive/schemas/TriggerConditions/Microsoft.OnError.uischema b/libraries/botbuilder-dialogs-adaptive/schemas/TriggerConditions/Microsoft.OnError.uischema index 88ae962e39..f6ab4629dc 100644 --- a/libraries/botbuilder-dialogs-adaptive/schemas/TriggerConditions/Microsoft.OnError.uischema +++ b/libraries/botbuilder-dialogs-adaptive/schemas/TriggerConditions/Microsoft.OnError.uischema @@ -3,7 +3,8 @@ "form": { "order": [ "condition", - "*" + "*", + "executionLimit" ], "hidden": [ "actions" diff --git a/libraries/botbuilder-dialogs-adaptive/src/conditions/onError.ts b/libraries/botbuilder-dialogs-adaptive/src/conditions/onError.ts index 6e19d1e207..c7f86c88db 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/conditions/onError.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/conditions/onError.ts @@ -5,12 +5,13 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ -import { Dialog } from 'botbuilder-dialogs'; +import { Dialog, TurnPath } from 'botbuilder-dialogs'; import { OnDialogEvent } from './onDialogEvent'; import { ActionContext } from '../actionContext'; import { AdaptiveEvents } from '../adaptiveEvents'; import { ActionChangeList } from '../actionChangeList'; import { ActionChangeType } from '../actionChangeType'; +import { NumberExpression } from 'adaptive-expressions'; /** * Actions triggered when an error event has been emitted. @@ -18,6 +19,11 @@ import { ActionChangeType } from '../actionChangeType'; export class OnError extends OnDialogEvent { static $kind = 'Microsoft.OnError'; + /** + * Gets or sets the number of executions allowed. Used to avoid infinite loops in case of error (OPTIONAL). + */ + executionLimit: NumberExpression = new NumberExpression(0); + /** * Initializes a new instance of the [OnError](xref:botbuilder-dialogs-adaptive.OnError) class. * @@ -28,6 +34,20 @@ export class OnError extends OnDialogEvent { super(AdaptiveEvents.error, actions, condition); } + /** + * Method called to execute the condition's actions. + * + * @param actionContext Context. + * @returns A promise with plan change list. + */ + async execute(actionContext: ActionContext): Promise { + const limit = this.currentExecutionLimit(); + + actionContext.state.setValue(TurnPath.executionLimit, limit); + + return await super.execute(actionContext); + } + /** * Called when a change list is created. * @@ -43,4 +63,12 @@ export class OnError extends OnDialogEvent { changeList.changeType = ActionChangeType.replaceSequence; return changeList; } + + currentExecutionLimit = function (): number { + if (this.executionLimit > 0) { + return this.executionLimit; + } + //10 is the default number of executions we'll allow before breaking the loop. + return 10; + }; } diff --git a/libraries/botbuilder-dialogs/etc/botbuilder-dialogs.api.md b/libraries/botbuilder-dialogs/etc/botbuilder-dialogs.api.md index 98c4cadac0..91b4b291c0 100644 --- a/libraries/botbuilder-dialogs/etc/botbuilder-dialogs.api.md +++ b/libraries/botbuilder-dialogs/etc/botbuilder-dialogs.api.md @@ -823,6 +823,8 @@ export class TurnPath { // (undocumented) static readonly dialogEvent = "turn.dialogEvent"; // (undocumented) + static readonly executionLimit = "turn.executionLimit"; + // (undocumented) static readonly interrupted = "turn.interrupted"; // (undocumented) static readonly lastResult = "turn.lastresult"; diff --git a/libraries/botbuilder-dialogs/src/dialogHelper.ts b/libraries/botbuilder-dialogs/src/dialogHelper.ts index 962d23b363..7324849c84 100644 --- a/libraries/botbuilder-dialogs/src/dialogHelper.ts +++ b/libraries/botbuilder-dialogs/src/dialogHelper.ts @@ -94,6 +94,9 @@ export async function internalRun( // or we have had an exception AND there was an OnError action which captured the error. We need to continue the // turn based on the actions the OnError handler introduced. let endOfTurn = false; + let errorHandlerCalled = 0; + const TURN_STATE = 'turn'; + while (!endOfTurn) { try { dialogTurnResult = await innerRun(context, dialogId, dialogContext); @@ -101,8 +104,19 @@ export async function internalRun( // turn successfully completed, break the loop endOfTurn = true; } catch (err) { + errorHandlerCalled++; + // fire error event, bubbling from the leaf. - const handled = await dialogContext.emitEvent(DialogEvents.error, err, true, true); + let handled = await dialogContext.emitEvent(DialogEvents.error, err, true, true); + + let executionLimit = 0; + executionLimit = context.turnState.get(TURN_STATE).executionLimit; + + if (executionLimit > 0 && errorHandlerCalled > executionLimit) { + // if the errorHandler has being called multiple times, there's an error inside the onError. + // We should throw the exception and break the loop. + handled = false; + } if (!handled) { // error was NOT handled, throw the exception and end the turn. diff --git a/libraries/botbuilder-dialogs/src/memory/turnPath.ts b/libraries/botbuilder-dialogs/src/memory/turnPath.ts index 497618c48a..c0beb61f02 100644 --- a/libraries/botbuilder-dialogs/src/memory/turnPath.ts +++ b/libraries/botbuilder-dialogs/src/memory/turnPath.ts @@ -45,4 +45,7 @@ export class TurnPath { /// This is a bool which if set means that the turncontext.activity has been consumed by some component in the system. static readonly activityProcessed = 'turn.activityProcessed'; + + /// Used to limit the execution of a trigger avoiding infinite loops in case of errors. + static readonly executionLimit = 'turn.executionLimit'; } diff --git a/libraries/tests.schema b/libraries/tests.schema index 53c20ad7a0..84f83dc9de 100644 --- a/libraries/tests.schema +++ b/libraries/tests.schema @@ -6260,6 +6260,15 @@ } }, "properties": { + "executionLimit": { + "$ref": "#/definitions/numberExpression", + "title": "Execution limit", + "description": "Number of executions allowed for this trigger. Used to break loops in case of errors inside the trigger.", + "examples": [ + 3, + "=f(x)" + ] + }, "condition": { "$ref": "#/definitions/condition", "title": "Condition", diff --git a/libraries/tests.uischema b/libraries/tests.uischema index 9d11eb27e8..aed50510ec 100644 --- a/libraries/tests.uischema +++ b/libraries/tests.uischema @@ -851,7 +851,8 @@ "label": "Error occurred", "order": [ "condition", - "*" + "*", + "executionLimit" ], "subtitle": "Error event" }