diff --git a/docs/images/error_boundary_event.png b/docs/images/error_boundary_event.png
new file mode 100644
index 00000000..e4ddd0a2
Binary files /dev/null and b/docs/images/error_boundary_event.png differ
diff --git a/docs/images/event_sub_process.png b/docs/images/event_sub_process.png
new file mode 100644
index 00000000..fd39b59b
Binary files /dev/null and b/docs/images/event_sub_process.png differ
diff --git a/docs/supported-elements.md b/docs/supported-elements.md
index 1dc70a1e..9bca69b9 100644
--- a/docs/supported-elements.md
+++ b/docs/supported-elements.md
@@ -28,6 +28,12 @@ There are some comments as well, which describe the level of support per each el
* get & set variables from/to context (of the instance) is possible
* variable mapping is supported (for input and output, see [Variables](#variables))
+## Error Boundary Event
+{: .width-60pt }
+
+* business errors raised by a task which requires modeling.
+* multiple end events are supported as well.
+
## Sub Process
{: .width-60pt }
@@ -37,6 +43,13 @@ There are some comments as well, which describe the level of support per each el
* sub-processes can have their own start and end events.
* supports variable mapping for input and output, similar to tasks.
* can be used to handle repetitive or complex logic within a process.
+*
+## Event Sub Process
+{: .width-60pt }
+
+* Only Error Event Sub Process is supported
+* event subprocess is a subprocess triggered by an event
+* it must have an event based start event
## Gateways
diff --git a/pkg/bpmn_engine/command.go b/pkg/bpmn_engine/command.go
index 1ea76376..8ac06e63 100644
--- a/pkg/bpmn_engine/command.go
+++ b/pkg/bpmn_engine/command.go
@@ -5,10 +5,12 @@ import "github.com/nitram509/lib-bpmn-engine/pkg/spec/BPMN20"
type commandType string
const (
- flowTransitionType commandType = "flowTransition"
- activityType commandType = "activity"
- continueActivityType commandType = "continueActivity"
+ flowTransitionType commandType = "flowTransition"
+ activityType commandType = "activity"
+ continueActivityType commandType = "continueActivity"
+ // A command that there is a technical error and the engine should fail the process instance
errorType commandType = "error"
+ eventSubProcessCompletedType commandType = "eventSubProcessCompletedType"
checkExclusiveGatewayDoneType commandType = "checkExclusiveGatewayDone"
)
@@ -72,3 +74,12 @@ type checkExclusiveGatewayDoneCommand struct {
func (t checkExclusiveGatewayDoneCommand) Type() commandType {
return checkExclusiveGatewayDoneType
}
+
+type eventSubProcessCompletedCommand struct {
+ // activity reference to the event sub-process, which has completed
+ activity activity
+}
+
+func (t eventSubProcessCompletedCommand) Type() commandType {
+ return eventSubProcessCompletedType
+}
diff --git a/pkg/bpmn_engine/engine.go b/pkg/bpmn_engine/engine.go
index b2ab1d0d..21637db2 100644
--- a/pkg/bpmn_engine/engine.go
+++ b/pkg/bpmn_engine/engine.go
@@ -203,11 +203,16 @@ func (state *BpmnEngineState) run(process BPMN20.ProcessElement, instance *proce
instance.ActivityState = Failed
// *activityState = Failed // TODO: check if meaningful
break
+ case eventSubProcessCompletedType:
+ subProcessActivity := cmd.(eventSubProcessCompletedCommand).activity
+ instance.SetState(subProcessActivity.State())
+ state.exportElementEvent(process, *instance, process, exporter.ElementCompleted)
+ break
case checkExclusiveGatewayDoneType:
activity := cmd.(checkExclusiveGatewayDoneCommand).gatewayActivity
state.checkExclusiveGatewayDone(activity)
default:
- panic("[invariant check] command type check not fully implemented")
+ return newEngineErrorf("[invariant check] command type check not fully implemented")
}
}
@@ -239,12 +244,41 @@ func (state *BpmnEngineState) handleElement(process BPMN20.ProcessElement, act a
state.exportElementEvent(process, *instance, *element, exporter.ElementCompleted) // special case here, to end the instance
case BPMN20.ServiceTask:
taskElement := (*element).(BPMN20.TaskElement)
- _, activity = state.handleServiceTask(process, instance, &taskElement)
- createFlowTransitions = activity.State() == Completed
+ _, job, jobErr := state.handleServiceTask(process, instance, &taskElement)
+ err = jobErr
+ activity = job
+ if err != nil {
+ nextCommands = append(nextCommands, errorCommand{
+ err: err,
+ elementId: (*element).GetId(),
+ elementName: (*element).GetName(),
+ })
+ } else if job.ErrorCode != "" {
+ // The current process will remain ACTIVE until the event sub-processes have completed.
+ nextCommands = handleErrorEvent(process, instance, element, job.ErrorCode)
+ createFlowTransitions = false // TODO confirm
+ } else {
+ // Only follow sequence flow if there are no Technical or Business Errors
+ createFlowTransitions = activity.State() == Completed
+ }
case BPMN20.UserTask:
taskElement := (*element).(BPMN20.TaskElement)
- activity = state.handleUserTask(process, instance, &taskElement)
- createFlowTransitions = activity.State() == Completed
+ job, jobErr := state.handleUserTask(process, instance, &taskElement)
+ err = jobErr
+ activity = job
+ if err != nil {
+ nextCommands = append(nextCommands, errorCommand{
+ err: err,
+ elementId: (*element).GetId(),
+ elementName: (*element).GetName(),
+ })
+ } else if job.ErrorCode != "" {
+ nextCommands = handleErrorEvent(process, instance, element, job.ErrorCode)
+ createFlowTransitions = false
+ } else {
+ // Only follow sequence flow if there are no Technical or Business Errors
+ createFlowTransitions = activity.State() == Completed
+ }
case BPMN20.IntermediateCatchEvent:
ice := (*element).(BPMN20.TIntermediateCatchEvent)
createFlowTransitions, activity, err = state.handleIntermediateCatchEvent(process, instance, ice, originActivity)
@@ -292,17 +326,31 @@ func (state *BpmnEngineState) handleElement(process BPMN20.ProcessElement, act a
createFlowTransitions = true
case BPMN20.SubProcess:
subProcessElement := (*element).(BPMN20.TSubProcess)
- activity, err = state.handleSubProcess(instance, &subProcessElement)
+ subProcess, subProcessErr := state.handleSubProcess(instance, &subProcessElement)
+ activity = subProcess
+ err = subProcessErr
if err != nil {
nextCommands = append(nextCommands, errorCommand{
err: err,
elementId: (*element).GetId(),
elementName: (*element).GetName(),
})
+ } else if subProcessElement.TriggeredByEvent {
+ // We need to complete the parent process when an event sub-process has completed. but we cant do it here
+ nextCommands = append(nextCommands, eventSubProcessCompletedCommand{
+ activity: subProcess,
+ })
}
createFlowTransitions = activity.State() == Completed
+ case BPMN20.BoundaryEvent:
+ boundary := (*element).(BPMN20.TBoundaryEvent)
+ activity, err = state.handleBoundaryEvent(&boundary, instance)
default:
- panic(fmt.Sprintf("[invariant check] unsupported element: id=%s, type=%s", (*element).GetId(), (*element).GetType()))
+ nextCommands = append(nextCommands, errorCommand{
+ err: newEngineErrorf("[invariant check] unsupported element: id=%s, type=%s", (*element).GetId(), (*element).GetType()),
+ elementId: (*element).GetId(),
+ elementName: (*element).GetName(),
+ })
}
if createFlowTransitions && err == nil {
nextCommands = append(nextCommands, createNextCommands(process, instance, element, activity)...)
@@ -310,6 +358,115 @@ func (state *BpmnEngineState) handleElement(process BPMN20.ProcessElement, act a
return nextCommands
}
+func handleErrorEvent(process BPMN20.ProcessElement, instance *processInstanceInfo, element *BPMN20.BaseElement, errorCode string) []command {
+ // Find the error by code on the process
+ if errT, found := findErrorDefinition(instance.ProcessInfo.definitions, errorCode); found {
+
+ // Find the boundary events for the task
+ boundaryEvents := findBoundaryEventsForTypeAndReference(instance.ProcessInfo.definitions, BPMN20.ErrorBoundary, (*element).GetId())
+ if boundaryEvent, foundBoundary := findBoundaryEventForError(boundaryEvents, errT.Id); foundBoundary {
+ return []command{
+ activityCommand{element: BPMN20.Ptr[BPMN20.BaseElement](boundaryEvent)},
+ }
+ }
+
+ // If we still haven't found a command then we should look to see if there is an event sub process we can follow
+ if subProcess, subFound := findEventSubprocessForError(process, errT.Id); subFound {
+ return []command{
+ activityCommand{element: BPMN20.Ptr[BPMN20.BaseElement](subProcess)},
+ }
+ }
+
+ // If not see if there is a catch-all boundary event
+ if boundaryEvent, foundBoundary := findBoundaryEventForError(boundaryEvents, ""); foundBoundary {
+ return []command{
+ activityCommand{element: BPMN20.Ptr[BPMN20.BaseElement](boundaryEvent)},
+ }
+ }
+
+ // If not find an event sub process matching catchall
+ if subProcess, subFound := findEventSubprocessForError(process, ""); subFound {
+ return []command{
+ activityCommand{element: BPMN20.Ptr[BPMN20.BaseElement](subProcess)},
+ }
+ }
+
+ // TODO continue lookup up to the parent process if this is a sub process - not supported yet
+
+ return []command{
+ errorCommand{
+ err: newEngineErrorf("Could not find suitable handler for ErrorCode event id=%s, code=%s", errT.Id, errT.ErrorCode),
+ elementId: (*element).GetId(),
+ elementName: (*element).GetName(),
+ },
+ }
+ } else {
+ return []command{
+ errorCommand{
+ err: newEngineErrorf("Could not find error definition \"%s\"", errorCode),
+ elementId: (*element).GetId(),
+ elementName: (*element).GetName(),
+ },
+ }
+ }
+}
+
+func findEventSubprocessForError(process BPMN20.ProcessElement, errorReferenceID string) (BPMN20.TSubProcess, bool) {
+ // Look for event sub-processes in the process
+ for _, subProcess := range process.GetSubProcess() {
+ // Check if this is an event sub-process (triggered by event)
+ if subProcess.TriggeredByEvent {
+ // Look for start events in the sub-process
+ for _, startEvent := range subProcess.StartEvents {
+ // Check if this start event has an error event definition
+ if startEvent.ErrorEventDefinition.ErrorRef == errorReferenceID {
+ // We found an event sub-process with an error start event
+ return subProcess, true
+ }
+ }
+ }
+ }
+
+ // No matching event sub-process found
+ return BPMN20.TSubProcess{}, false
+}
+
+// findBoundaryEventsForReference finds all boundary events attached to the provided element
+func findBoundaryEventsForTypeAndReference(definitions BPMN20.TDefinitions, boundaryType BPMN20.BoundaryType, referenceID string) []BPMN20.TBoundaryEvent {
+ boundaryEvents := make([]BPMN20.TBoundaryEvent, 0)
+ for _, boundary := range definitions.Process.BoundaryEvent {
+ if boundary.AttachedToRef == referenceID && boundary.GetBoundaryType() == boundaryType {
+ boundaryEvents = append(boundaryEvents, boundary)
+ }
+ }
+ return boundaryEvents
+}
+
+func findBoundaryEventForError(boundaryEvents []BPMN20.TBoundaryEvent, errorID string) (BPMN20.TBoundaryEvent, bool) {
+ for _, boundaryEvent := range boundaryEvents {
+ // Check if this boundary event has an error event definition
+ if boundaryEvent.ErrorEventDefinition.ErrorRef == errorID {
+ return boundaryEvent, true
+ }
+ }
+ return BPMN20.TBoundaryEvent{}, false
+}
+
+func findErrorDefinition(definitions BPMN20.TDefinitions, errorCode string) (BPMN20.TError, bool) {
+
+ // Iterate through all errors in the definitions
+ for _, err := range definitions.Errors {
+ // Check if the error code matches the requested code
+ if err.ErrorCode == errorCode {
+ return err, true
+ }
+ }
+
+ // Return empty error if not found
+ return BPMN20.TError{}, false
+
+}
+
func createCheckExclusiveGatewayDoneCommand(originActivity activity) (cmds []command) {
if (*originActivity.Element()).GetType() == BPMN20.EventBasedGateway {
evtBasedGatewayActivity := originActivity.(*eventBasedGatewayActivity)
@@ -473,3 +630,19 @@ func (state *BpmnEngineState) findCreatedTimers(instance *processInstanceInfo) (
}
return result
}
+
+func (state *BpmnEngineState) handleBoundaryEvent(element *BPMN20.TBoundaryEvent, instance *processInstanceInfo) (activity, error) {
+ var be BPMN20.BaseElement = element
+ activity := &elementActivity{
+ key: state.generateKey(),
+ state: Completed,
+ element: &be,
+ }
+ variableHolder := NewVarHolder(&instance.VariableHolder, nil)
+ err := propagateProcessInstanceVariables(&variableHolder, element.GetOutputMapping())
+ if err != nil {
+ instance.ActivityState = Failed
+ }
+
+ return activity, err
+}
diff --git a/pkg/bpmn_engine/engine_test.go b/pkg/bpmn_engine/engine_test.go
index 00f3a669..ce421737 100644
--- a/pkg/bpmn_engine/engine_test.go
+++ b/pkg/bpmn_engine/engine_test.go
@@ -1,6 +1,8 @@
package bpmn_engine
import (
+ "fmt"
+ "github.com/nitram509/lib-bpmn-engine/pkg/bpmn_engine/exporter"
"testing"
"time"
@@ -22,6 +24,41 @@ func (callPath *CallPath) TaskHandler(job ActivatedJob) {
job.Complete()
}
+func pathString(paths []string) string {
+ path := ""
+ for _, p := range paths {
+ path += p + "\n"
+ }
+ return path
+}
+
+// PathRecordingEventExporter records the paths taken during the execution of a process
+// into a string.
+type PathRecordingEventExporter struct {
+ paths []string
+}
+
+func (e *PathRecordingEventExporter) String() string {
+ return pathString(e.paths)
+}
+
+// NewEventLogExporter creates a new instance of a PathRecordingEventExporter
+func NewPathRecordingEventExporter() *PathRecordingEventExporter {
+ return &PathRecordingEventExporter{
+ paths: make([]string, 0),
+ }
+}
+
+func (*PathRecordingEventExporter) NewProcessEvent(_ *exporter.ProcessEvent) {}
+
+func (*PathRecordingEventExporter) EndProcessEvent(_ *exporter.ProcessInstanceEvent) {}
+
+func (*PathRecordingEventExporter) NewProcessInstanceEvent(_ *exporter.ProcessInstanceEvent) {}
+
+func (e *PathRecordingEventExporter) NewElementEvent(_ *exporter.ProcessInstanceEvent, elementInfo *exporter.ElementInfo) {
+ e.paths = append(e.paths, fmt.Sprintf("%s(%s)", elementInfo.ElementId, elementInfo.Intent))
+}
+
func Test_BpmnEngine_interfaces_implemented(t *testing.T) {
var _ BpmnEngine = &BpmnEngineState{}
}
@@ -215,6 +252,8 @@ func Test_CreateInstanceById_uses_latest_process_version(t *testing.T) {
func Test_CreateAndRunInstanceById_uses_latest_process_version(t *testing.T) {
// setup
engine := New()
+ engine.NewTaskHandler().Id("id").Handler(jobCompleteHandler)
+ engine.NewTaskHandler().Id("test-2").Handler(jobCompleteHandler)
// when
v1, err := engine.LoadFromFile("../../test-cases/simple_task.bpmn")
diff --git a/pkg/bpmn_engine/exporter/logging.go b/pkg/bpmn_engine/exporter/logging.go
new file mode 100644
index 00000000..716ca551
--- /dev/null
+++ b/pkg/bpmn_engine/exporter/logging.go
@@ -0,0 +1,30 @@
+package exporter
+
+import "fmt"
+
+// LoggingEventExported writes all events to a log file
+type LoggingEventExported struct {
+}
+
+// NewEventLogExporter creates a new instance of a LoggingEventExported
+func NewEventLogExporter() *LoggingEventExported {
+ return &LoggingEventExported{}
+}
+
+func (*LoggingEventExported) NewProcessEvent(event *ProcessEvent) {
+ fmt.Printf("New Process event version: %d, processKey: %d, processID: %s\n", event.Version, event.ProcessKey, event.ProcessId)
+}
+
+func (*LoggingEventExported) EndProcessEvent(event *ProcessInstanceEvent) {
+ fmt.Printf("End Process event version: %d, processKey: %d, processID: %s, processInstanceKey: %d\n", event.Version, event.ProcessKey, event.ProcessId, event.ProcessInstanceKey)
+}
+
+func (*LoggingEventExported) NewProcessInstanceEvent(event *ProcessInstanceEvent) {
+ fmt.Printf("New Process Instance version: %d, processKey: %d, processID: %s, processInstanceKey: %d\n", event.Version, event.ProcessKey, event.ProcessId, event.ProcessInstanceKey)
+}
+
+func (*LoggingEventExported) NewElementEvent(event *ProcessInstanceEvent, elementInfo *ElementInfo) {
+ fmt.Printf("New Element event version: %d, processKey: %d, processID: %s, processInstanceKey: %d, elementType: %s, elementId: %s, intent: %s\n",
+ event.Version, event.ProcessKey, event.ProcessId, event.ProcessInstanceKey,
+ elementInfo.BpmnElementType, elementInfo.ElementId, elementInfo.Intent)
+}
diff --git a/pkg/bpmn_engine/jobs.go b/pkg/bpmn_engine/jobs.go
index e03117be..153df515 100644
--- a/pkg/bpmn_engine/jobs.go
+++ b/pkg/bpmn_engine/jobs.go
@@ -14,6 +14,10 @@ type job struct {
JobState ActivityState `json:"s"`
CreatedAt time.Time `json:"c"`
baseElement *BPMN20.BaseElement
+ // Failure returned by a handler with job.Fail(string)
+ Failure string `json:"f,omitempty"`
+ // ErrorCode event thrown by a handler with job.ThrowError(string)
+ ErrorCode string `json:"ec,omitempty"`
}
func (j job) Key() int64 {
diff --git a/pkg/bpmn_engine/jobs_activated.go b/pkg/bpmn_engine/jobs_activated.go
index a16b38a0..4399a240 100644
--- a/pkg/bpmn_engine/jobs_activated.go
+++ b/pkg/bpmn_engine/jobs_activated.go
@@ -9,6 +9,7 @@ type activatedJob struct {
processInstanceInfo *processInstanceInfo
completeHandler func()
failHandler func(reason string)
+ errorHandler func(errorCode string)
key int64
processInstanceKey int64
bpmnProcessId string
@@ -53,11 +54,15 @@ type ActivatedJob interface {
CreatedAt() time.Time
// Fail does set the State the worker missed completing the job
- // Fail and Complete mutual exclude each other
+ // ThrowError, Fail and Complete mutual exclude each other
Fail(reason string)
+ // ThrowError throws an error event
+ // ThrowError, Fail and Complete mutual exclude each other
+ ThrowError(errorCode string)
+
// Complete does set the State the worker successfully completing the job
- // Fail and Complete mutual exclude each other
+ // ThrowError, Fail and Complete mutual exclude each other
Complete()
}
@@ -116,6 +121,11 @@ func (aj *activatedJob) Fail(reason string) {
aj.failHandler(reason)
}
+// ThrowError implements ActivatedJob
+func (aj *activatedJob) ThrowError(errorCode string) {
+ aj.errorHandler(errorCode)
+}
+
// Complete implements ActivatedJob
func (aj *activatedJob) Complete() {
aj.completeHandler()
diff --git a/pkg/bpmn_engine/jobs_test.go b/pkg/bpmn_engine/jobs_test.go
index 12460b38..0c1e2d25 100644
--- a/pkg/bpmn_engine/jobs_test.go
+++ b/pkg/bpmn_engine/jobs_test.go
@@ -1,6 +1,8 @@
package bpmn_engine
import (
+ "github.com/corbym/gocrest"
+ "github.com/nitram509/lib-bpmn-engine/pkg/bpmn_engine/exporter"
"testing"
"github.com/corbym/gocrest/has"
@@ -33,7 +35,7 @@ func Test_job_implements_Activity(t *testing.T) {
var _ activity = &job{}
}
-func Test_a_job_can_fail_and_keeps_the_instance_in_active_state(t *testing.T) {
+func Test_a_job_can_fail_and_keeps_fails_the_instance(t *testing.T) {
// setup
bpmnEngine := New()
process, _ := bpmnEngine.LoadFromFile("../../test-cases/simple_task.bpmn")
@@ -41,7 +43,7 @@ func Test_a_job_can_fail_and_keeps_the_instance_in_active_state(t *testing.T) {
instance, _ := bpmnEngine.CreateAndRunInstance(process.ProcessKey, nil)
- then.AssertThat(t, instance.ActivityState, is.EqualTo(Active))
+ then.AssertThat(t, instance.ActivityState, is.EqualTo(Failed))
}
// Test_simple_count_loop requires correct Task-Output-Mapping in the BPMN file
@@ -155,7 +157,7 @@ func Test_instance_fails_on_Invalid_Input_mapping(t *testing.T) {
// when
pi, err := bpmnEngine.CreateAndRunInstance(process.ProcessKey, nil)
- then.AssertThat(t, err, is.Nil())
+ then.AssertThat(t, err.Error(), is.Not(is.Nil()))
// then
then.AssertThat(t, cp.CallPath, is.EqualTo(""))
@@ -292,8 +294,9 @@ func Test_task_no_output_variables_mapping_on_failure(t *testing.T) {
job.Fail("because I can")
})
- instance, _ := bpmnEngine.CreateAndRunInstance(process.ProcessKey, nil)
- then.AssertThat(t, instance.ActivityState, is.EqualTo(Active))
+ instance, err := bpmnEngine.CreateAndRunInstance(process.ProcessKey, nil)
+ then.AssertThat(t, instance.ActivityState, is.EqualTo(Failed))
+ then.AssertThat(t, err.Error(), is.EqualTo("because I can"))
then.AssertThat(t, instance.GetVariable("aVariable"), is.Nil())
}
@@ -380,3 +383,734 @@ func Test_multiple_instances_with_same_user_task_ids(t *testing.T) {
then.AssertThat(t, called[instance1.GetInstanceKey()], has.Length(4))
then.AssertThat(t, called[instance2.GetInstanceKey()], has.Length(4))
}
+
+func Test_error_boundary_event(t *testing.T) {
+
+ type handler struct {
+ fn func(job ActivatedJob)
+ id string
+ }
+
+ type varAssertion struct {
+ assertion *gocrest.Matcher
+ key string
+ }
+
+ type args struct {
+ file string
+ handlers []handler
+ }
+
+ type wants struct {
+ instanceState ActivityState
+ processError *gocrest.Matcher
+ varAssertions []varAssertion
+ paths []string
+ }
+
+ tests := []struct {
+ name string
+ args args
+ wants wants
+ }{
+ {
+ name: "Single boundary error event",
+ args: args{
+ file: "../../test-cases/error-boundary-event.bpmn",
+ handlers: []handler{
+ {
+ id: "error-task",
+ fn: func(job ActivatedJob) {
+ job.SetVariable("aVariable", true)
+ job.ThrowError("error1")
+ },
+ },
+ {
+ id: "handle-error-task",
+ fn: func(job ActivatedJob) {
+ job.Complete()
+ },
+ },
+ },
+ },
+ wants: wants{
+ instanceState: Completed,
+ processError: is.Nil(),
+ varAssertions: []varAssertion{
+ {
+ key: "aVariable",
+ assertion: is.True(),
+ },
+ },
+ paths: []string{
+ "start(ELEMENT_ACTIVATED)",
+ "start(ELEMENT_COMPLETED)",
+ "flow1(SEQUENCE_FLOW_TAKEN)",
+ "error-task(ELEMENT_ACTIVATED)",
+ "error-task(ELEMENT_COMPLETED)",
+ "error-boundary-event(ELEMENT_ACTIVATED)",
+ "error-boundary-event(ELEMENT_COMPLETED)",
+ "flow-handle-error(SEQUENCE_FLOW_TAKEN)",
+ "handle-error-task(ELEMENT_ACTIVATED)",
+ "handle-error-task(ELEMENT_COMPLETED)",
+ "flow-handled-error(SEQUENCE_FLOW_TAKEN)",
+ "handled-error-end(ELEMENT_ACTIVATED)",
+ "handled-error-end(ELEMENT_COMPLETED)",
+ "handled-error-end(ELEMENT_COMPLETED)",
+ },
+ },
+ },
+ {
+ name: "Catchall boundary error event with unknown error",
+ args: args{
+ file: "../../test-cases/error-boundary-event-catchall.bpmn",
+ handlers: []handler{
+ {
+ id: "error-task",
+ fn: func(job ActivatedJob) {
+ job.SetVariable("aVariable", true)
+ job.ThrowError("unknown_error")
+ },
+ },
+ {
+ id: "handle-error-task",
+ fn: func(job ActivatedJob) {
+ job.Complete()
+ },
+ },
+ },
+ },
+ wants: wants{
+ instanceState: Failed,
+ processError: is.EqualTo(newEngineErrorf("Could not find error definition \"unknown_error\"")),
+ varAssertions: []varAssertion{
+ {
+ key: "aVariable",
+ assertion: is.True(),
+ },
+ },
+ paths: []string{
+ "start(ELEMENT_ACTIVATED)",
+ "start(ELEMENT_COMPLETED)",
+ "flow1(SEQUENCE_FLOW_TAKEN)",
+ "error-task(ELEMENT_ACTIVATED)",
+ "error-task(ELEMENT_COMPLETED)",
+ },
+ },
+ },
+ {
+ name: "Catchall boundary error event",
+ args: args{
+ file: "../../test-cases/error-boundary-event-catchall.bpmn",
+ handlers: []handler{
+ {
+ id: "error-task",
+ fn: func(job ActivatedJob) {
+ job.SetVariable("aVariable", true)
+ job.ThrowError("error1")
+ },
+ },
+ {
+ id: "handle-error-task",
+ fn: func(job ActivatedJob) {
+ job.Complete()
+ },
+ },
+ },
+ },
+ wants: wants{
+ instanceState: Completed,
+ processError: is.Nil(),
+ varAssertions: []varAssertion{
+ {
+ key: "aVariable",
+ assertion: is.True(),
+ },
+ },
+ paths: []string{
+ "start(ELEMENT_ACTIVATED)",
+ "start(ELEMENT_COMPLETED)",
+ "flow1(SEQUENCE_FLOW_TAKEN)",
+ "error-task(ELEMENT_ACTIVATED)",
+ "error-task(ELEMENT_COMPLETED)",
+ "error-boundary-event(ELEMENT_ACTIVATED)",
+ "error-boundary-event(ELEMENT_COMPLETED)",
+ "flow-handle-error(SEQUENCE_FLOW_TAKEN)",
+ "handle-error-task(ELEMENT_ACTIVATED)",
+ "handle-error-task(ELEMENT_COMPLETED)",
+ "flow-handled-error(SEQUENCE_FLOW_TAKEN)",
+ "handled-error-end(ELEMENT_ACTIVATED)",
+ "handled-error-end(ELEMENT_COMPLETED)",
+ "handled-error-end(ELEMENT_COMPLETED)",
+ },
+ },
+ },
+ {
+ name: "Single boundary error event with output",
+ args: args{
+ file: "../../test-cases/error-boundary-event-outputs.bpmn",
+ handlers: []handler{
+ {
+ id: "error-task",
+ fn: func(job ActivatedJob) {
+ job.SetVariable("aVariable", true)
+ job.SetVariable("errorValue", "something broke")
+ job.ThrowError("error1")
+ },
+ },
+ {
+ id: "handle-error-task",
+ fn: func(job ActivatedJob) {
+ job.Complete()
+ },
+ },
+ },
+ },
+ wants: wants{
+ instanceState: Completed,
+ processError: is.Nil(),
+ varAssertions: []varAssertion{
+ {
+ key: "aVariable",
+ assertion: is.True(),
+ },
+ {
+ key: "errorValue",
+ assertion: is.EqualTo("something broke"),
+ },
+ {
+ key: "mappedErrorValue",
+ assertion: is.EqualTo("something broke"),
+ },
+ },
+ paths: []string{
+ "start(ELEMENT_ACTIVATED)",
+ "start(ELEMENT_COMPLETED)",
+ "flow1(SEQUENCE_FLOW_TAKEN)",
+ "error-task(ELEMENT_ACTIVATED)",
+ "error-task(ELEMENT_COMPLETED)",
+ "error-boundary-event(ELEMENT_ACTIVATED)",
+ "error-boundary-event(ELEMENT_COMPLETED)",
+ "flow-handle-error(SEQUENCE_FLOW_TAKEN)",
+ "handle-error-task(ELEMENT_ACTIVATED)",
+ "handle-error-task(ELEMENT_COMPLETED)",
+ "flow-handled-error(SEQUENCE_FLOW_TAKEN)",
+ "handled-error-end(ELEMENT_ACTIVATED)",
+ "handled-error-end(ELEMENT_COMPLETED)",
+ "handled-error-end(ELEMENT_COMPLETED)",
+ },
+ },
+ },
+ {
+ name: "Single boundary error event unknown error",
+ args: args{
+ file: "../../test-cases/error-boundary-event.bpmn",
+ handlers: []handler{
+ {
+ id: "error-task",
+ fn: func(job ActivatedJob) {
+ job.SetVariable("aVariable", true)
+ job.ThrowError("unknown_error")
+ },
+ },
+ },
+ },
+ wants: wants{
+ instanceState: Failed,
+ processError: is.EqualTo(newEngineErrorf("Could not find error definition \"unknown_error\"")),
+ varAssertions: []varAssertion{
+ {
+ key: "aVariable",
+ assertion: is.True(),
+ },
+ },
+ paths: []string{
+ "start(ELEMENT_ACTIVATED)",
+ "start(ELEMENT_COMPLETED)",
+ "flow1(SEQUENCE_FLOW_TAKEN)",
+ "error-task(ELEMENT_ACTIVATED)",
+ "error-task(ELEMENT_COMPLETED)",
+ },
+ },
+ },
+ {
+ name: "Multi boundary error event unknown error",
+ args: args{
+ file: "../../test-cases/error-boundary-event-multiple.bpmn",
+ handlers: []handler{
+ {
+ id: "error-task",
+ fn: func(job ActivatedJob) {
+ job.SetVariable("aVariable", true)
+ job.ThrowError("unknown_error")
+ },
+ },
+ },
+ },
+ wants: wants{
+ instanceState: Failed,
+ processError: is.EqualTo(newEngineErrorf("Could not find error definition \"unknown_error\"")),
+ varAssertions: []varAssertion{
+ {
+ key: "aVariable",
+ assertion: is.True(),
+ },
+ },
+ paths: []string{
+ "start(ELEMENT_ACTIVATED)",
+ "start(ELEMENT_COMPLETED)",
+ "flow1(SEQUENCE_FLOW_TAKEN)",
+ "error-task(ELEMENT_ACTIVATED)",
+ "error-task(ELEMENT_COMPLETED)",
+ },
+ },
+ },
+ {
+ name: "Multi boundary error event",
+ args: args{
+ file: "../../test-cases/error-boundary-event-multiple.bpmn",
+ handlers: []handler{
+ {
+ id: "error-task",
+ fn: func(job ActivatedJob) {
+ job.SetVariable("aVariable", true)
+ job.ThrowError("error1")
+ },
+ },
+ {
+ id: "handle-error-task",
+ fn: func(job ActivatedJob) {
+ job.Complete()
+ },
+ },
+ },
+ },
+ wants: wants{
+ instanceState: Completed,
+ processError: is.Nil(),
+ varAssertions: []varAssertion{
+ {
+ key: "aVariable",
+ assertion: is.True(),
+ },
+ },
+ paths: []string{
+ "start(ELEMENT_ACTIVATED)",
+ "start(ELEMENT_COMPLETED)",
+ "flow1(SEQUENCE_FLOW_TAKEN)",
+ "error-task(ELEMENT_ACTIVATED)",
+ "error-task(ELEMENT_COMPLETED)",
+ "error-boundary-event(ELEMENT_ACTIVATED)",
+ "error-boundary-event(ELEMENT_COMPLETED)",
+ "flow-handle-error(SEQUENCE_FLOW_TAKEN)",
+ "handle-error-task(ELEMENT_ACTIVATED)",
+ "handle-error-task(ELEMENT_COMPLETED)",
+ "flow-handled-error(SEQUENCE_FLOW_TAKEN)",
+ "handled-error-end(ELEMENT_ACTIVATED)",
+ "handled-error-end(ELEMENT_COMPLETED)",
+ "handled-error-end(ELEMENT_COMPLETED)",
+ },
+ },
+ },
+ {
+ name: "Multi boundary error event catchall",
+ args: args{
+ file: "../../test-cases/error-boundary-event-multiple.bpmn",
+ handlers: []handler{
+ {
+ id: "error-task",
+ fn: func(job ActivatedJob) {
+ job.SetVariable("aVariable", true)
+ job.ThrowError("error2")
+ },
+ },
+ {
+ id: "handle-all-task",
+ fn: func(job ActivatedJob) {
+ job.Complete()
+ },
+ },
+ },
+ },
+ wants: wants{
+ instanceState: Completed,
+ processError: is.Nil(),
+ varAssertions: []varAssertion{
+ {
+ key: "aVariable",
+ assertion: is.True(),
+ },
+ },
+ paths: []string{
+ "start(ELEMENT_ACTIVATED)",
+ "start(ELEMENT_COMPLETED)",
+ "flow1(SEQUENCE_FLOW_TAKEN)",
+ "error-task(ELEMENT_ACTIVATED)",
+ "error-task(ELEMENT_COMPLETED)",
+ "all-boundary-event(ELEMENT_ACTIVATED)",
+ "all-boundary-event(ELEMENT_COMPLETED)",
+ "flow-handle-all(SEQUENCE_FLOW_TAKEN)",
+ "handle-all-task(ELEMENT_ACTIVATED)",
+ "handle-all-task(ELEMENT_COMPLETED)",
+ "flow-handled-all(SEQUENCE_FLOW_TAKEN)",
+ "handled-all-end(ELEMENT_ACTIVATED)",
+ "handled-all-end(ELEMENT_COMPLETED)",
+ "handled-all-end(ELEMENT_COMPLETED)",
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // setup
+ bpmnEngine := New()
+ pathRecordingExporter := NewPathRecordingEventExporter()
+ bpmnEngine.AddEventExporter(pathRecordingExporter)
+ bpmnEngine.AddEventExporter(exporter.NewEventLogExporter())
+ process, _ := bpmnEngine.LoadFromFile(tt.args.file)
+ for _, handler := range tt.args.handlers {
+ bpmnEngine.NewTaskHandler().Id(handler.id).Handler(handler.fn)
+ }
+ instance, err := bpmnEngine.CreateAndRunInstance(process.ProcessKey, nil)
+ then.AssertThat(t, instance.ActivityState, is.EqualTo(tt.wants.instanceState))
+ then.AssertThat(t, err, tt.wants.processError)
+ then.AssertThat(t, pathRecordingExporter.String(), is.EqualTo(pathString(tt.wants.paths)))
+
+ for _, varAssert := range tt.wants.varAssertions {
+ then.AssertThat(t, instance.GetVariable(varAssert.key), varAssert.assertion)
+ }
+ })
+ }
+}
+
+func Test_error_event_subprocess(t *testing.T) {
+
+ type handler struct {
+ fn func(job ActivatedJob)
+ id string
+ }
+
+ type varAssertion struct {
+ assertion *gocrest.Matcher
+ key string
+ }
+
+ type args struct {
+ file string
+ handlers []handler
+ }
+
+ type wants struct {
+ instanceState ActivityState
+ processError *gocrest.Matcher
+ varAssertions []varAssertion
+ paths []string
+ }
+
+ tests := []struct {
+ name string
+ args args
+ wants wants
+ }{
+ {
+ name: "Single boundary error event",
+ args: args{
+ file: "../../test-cases/error-event-subprocess.bpmn",
+ handlers: []handler{
+ {
+ id: "error-task",
+ fn: func(job ActivatedJob) {
+ job.SetVariable("aVariable", true)
+ job.ThrowError("error1")
+ },
+ },
+ {
+ id: "handle-error-task",
+ fn: jobCompleteHandler,
+ },
+ },
+ },
+ wants: wants{
+ instanceState: Completed,
+ processError: is.Nil(),
+ varAssertions: []varAssertion{
+ {
+ key: "aVariable",
+ assertion: is.True(),
+ },
+ },
+ paths: []string{
+ "start(ELEMENT_ACTIVATED)",
+ "start(ELEMENT_COMPLETED)",
+ "flow1(SEQUENCE_FLOW_TAKEN)",
+ "error-task(ELEMENT_ACTIVATED)",
+ "error-task(ELEMENT_COMPLETED)",
+ "error-sub-process(ELEMENT_ACTIVATED)",
+ "error1-start-event(ELEMENT_ACTIVATED)",
+ "error1-start-event(ELEMENT_COMPLETED)",
+ "flow-error1(SEQUENCE_FLOW_TAKEN)",
+ "handle-error-task(ELEMENT_ACTIVATED)",
+ "handle-error-task(ELEMENT_COMPLETED)",
+ "flow-error1-end(SEQUENCE_FLOW_TAKEN)",
+ "end-error1(ELEMENT_ACTIVATED)",
+ "end-error1(ELEMENT_COMPLETED)",
+ "end-error1(ELEMENT_COMPLETED)",
+ "error-sub-process(ELEMENT_COMPLETED)",
+ "error-event-subprocess(ELEMENT_COMPLETED)",
+ },
+ },
+ },
+ {
+ name: "Single boundary error event catchall",
+ args: args{
+ file: "../../test-cases/error-event-subprocess.bpmn",
+ handlers: []handler{
+ {
+ id: "error-task",
+ fn: func(job ActivatedJob) {
+ job.SetVariable("aVariable", true)
+ job.ThrowError("error2")
+ },
+ },
+ {
+ id: "handle-catchall-task",
+ fn: jobCompleteHandler,
+ },
+ },
+ },
+ wants: wants{
+ instanceState: Completed,
+ processError: is.Nil(),
+ varAssertions: []varAssertion{
+ {
+ key: "aVariable",
+ assertion: is.True(),
+ },
+ },
+ paths: []string{
+ "start(ELEMENT_ACTIVATED)",
+ "start(ELEMENT_COMPLETED)",
+ "flow1(SEQUENCE_FLOW_TAKEN)",
+ "error-task(ELEMENT_ACTIVATED)",
+ "error-task(ELEMENT_COMPLETED)",
+ "catchall-sub-process(ELEMENT_ACTIVATED)",
+ "catchall-start-event(ELEMENT_ACTIVATED)",
+ "catchall-start-event(ELEMENT_COMPLETED)",
+ "flow-catchall(SEQUENCE_FLOW_TAKEN)",
+ "handle-catchall-task(ELEMENT_ACTIVATED)",
+ "handle-catchall-task(ELEMENT_COMPLETED)",
+ "flow-catchall-end(SEQUENCE_FLOW_TAKEN)",
+ "end-catchall(ELEMENT_ACTIVATED)",
+ "end-catchall(ELEMENT_COMPLETED)",
+ "end-catchall(ELEMENT_COMPLETED)",
+ "catchall-sub-process(ELEMENT_COMPLETED)",
+ "error-event-subprocess(ELEMENT_COMPLETED)",
+ },
+ },
+ },
+ {
+ // In this test we expect that the error boundary event is triggered
+ name: "Multiple boundary error events and Subprocesses error1",
+ args: args{
+ file: "../../test-cases/error-boundary-event-and-subprocess.bpmn",
+ handlers: []handler{
+ {
+ id: "error-task",
+ fn: func(job ActivatedJob) {
+ job.SetVariable("aVariable", true)
+ job.ThrowError("error1")
+ },
+ },
+ {
+ id: "handle-error1-task",
+ fn: jobCompleteHandler,
+ },
+ },
+ },
+ wants: wants{
+ instanceState: Completed,
+ processError: is.Nil(),
+ varAssertions: []varAssertion{
+ {
+ key: "aVariable",
+ assertion: is.True(),
+ },
+ },
+ paths: []string{
+ "start(ELEMENT_ACTIVATED)",
+ "start(ELEMENT_COMPLETED)",
+ "flow1(SEQUENCE_FLOW_TAKEN)",
+ "error-task(ELEMENT_ACTIVATED)",
+ "error-task(ELEMENT_COMPLETED)",
+ "error-boundary-event(ELEMENT_ACTIVATED)",
+ "error-boundary-event(ELEMENT_COMPLETED)",
+ "flow-handle-error(SEQUENCE_FLOW_TAKEN)",
+ "handle-error1-task(ELEMENT_ACTIVATED)",
+ "handle-error1-task(ELEMENT_COMPLETED)",
+ "flow-handled-error(SEQUENCE_FLOW_TAKEN)",
+ "handled-error-end(ELEMENT_ACTIVATED)",
+ "handled-error-end(ELEMENT_COMPLETED)",
+ "handled-error-end(ELEMENT_COMPLETED)",
+ },
+ },
+ },
+ {
+ // In this test we expect that the error2 event subprocess is triggered
+ name: "Multiple boundary error events and Subprocesses error2",
+ args: args{
+ file: "../../test-cases/error-boundary-event-and-subprocess.bpmn",
+ handlers: []handler{
+ {
+ id: "error-task",
+ fn: func(job ActivatedJob) {
+ job.SetVariable("aVariable", true)
+ job.ThrowError("error2")
+ },
+ },
+ {
+ id: "handle-error2-sub-task",
+ fn: jobCompleteHandler,
+ },
+ },
+ },
+ wants: wants{
+ instanceState: Completed,
+ processError: is.Nil(),
+ varAssertions: []varAssertion{
+ {
+ key: "aVariable",
+ assertion: is.True(),
+ },
+ },
+ paths: []string{
+ "start(ELEMENT_ACTIVATED)",
+ "start(ELEMENT_COMPLETED)",
+ "flow1(SEQUENCE_FLOW_TAKEN)",
+ "error-task(ELEMENT_ACTIVATED)",
+ "error-task(ELEMENT_COMPLETED)",
+ "error-sub-process(ELEMENT_ACTIVATED)",
+ "error1-start-event(ELEMENT_ACTIVATED)",
+ "error1-start-event(ELEMENT_COMPLETED)",
+ "flow-error2(SEQUENCE_FLOW_TAKEN)",
+ "handle-error2-sub-task(ELEMENT_ACTIVATED)",
+ "handle-error2-sub-task(ELEMENT_COMPLETED)",
+ "flow-error2-end(SEQUENCE_FLOW_TAKEN)",
+ "end-error1(ELEMENT_ACTIVATED)",
+ "end-error1(ELEMENT_COMPLETED)",
+ "end-error1(ELEMENT_COMPLETED)",
+ "error-sub-process(ELEMENT_COMPLETED)",
+ "error-boundary-event-and-subprocess(ELEMENT_COMPLETED)",
+ },
+ },
+ },
+ {
+ // In this test we expect that the catchall error boundary event is triggered
+ name: "Multiple boundary error events and Subprocesses error3",
+ args: args{
+ file: "../../test-cases/error-boundary-event-and-subprocess.bpmn",
+ handlers: []handler{
+ {
+ id: "error-task",
+ fn: func(job ActivatedJob) {
+ job.SetVariable("aVariable", true)
+ job.ThrowError("error3")
+ },
+ },
+ {
+ id: "handle-all-task",
+ fn: jobCompleteHandler,
+ },
+ },
+ },
+ wants: wants{
+ instanceState: Completed,
+ processError: is.Nil(),
+ varAssertions: []varAssertion{
+ {
+ key: "aVariable",
+ assertion: is.True(),
+ },
+ },
+ paths: []string{
+ "start(ELEMENT_ACTIVATED)",
+ "start(ELEMENT_COMPLETED)",
+ "flow1(SEQUENCE_FLOW_TAKEN)",
+ "error-task(ELEMENT_ACTIVATED)",
+ "error-task(ELEMENT_COMPLETED)",
+ "all-boundary-event(ELEMENT_ACTIVATED)",
+ "all-boundary-event(ELEMENT_COMPLETED)",
+ "flow-handle-all(SEQUENCE_FLOW_TAKEN)",
+ "handle-all-task(ELEMENT_ACTIVATED)",
+ "handle-all-task(ELEMENT_COMPLETED)",
+ "flow-handled-all(SEQUENCE_FLOW_TAKEN)",
+ "handled-all-end(ELEMENT_ACTIVATED)",
+ "handled-all-end(ELEMENT_COMPLETED)",
+ "handled-all-end(ELEMENT_COMPLETED)",
+ },
+ },
+ }, {
+ name: "Single boundary error event unknown error",
+ args: args{
+ file: "../../test-cases/error-event-subprocess.bpmn",
+ handlers: []handler{
+ {
+ id: "error-task",
+ fn: func(job ActivatedJob) {
+ job.SetVariable("aVariable", true)
+ job.ThrowError("unknown_error")
+ },
+ },
+ {
+ id: "handle-error-task",
+ fn: jobCompleteHandler,
+ },
+ },
+ },
+ wants: wants{
+ instanceState: Failed,
+ processError: is.EqualTo(newEngineErrorf("Could not find error definition \"unknown_error\"")),
+ varAssertions: []varAssertion{
+ {
+ key: "aVariable",
+ assertion: is.True(),
+ },
+ },
+ paths: []string{
+ "start(ELEMENT_ACTIVATED)",
+ "start(ELEMENT_COMPLETED)",
+ "flow1(SEQUENCE_FLOW_TAKEN)",
+ "error-task(ELEMENT_ACTIVATED)",
+ "error-task(ELEMENT_COMPLETED)",
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // setup
+ bpmnEngine := New()
+ pathExporter := NewPathRecordingEventExporter()
+ bpmnEngine.AddEventExporter(pathExporter)
+ bpmnEngine.AddEventExporter(exporter.NewEventLogExporter())
+ process, _ := bpmnEngine.LoadFromFile(tt.args.file)
+ for _, handler := range tt.args.handlers {
+ bpmnEngine.NewTaskHandler().Id(handler.id).Handler(handler.fn)
+ }
+ instance, err := bpmnEngine.CreateAndRunInstance(process.ProcessKey, nil)
+ then.AssertThat(t, instance.ActivityState, is.EqualTo(tt.wants.instanceState))
+ then.AssertThat(t, err, tt.wants.processError)
+ then.AssertThat(t, pathExporter.String(), is.EqualTo(pathString(tt.wants.paths)))
+
+ for _, varAssert := range tt.wants.varAssertions {
+ then.AssertThat(t, instance.GetVariable(varAssert.key), varAssert.assertion)
+ }
+ })
+ }
+
+}
+
+// TODO boundaryEvent and eventSubProcesses where specific boundary event is selected
+// TODO boundaryEvent and eventSubProcesses where specific sub process is selected
+// TODO boundaryEvent and eventSubProcesses where boundaryevent catch all is selected
diff --git a/pkg/bpmn_engine/tasks.go b/pkg/bpmn_engine/tasks.go
index cb6a9a0c..a27f41b5 100644
--- a/pkg/bpmn_engine/tasks.go
+++ b/pkg/bpmn_engine/tasks.go
@@ -1,8 +1,10 @@
package bpmn_engine
-import "github.com/nitram509/lib-bpmn-engine/pkg/spec/BPMN20"
+import (
+ "github.com/nitram509/lib-bpmn-engine/pkg/spec/BPMN20"
+)
-func (state *BpmnEngineState) handleServiceTask(process BPMN20.ProcessElement, instance *processInstanceInfo, element *BPMN20.TaskElement) (bool, *job) {
+func (state *BpmnEngineState) handleServiceTask(process BPMN20.ProcessElement, instance *processInstanceInfo, element *BPMN20.TaskElement) (bool, *job, error) {
job := findOrCreateJob(&state.jobs, element, instance, state.generateKey)
handler := state.findTaskHandler(element)
@@ -10,8 +12,15 @@ func (state *BpmnEngineState) handleServiceTask(process BPMN20.ProcessElement, i
job.JobState = Active
variableHolder := NewVarHolder(&instance.VariableHolder, nil)
activatedJob := &activatedJob{
- processInstanceInfo: instance,
- failHandler: func(reason string) { job.JobState = Failed },
+ processInstanceInfo: instance,
+ failHandler: func(reason string) {
+ job.Failure = reason
+ job.JobState = Failed
+ },
+ errorHandler: func(error string) {
+ job.ErrorCode = error
+ job.JobState = Terminated
+ },
completeHandler: func() { job.JobState = Completed },
key: state.generateKey(),
processInstanceKey: instance.InstanceKey,
@@ -25,22 +34,26 @@ func (state *BpmnEngineState) handleServiceTask(process BPMN20.ProcessElement, i
if err := evaluateLocalVariables(&variableHolder, (*element).GetInputMapping()); err != nil {
job.JobState = Failed
instance.ActivityState = Failed
- return false, job
+ return false, job, err
}
handler(activatedJob)
- if job.JobState == Completed {
+ if job.JobState == Completed || job.JobState == Terminated {
if err := propagateProcessInstanceVariables(&variableHolder, (*element).GetOutputMapping()); err != nil {
job.JobState = Failed
instance.ActivityState = Failed
}
+ } else if job.JobState == Failed {
+ // If there is a technical error with the job, fail the instance
+ instance.ActivityState = Failed
+ return false, job, newEngineErrorf(job.Failure)
}
}
- return job.JobState == Completed, job
+ return job.JobState == Completed, job, nil
}
-func (state *BpmnEngineState) handleUserTask(process BPMN20.ProcessElement, instance *processInstanceInfo, element *BPMN20.TaskElement) *job {
+func (state *BpmnEngineState) handleUserTask(process BPMN20.ProcessElement, instance *processInstanceInfo, element *BPMN20.TaskElement) (*job, error) {
// TODO consider different handlers, since Service Tasks are different in their definition than user tasks
- _, j := state.handleServiceTask(process, instance, element)
- return j
+ _, j, err := state.handleServiceTask(process, instance, element)
+ return j, err
}
diff --git a/pkg/spec/BPMN20/bpmn_structs.go b/pkg/spec/BPMN20/bpmn_structs.go
index 1e643711..b6a1833c 100644
--- a/pkg/spec/BPMN20/bpmn_structs.go
+++ b/pkg/spec/BPMN20/bpmn_structs.go
@@ -12,6 +12,18 @@ type TDefinitions struct {
ExporterVersion string `xml:"exporterVersion,attr"`
Process TProcess `xml:"process"`
Messages []TMessage `xml:"message"`
+ Errors []TError `xml:"error"`
+}
+
+type TError struct {
+ Id string `xml:"id,attr"`
+ Name string `xml:"name,attr"`
+ ErrorCode string `xml:"errorCode,attr"`
+}
+
+type TErrorEventDefinition struct {
+ Id string `xml:"id,attr"`
+ ErrorRef string `xml:"errorRef,attr"`
}
type TCallableElement struct {
@@ -37,6 +49,16 @@ type TProcess struct {
IntermediateTrowEvent []TIntermediateThrowEvent `xml:"intermediateThrowEvent"`
EventBasedGateway []TEventBasedGateway `xml:"eventBasedGateway"`
InclusiveGateway []TInclusiveGateway `xml:"inclusiveGateway"`
+ BoundaryEvent []TBoundaryEvent `xml:"boundaryEvent"`
+}
+
+type TBoundaryEvent struct {
+ TBaseElement
+ Name string `xml:"name,attr"`
+ AttachedToRef string `xml:"attachedToRef,attr"`
+ OutgoingAssociation []string `xml:"outgoing"`
+ ErrorEventDefinition *TErrorEventDefinition `xml:"errorEventDefinition,omitempty"`
+ Output []extensions.TIoMapping `xml:"extensionElements>ioMapping>output"`
}
type TSubProcess struct {
@@ -54,6 +76,7 @@ type TSubProcess struct {
IntermediateTrowEvent []TIntermediateThrowEvent `xml:"intermediateThrowEvent"`
EventBasedGateway []TEventBasedGateway `xml:"eventBasedGateway"`
InclusiveGateway []TInclusiveGateway `xml:"inclusiveGateway"`
+ BoundaryEvent []TBoundaryEvent `xml:"boundaryEvent"`
}
// TBaseElement is an "abstract" struct
@@ -111,12 +134,14 @@ type TExpression struct {
type TStartEvent struct {
TCatchEvent
- IsInterrupting bool `xml:"isInterrupting,attr"`
- ParallelMultiple bool `xml:"parallelMultiple,attr"`
+ IsInterrupting bool `xml:"isInterrupting,attr"`
+ ParallelMultiple bool `xml:"parallelMultiple,attr"`
+ ErrorEventDefinition TErrorEventDefinition `xml:"errorEventDefinition"`
}
type TEndEvent struct {
TThrowEvent
+ ErrorEventDefinition TErrorEventDefinition `xml:"errorEventDefinition"`
}
type TServiceTask struct {
diff --git a/pkg/spec/BPMN20/elements.go b/pkg/spec/BPMN20/elements.go
index 0f312013..93ab9d81 100644
--- a/pkg/spec/BPMN20/elements.go
+++ b/pkg/spec/BPMN20/elements.go
@@ -4,6 +4,7 @@ import "github.com/nitram509/lib-bpmn-engine/pkg/spec/BPMN20/extensions"
type ElementType string
type GatewayDirection string
+type BoundaryType string
const (
Process ElementType = "PROCESS"
@@ -18,13 +19,17 @@ const (
IntermediateThrowEvent ElementType = "INTERMEDIATE_THROW_EVENT"
EventBasedGateway ElementType = "EVENT_BASED_GATEWAY"
InclusiveGateway ElementType = "INCLUSIVE_GATEWAY"
-
- SequenceFlow ElementType = "SEQUENCE_FLOW"
+ SequenceFlow ElementType = "SEQUENCE_FLOW"
+ BoundaryEvent ElementType = "BOUNDARY_EVENT"
Unspecified GatewayDirection = "Unspecified"
Converging GatewayDirection = "Converging"
Diverging GatewayDirection = "Diverging"
Mixed GatewayDirection = "Mixed"
+
+ // Type of boundary event, error, message, timer, etc
+ ErrorBoundary BoundaryType = "Error"
+ UnknownBoundary BoundaryType = "Unknown"
)
type BaseElement interface {
@@ -65,6 +70,7 @@ type ProcessElement interface {
GetEventBasedGateway() []TEventBasedGateway
GetSubProcess() []TSubProcess
GetInclusiveGateway() []TInclusiveGateway
+ GetBoundaryEvent() []TBoundaryEvent
}
func (startEvent TStartEvent) GetId() string {
@@ -428,6 +434,10 @@ func (process TProcess) GetInclusiveGateway() []TInclusiveGateway {
return process.InclusiveGateway
}
+func (process TProcess) GetBoundaryEvent() []TBoundaryEvent {
+ return process.BoundaryEvent
+}
+
func (subProcess TSubProcess) GetId() string {
return subProcess.Id
}
@@ -495,3 +505,38 @@ func (subProcess TSubProcess) GetSubProcess() []TSubProcess {
func (subProcess TSubProcess) GetInclusiveGateway() []TInclusiveGateway {
return subProcess.InclusiveGateway
}
+
+func (process TSubProcess) GetBoundaryEvent() []TBoundaryEvent {
+ return process.BoundaryEvent
+}
+
+func (be TBoundaryEvent) GetId() string {
+ return be.Id
+}
+
+func (be TBoundaryEvent) GetName() string {
+ return be.Name
+}
+
+func (be TBoundaryEvent) GetIncomingAssociation() []string {
+ return []string{}
+}
+
+func (be TBoundaryEvent) GetOutgoingAssociation() []string {
+ return be.OutgoingAssociation
+}
+
+func (be TBoundaryEvent) GetType() ElementType {
+ return BoundaryEvent
+}
+
+func (be TBoundaryEvent) GetBoundaryType() BoundaryType {
+ if be.ErrorEventDefinition != nil {
+ return ErrorBoundary
+ }
+ return UnknownBoundary
+}
+
+func (be TBoundaryEvent) GetOutputMapping() []extensions.TIoMapping {
+ return be.Output
+}
diff --git a/pkg/spec/BPMN20/elements_test.go b/pkg/spec/BPMN20/elements_test.go
index e3b1e08e..bccae29f 100644
--- a/pkg/spec/BPMN20/elements_test.go
+++ b/pkg/spec/BPMN20/elements_test.go
@@ -1,6 +1,13 @@
package BPMN20
-import "testing"
+import (
+ "github.com/corbym/gocrest/has"
+ "github.com/nitram509/lib-bpmn-engine/pkg/spec/BPMN20/extensions"
+ "testing"
+
+ "github.com/corbym/gocrest/is"
+ "github.com/corbym/gocrest/then"
+)
// tests to get quick compiler warnings, when interface is not correctly implemented
@@ -18,4 +25,55 @@ func Test_all_interfaces_implemented(t *testing.T) {
var _ BaseElement = &TIntermediateThrowEvent{}
var _ BaseElement = &TEventBasedGateway{}
var _ BaseElement = &TInclusiveGateway{}
+ var _ BaseElement = &TBoundaryEvent{}
+}
+
+func Test_ErrorBoundaryEvent(t *testing.T) {
+
+ event := TBoundaryEvent{
+ TBaseElement: TBaseElement{Id: "event_1", Documentation: "Documentation"},
+ Name: "Boundary Event",
+ AttachedToRef: "task_1",
+ OutgoingAssociation: []string{"flow_1"},
+ ErrorEventDefinition: &TErrorEventDefinition{
+ Id: "errorDef_1",
+ ErrorRef: "error_1",
+ },
+ Output: []extensions.TIoMapping{
+ {
+ Source: "ioSource",
+ Target: "ioTarget",
+ },
+ },
+ }
+ then.AssertThat(t, event.GetId(), is.EqualTo("event_1"))
+ then.AssertThat(t, event.GetName(), is.EqualTo("Boundary Event"))
+ then.AssertThat(t, event.GetIncomingAssociation(), has.Length(0))
+ then.AssertThat(t, event.GetOutgoingAssociation(), has.Length(1))
+ then.AssertThat(t, event.GetType(), is.EqualTo(BoundaryEvent))
+ then.AssertThat(t, event.GetBoundaryType(), is.EqualTo(ErrorBoundary))
+ then.AssertThat(t, event.GetOutputMapping(), has.Length(1))
+}
+
+func Test_UnknownBoundaryEvent(t *testing.T) {
+
+ event := TBoundaryEvent{
+ TBaseElement: TBaseElement{Id: "event_1", Documentation: "Documentation"},
+ Name: "Boundary Event",
+ AttachedToRef: "task_1",
+ OutgoingAssociation: []string{"flow_1"},
+ Output: []extensions.TIoMapping{
+ {
+ Source: "ioSource",
+ Target: "ioTarget",
+ },
+ },
+ }
+ then.AssertThat(t, event.GetId(), is.EqualTo("event_1"))
+ then.AssertThat(t, event.GetName(), is.EqualTo("Boundary Event"))
+ then.AssertThat(t, event.GetIncomingAssociation(), has.Length(0))
+ then.AssertThat(t, event.GetOutgoingAssociation(), has.Length(1))
+ then.AssertThat(t, event.GetType(), is.EqualTo(BoundaryEvent))
+ then.AssertThat(t, event.GetBoundaryType(), is.EqualTo(UnknownBoundary))
+ then.AssertThat(t, event.GetOutputMapping(), has.Length(1))
}
diff --git a/pkg/spec/BPMN20/helper.go b/pkg/spec/BPMN20/helper.go
index 7199c4fc..d3b14012 100644
--- a/pkg/spec/BPMN20/helper.go
+++ b/pkg/spec/BPMN20/helper.go
@@ -76,6 +76,9 @@ func FindBaseElementsById(processElement ProcessElement, id string) (elements []
for _, inclusiveGateway := range processElement.GetInclusiveGateway() {
appendWhenIdMatches(Ptr[BaseElement](inclusiveGateway))
}
+ for _, boundaryEvent := range processElement.GetBoundaryEvent() {
+ appendWhenIdMatches(Ptr[BaseElement](boundaryEvent))
+ }
for _, subProcess := range processElement.GetSubProcess() {
appendWhenIdMatches(Ptr[BaseElement](subProcess))
// search recursively for further elements
diff --git a/test-cases/error-boundary-event-and-subprocess.bpmn b/test-cases/error-boundary-event-and-subprocess.bpmn
new file mode 100644
index 00000000..270fffd6
--- /dev/null
+++ b/test-cases/error-boundary-event-and-subprocess.bpmn
@@ -0,0 +1,204 @@
+
+
+
+
+
+ flow-catchall
+
+
+
+ flow-catchall-end
+
+
+
+
+
+
+
+ flow-catchall
+ flow-catchall-end
+
+
+
+
+ flow-error2-end
+
+
+ flow-error2
+
+
+
+
+
+
+
+
+ flow-error2
+ flow-error2-end
+
+
+
+ flow1
+
+
+
+
+
+ flow1
+ flow-success
+
+
+ flow-handled-error
+
+
+ flow-success
+
+
+
+
+
+ flow-handle-error
+ flow-handled-error
+
+
+ flow-handled-all
+
+
+
+
+
+ flow-handle-all
+ flow-handled-all
+
+
+
+
+
+
+
+ flow-handle-all
+
+
+
+
+
+
+
+
+ flow-handle-error
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test-cases/error-boundary-event-catchall.bpmn b/test-cases/error-boundary-event-catchall.bpmn
new file mode 100644
index 00000000..ad049f14
--- /dev/null
+++ b/test-cases/error-boundary-event-catchall.bpmn
@@ -0,0 +1,82 @@
+
+
+
+
+ flow1
+
+
+
+
+
+ flow1
+ flow-success
+
+
+
+
+
+
+
+
+ flow-handle-error
+
+
+
+ flow-handled-error
+
+
+
+ flow-success
+
+
+
+
+
+
+ flow-handle-error
+ flow-handled-error
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test-cases/error-boundary-event-multiple.bpmn b/test-cases/error-boundary-event-multiple.bpmn
new file mode 100644
index 00000000..dd4960d2
--- /dev/null
+++ b/test-cases/error-boundary-event-multiple.bpmn
@@ -0,0 +1,125 @@
+
+
+
+
+ flow1
+
+
+
+
+
+ flow1
+ flow-success
+
+
+
+
+
+
+
+
+ flow-handle-error
+
+
+
+ flow-handled-error
+
+
+
+ flow-success
+
+
+
+
+
+
+ flow-handle-error
+ flow-handled-error
+
+
+
+
+
+
+
+
+ flow-handle-all
+
+
+
+
+ flow-handled-all
+
+
+
+
+
+
+ flow-handle-all
+ flow-handled-all
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test-cases/error-boundary-event-outputs.bpmn b/test-cases/error-boundary-event-outputs.bpmn
new file mode 100644
index 00000000..02902c83
--- /dev/null
+++ b/test-cases/error-boundary-event-outputs.bpmn
@@ -0,0 +1,82 @@
+
+
+
+
+ flow1
+
+
+
+
+
+ flow1
+ flow-success
+
+
+
+
+
+
+
+
+ flow-handle-error
+
+
+
+ flow-handled-error
+
+
+
+ flow-success
+
+
+
+
+
+
+ flow-handle-error
+ flow-handled-error
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test-cases/error-boundary-event.bpmn b/test-cases/error-boundary-event.bpmn
new file mode 100644
index 00000000..2c1e287c
--- /dev/null
+++ b/test-cases/error-boundary-event.bpmn
@@ -0,0 +1,77 @@
+
+
+
+
+ flow1
+
+
+
+
+
+ flow1
+ flow-success
+
+
+
+ flow-handle-error
+
+
+
+ flow-handled-error
+
+
+
+ flow-success
+
+
+
+
+
+
+ flow-handle-error
+ flow-handled-error
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test-cases/error-event-subprocess.bpmn b/test-cases/error-event-subprocess.bpmn
new file mode 100644
index 00000000..b3a0fb34
--- /dev/null
+++ b/test-cases/error-event-subprocess.bpmn
@@ -0,0 +1,122 @@
+
+
+
+
+
+ flow-catchall
+
+
+
+ flow-catchall-end
+
+
+
+
+
+
+
+ flow-catchall
+ flow-catchall-end
+
+
+
+ flow-success
+
+
+
+
+
+ flow1
+ flow-success
+
+
+
+ flow-error1-end
+
+
+ flow-error1
+
+
+
+
+
+
+
+
+ flow-error1
+ flow-error1-end
+
+
+
+
+
+ flow1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+