From 4d1dd8d170051163a1fe0d8c238b82c7ff7db311 Mon Sep 17 00:00:00 2001 From: paed01 Date: Wed, 12 Jul 2023 10:15:30 +0200 Subject: [PATCH] allow activity to be discarded pending resume fix cancelled spelling --- CHANGELOG.md | 2 +- README.md | 2 +- dist/definition/DefinitionExecution.js | 11 +- dist/events/BoundaryEvent.js | 25 +++- docs/SharedApi.md | 4 + docs/SignalTask.md | 55 +++++++++ docs/TimerEventDefinition.md | 4 +- package.json | 10 +- src/definition/DefinitionExecution.js | 11 +- src/events/BoundaryEvent.js | 14 ++- test/activity/ActivityExecution-test.js | 2 +- .../CancelEventDefinition-test.js | 4 +- .../TimerEventDefinition-test.js | 12 +- test/feature/EventBasedGateway-feature.js | 2 +- test/feature/call-activity-feature.js | 67 ++++++++++- test/feature/issues/issues-feature.js | 2 +- test/feature/timers-feature.js | 108 +++++++++++++++++- 17 files changed, 299 insertions(+), 36 deletions(-) create mode 100644 docs/SignalTask.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 631259ee..97f56c9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -271,7 +271,7 @@ Refactor scripts again # 2.1.0 -Transactions and compensation if canceled. +Transactions and compensation if cancelled. ## Additions - Add support for Transaction diff --git a/README.md b/README.md index df424bf8..ff254d81 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ The following elements are tested and supported. - SignalEventDefinition - throw - catch -- SignalTask +- [SignalTask](/docs/SignalTask.md) - ManualTask - UserTask - [StandardLoopCharacteristics](/docs/LoopCharacteristics.md) diff --git a/dist/definition/DefinitionExecution.js b/dist/definition/DefinitionExecution.js index d71ca3dd..98d07baf 100644 --- a/dist/definition/DefinitionExecution.js +++ b/dist/definition/DefinitionExecution.js @@ -602,7 +602,16 @@ DefinitionExecution.prototype._onCancelCallActivity = function onCancelCallActiv const targetProcess = this.getProcessByExecutionId(bpExecutionId); if (!targetProcess) return; this._debug(`cancel call from <${fromParent.id}.${fromId}> to <${calledElement}>`); - targetProcess.getApi().discard(); + if (!targetProcess.isRunning) { + targetProcess.getApi({ + content: { + id: targetProcess.id, + executionId: targetProcess.executionId + } + }).discard(); + } else { + targetProcess.getApi().discard(); + } }; DefinitionExecution.prototype._onDelegateMessage = function onDelegateMessage(routingKey, executeMessage) { const content = executeMessage.content; diff --git a/dist/events/BoundaryEvent.js b/dist/events/BoundaryEvent.js index 40c50dee..57afb05b 100644 --- a/dist/events/BoundaryEvent.js +++ b/dist/events/BoundaryEvent.js @@ -107,13 +107,28 @@ BoundaryEventBehaviour.prototype._onCompleted = function onCompleted(_, { })); } this[kCompleteContent] = content; - const inbound = this[kExecuteMessage].content.inbound; + const { + inbound, + executionId + } = this[kExecuteMessage].content; const attachedToContent = inbound && inbound[0]; const attachedTo = this.attachedTo; - this.activity.logger.debug(`<${this.executionId} (${this.id})> cancel ${attachedTo.status} activity <${attachedToContent.executionId} (${attachedToContent.id})>`); - attachedTo.getApi({ - content: attachedToContent - }).discard(); + this.activity.logger.debug(`<${executionId} (${this.id})> cancel ${attachedTo.status} activity <${attachedToContent.executionId} (${attachedToContent.id})>`); + if (content.isRecovered && !attachedTo.isRunning) { + const attachedExecuteTag = `_on-attached-execute-${executionId}`; + this[kAttachedTags].push(attachedExecuteTag); + attachedTo.broker.subscribeOnce('execution', '#', () => { + attachedTo.getApi({ + content: attachedToContent + }).discard(); + }, { + consumerTag: attachedExecuteTag + }); + } else { + attachedTo.getApi({ + content: attachedToContent + }).discard(); + } }; BoundaryEventBehaviour.prototype._onAttachedLeave = function onAttachedLeave(_, { content diff --git a/docs/SharedApi.md b/docs/SharedApi.md index 6170d869..0e52f0bf 100644 --- a/docs/SharedApi.md +++ b/docs/SharedApi.md @@ -49,6 +49,10 @@ Arguments: - `options`: optional object with broker message options - `delegate`: optional boolean to delegate the signal to all interested parties +### `fail(error)` + +Fail activity with error. The purpose is to fail user-/signal tasks waiting for user input. The behaviour differs between different type of activities. + ### `stop()` Stop element run. Publishes stop message on element broker `api` exchange. diff --git a/docs/SignalTask.md b/docs/SignalTask.md new file mode 100644 index 00000000..16fcf37a --- /dev/null +++ b/docs/SignalTask.md @@ -0,0 +1,55 @@ +SignalTask +=========== + +Signal-/User-/Manual task behaviour. + +```javascript +import * as elements from 'bpmn-elements'; + +import BpmnModdle from 'bpmn-moddle'; + +import {default as serialize, TypeResolver} from 'moddle-context-serializer'; + +const {Context, Definition} = elements; +const typeResolver = TypeResolver(elements); + +const source = ` + + + + + +`; + +(async () => { + const def = await run(); + const [userTask] = def.getPostponed(); + + userTask.fail(new Error('Custom errror')); +})(); + +async function run() { + const moddleContext = await getModdleContext(source); + const options = { + Logger, + }; + const context = new Context(serialize(moddleContext, typeResolver)); + + const definition = new Definition(context, options); + definition.run(); + return definition; +} + +function getModdleContext(sourceXml) { + const bpmnModdle = new BpmnModdle(); + return bpmnModdle.fromXML(sourceXml.trim()); +} + +function Logger(scope) { + return { + debug: console.debug.bind(console, 'bpmn-elements:' + scope), + error: console.error.bind(console, 'bpmn-elements:' + scope), + warn: console.warn.bind(console, 'bpmn-elements:' + scope), + }; +} +``` diff --git a/docs/TimerEventDefinition.md b/docs/TimerEventDefinition.md index a0647cfd..1199b810 100644 --- a/docs/TimerEventDefinition.md +++ b/docs/TimerEventDefinition.md @@ -22,7 +22,7 @@ Object with properties. A subset: ## `activity.timeout` -Fired when the timer has timed out or was canceled. +Fired when the timer has timed out or was cancelled. Object with `activity.timer` properties and some: @@ -42,7 +42,7 @@ Behaves the same as `timeDuration`. Due date will timeout immediately. An invali Default support for ISO8601 repeating interval. -If another format is used, e.g. cron, the event definition will wait until canceled. There are several modules to handle time cycles and this project tries to keep the number of dependencies to a minimum. +If another format is used, e.g. cron, the event definition will wait until cancelled. There are several modules to handle time cycles and this project tries to keep the number of dependencies to a minimum. # Combined `timeDuration` and `timeDate` diff --git a/package.json b/package.json index 240e4400..a759531b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bpmn-elements", - "version": "11.1.0", + "version": "11.1.1", "description": "Executable workflow elements based on BPMN 2.0", "type": "module", "main": "dist/index.js", @@ -45,9 +45,9 @@ ], "devDependencies": { "@aircall/expression-parser": "^1.0.4", - "@babel/cli": "^7.22.5", - "@babel/core": "^7.22.5", - "@babel/preset-env": "^7.22.5", + "@babel/cli": "^7.22.9", + "@babel/core": "^7.22.9", + "@babel/preset-env": "^7.22.9", "@babel/register": "^7.22.5", "@bonniernews/hot-bev": "^0.4.0", "bpmn-moddle": "^8.0.1", @@ -56,7 +56,7 @@ "chai": "^4.3.7", "chronokinesis": "^5.0.2", "debug": "^4.3.4", - "eslint": "^8.43.0", + "eslint": "^8.44.0", "eslint-plugin-import": "^2.27.5", "got": "^12.6.1", "mocha": "^10.1.0", diff --git a/src/definition/DefinitionExecution.js b/src/definition/DefinitionExecution.js index b9fa3d69..30f7863f 100644 --- a/src/definition/DefinitionExecution.js +++ b/src/definition/DefinitionExecution.js @@ -614,7 +614,16 @@ DefinitionExecution.prototype._onCancelCallActivity = function onCancelCallActiv this._debug(`cancel call from <${fromParent.id}.${fromId}> to <${calledElement}>`); - targetProcess.getApi().discard(); + if (!targetProcess.isRunning) { + targetProcess.getApi({ + content: { + id: targetProcess.id, + executionId: targetProcess.executionId, + }, + }).discard(); + } else { + targetProcess.getApi().discard(); + } }; DefinitionExecution.prototype._onDelegateMessage = function onDelegateMessage(routingKey, executeMessage) { diff --git a/src/events/BoundaryEvent.js b/src/events/BoundaryEvent.js index 6f92c421..8700989a 100644 --- a/src/events/BoundaryEvent.js +++ b/src/events/BoundaryEvent.js @@ -103,13 +103,21 @@ BoundaryEventBehaviour.prototype._onCompleted = function onCompleted(_, {content this[kCompleteContent] = content; - const inbound = this[kExecuteMessage].content.inbound; + const {inbound, executionId} = this[kExecuteMessage].content; const attachedToContent = inbound && inbound[0]; const attachedTo = this.attachedTo; - this.activity.logger.debug(`<${this.executionId} (${this.id})> cancel ${attachedTo.status} activity <${attachedToContent.executionId} (${attachedToContent.id})>`); + this.activity.logger.debug(`<${executionId} (${this.id})> cancel ${attachedTo.status} activity <${attachedToContent.executionId} (${attachedToContent.id})>`); - attachedTo.getApi({content: attachedToContent}).discard(); + if (content.isRecovered && !attachedTo.isRunning) { + const attachedExecuteTag = `_on-attached-execute-${executionId}`; + this[kAttachedTags].push(attachedExecuteTag); + attachedTo.broker.subscribeOnce('execution', '#', () => { + attachedTo.getApi({content: attachedToContent}).discard(); + }, {consumerTag: attachedExecuteTag}); + } else { + attachedTo.getApi({content: attachedToContent}).discard(); + } }; BoundaryEventBehaviour.prototype._onAttachedLeave = function onAttachedLeave(_, {content}) { diff --git a/test/activity/ActivityExecution-test.js b/test/activity/ActivityExecution-test.js index 949f05ac..f48b50e0 100644 --- a/test/activity/ActivityExecution-test.js +++ b/test/activity/ActivityExecution-test.js @@ -1517,7 +1517,7 @@ describe('ActivityExecution', () => { } }); - it('last iteration canceled completes execution', () => { + it('last iteration cancelled completes execution', () => { const task = createActivity(Behaviour); const execution = new ActivityExecution(task); diff --git a/test/eventDefinitions/CancelEventDefinition-test.js b/test/eventDefinitions/CancelEventDefinition-test.js index e48b6498..cb1667de 100644 --- a/test/eventDefinitions/CancelEventDefinition-test.js +++ b/test/eventDefinitions/CancelEventDefinition-test.js @@ -48,7 +48,7 @@ describe('CancelEventDefinition', () => { expect(messages[0].content.parent).to.have.property('executionId', 'theProcess_0'); }); - it('expects cancel with canceled routing key', () => { + it('expects cancel with cancelled routing key', () => { const catchEvent = new CancelEventDefinition(event, { type: 'bpmn:CancelEventDefinition', }); @@ -168,7 +168,7 @@ describe('CancelEventDefinition', () => { }, }); - event.broker.publish('execution', 'execute.canceled.event_1_0', {id: 'atomic', isTransaction: true}); + event.broker.publish('execution', 'execute.cancelled.event_1_0', {id: 'atomic', isTransaction: true}); event.broker.publish('api', 'activity.stop.event_1', {}, {type: 'stop'}); expect(event.broker).to.have.property('consumerCount', 0); diff --git a/test/eventDefinitions/TimerEventDefinition-test.js b/test/eventDefinitions/TimerEventDefinition-test.js index ad4c801d..93b55498 100644 --- a/test/eventDefinitions/TimerEventDefinition-test.js +++ b/test/eventDefinitions/TimerEventDefinition-test.js @@ -894,7 +894,7 @@ describe('TimerEventDefinition', () => { ActivityApi(broker, timerMessage).discard(); }); - it('can be canceled', (done) => { + it('can be cancelled', (done) => { const definition = new TimerEventDefinition(event, { type: 'bpmn:TimerEventDefinition', behaviour: { @@ -1097,7 +1097,7 @@ describe('TimerEventDefinition', () => { after(ck.reset); describe('cancel ' + descr, () => { - it('completes when parent is canceled', (done) => { + it('completes when parent is cancelled', (done) => { const messages = []; event.broker.subscribeTmp('event', 'activity.*', (_, msg) => { messages.push(msg); @@ -1124,7 +1124,7 @@ describe('TimerEventDefinition', () => { ActivityApi(event.broker, messages[0]).cancel(); }); - it('completes when parent is canceled on activity timer event', (done) => { + it('completes when parent is cancelled on activity timer event', (done) => { event.broker.subscribeOnce('execution', 'execute.completed', () => { done(); }); @@ -1146,7 +1146,7 @@ describe('TimerEventDefinition', () => { }); }); - it('completes if canceled on activity timer event', (done) => { + it('completes if cancelled on activity timer event', (done) => { event.broker.subscribeTmp('event', 'activity.timer', (_, msg) => { ActivityApi(event.broker, msg).cancel(); }, {noAck: true}); @@ -1168,7 +1168,7 @@ describe('TimerEventDefinition', () => { }); }); - it('completes when delegated a canceled with parent id', (done) => { + it('completes when delegated a cancelled with parent id', (done) => { const messages = []; event.broker.subscribeTmp('event', 'activity.timeout', (_, msg) => { messages.push(msg); @@ -1399,7 +1399,7 @@ describe('TimerEventDefinition', () => { ActivityApi(broker, timerMessage).discard(); }); - it('can be canceled', (done) => { + it('can be cancelled', (done) => { const broker = event.broker; let timerMessage; diff --git a/test/feature/EventBasedGateway-feature.js b/test/feature/EventBasedGateway-feature.js index 44062d8b..162f6b96 100644 --- a/test/feature/EventBasedGateway-feature.js +++ b/test/feature/EventBasedGateway-feature.js @@ -59,7 +59,7 @@ Feature('EventBasedGateway', () => { signalApi = await wait; }); - When('timer is canceled', () => { + When('timer is cancelled', () => { bp.cancelActivity({id: timerApi.id}); }); diff --git a/test/feature/call-activity-feature.js b/test/feature/call-activity-feature.js index a7752399..bd02dd61 100644 --- a/test/feature/call-activity-feature.js +++ b/test/feature/call-activity-feature.js @@ -1,8 +1,12 @@ +import * as ck from 'chronokinesis'; + import factory from '../helpers/factory.js'; import testHelpers from '../helpers/testHelpers.js'; import Definition from '../../src/definition/Definition.js'; Feature('Call activity', () => { + after(ck.reset); + Scenario('call process in the same diagram', () => { let definition; Given('a process with a call activity referencing a process', async () => { @@ -107,7 +111,7 @@ Feature('Call activity', () => { }); }); - Scenario('call activity is canceled', () => { + Scenario('call activity is cancelled by timer', () => { let definition; Given('a process with a call activity referencing a process', async () => { const source = ` @@ -775,6 +779,67 @@ Feature('Call activity', () => { expect(err.content.error).to.match(/not found/i); }); }); + + Scenario('recovered call activity is cancelled by timer', () => { + let context, definition; + Given('a process with a call activity referencing a process', async () => { + const source = ` + + + + + + + + P1D + + + + + + + + + `; + + context = await testHelpers.context(source); + definition = new Definition(context); + }); + + When('ran', () => { + definition.run(); + }); + + Then('call activity has started process', () => { + expect(definition.getRunningProcesses()).to.have.length(2); + }); + + And('a timer is running', () => { + expect(definition.environment.timers.executing).to.have.length(1); + }); + + let state; + Given('state is saved and run is stopped', () => { + state = definition.getState(); + definition.stop(); + }); + + let end; + When('run is recovered and resumed at timeout', () => { + ck.travel(Date.now() + 1000 * 60 * 60 * 24); + + definition = new Definition(context.clone()); + + end = definition.waitFor('end'); + + definition.recover(state); + definition.resume(); + }); + + Then('run completes', () => { + return end; + }); + }); }); function processOutput(elm) { diff --git a/test/feature/issues/issues-feature.js b/test/feature/issues/issues-feature.js index ae6c2f3b..83408a39 100644 --- a/test/feature/issues/issues-feature.js +++ b/test/feature/issues/issues-feature.js @@ -1073,7 +1073,7 @@ Feature('Issues', () => { }); let end; - When('interrupting timeout timer is canceled', () => { + When('interrupting timeout timer is cancelled', () => { postponed = definition.getPostponed(); expect(postponed).to.have.length(2); expect(postponed[0].id).to.equal('approvalTimeout'); diff --git a/test/feature/timers-feature.js b/test/feature/timers-feature.js index 6a7041d3..1630510b 100644 --- a/test/feature/timers-feature.js +++ b/test/feature/timers-feature.js @@ -84,7 +84,7 @@ Feature('Timers', () => { expect(execution.content).to.have.property('expireAt').to.deep.equal(new Date('1993-06-26')); }); - When('throw event is canceled', () => { + When('throw event is cancelled', () => { definition.cancelActivity({id: activity.id}); }); @@ -146,7 +146,7 @@ Feature('Timers', () => { expect(execution.content).to.have.property('timeCycle', 'R3/PT10H'); }); - Given('start event is canceled', () => { + Given('start event is cancelled', () => { definition.cancelActivity({id: 'start-cycle'}); }); @@ -270,7 +270,7 @@ Feature('Timers', () => { expect(execution.content).to.have.property('timeCycle', 'R3/PT10H'); }); - When('start event is canceled', () => { + When('start event is cancelled', () => { definition.cancelActivity({id: 'start-cycle'}); }); @@ -300,7 +300,7 @@ Feature('Timers', () => { expect(execution.content).to.have.property('expireAt').to.deep.equal(new Date('1993-06-26')); }); - When('throw event is canceled', () => { + When('throw event is cancelled', () => { definition.cancelActivity({id: activity.id}); }); @@ -361,7 +361,7 @@ Feature('Timers', () => { expect(execution.content).to.have.property('timeCycle', 'R3/PT10H'); }); - Given('start event is canceled', () => { + Given('start event is cancelled', () => { definition.cancelActivity({id: 'start-cycle'}); }); @@ -903,4 +903,102 @@ Feature('Timers', () => { return end; }); }); + + Scenario('timer bound to user task is resumed at timeout', () => { + before(ck.reset); + + let context, definition; + Given('a source with user task and a bound timer', async () => { + const source = ` + + + + + + PT8H + + + + `; + + context = await testHelpers.context(source); + definition = new Definition(context); + }); + + When('definition is ran with state save on wait', () => { + definition.run(); + }); + + Then('user task and timer is running', () => { + expect(definition.getPostponed()).to.have.length(2); + }); + + let state; + Given('state is saved', () => { + state = definition.getState(); + definition.stop(); + }); + + let end; + When('run is recovered, resumed at timeout', () => { + ck.travel(Date.now() + 1000 * 60 * 60 * 8); + definition = new Definition(context.clone()).recover(state); + end = definition.waitFor('leave'); + definition.resume(); + }); + + Then('run completes', () => { + return end; + }); + }); + + Scenario('timer bound to sub process is resumed at timeout', () => { + before(ck.reset); + + let context, definition; + Given('a source with sub process user task and a bound timer', async () => { + const source = ` + + + + + + + + PT8H + + + + `; + + context = await testHelpers.context(source); + definition = new Definition(context); + }); + + When('definition is ran with state save on wait', () => { + definition.run(); + }); + + Then('sub process and timer is running', () => { + expect(definition.getPostponed()).to.have.length(2); + }); + + let state; + Given('state is saved', () => { + state = definition.getState(); + definition.stop(); + }); + + let end; + When('run is recovered, resumed at timeout', () => { + ck.travel(Date.now() + 1000 * 60 * 60 * 8); + definition = new Definition(context.clone()).recover(state); + end = definition.waitFor('leave'); + definition.resume(); + }); + + Then('run completes', () => { + return end; + }); + }); });