lib-bpmn-engine icon indicating copy to clipboard operation
lib-bpmn-engine copied to clipboard

support for backwards

Open lastchiliarch opened this issue 3 years ago • 2 comments
trafficstars

Do we have any plan to support backwards ?

Problem: Assume that we allow user to repay the order instead of creating a new order when user choose to refund before confirm-receipt, it can handle confirm-recepit event and go to send-bill again. But it can't handle the next flow becasue of some event had already trigged. I can't clear or reset the processInfo either.

The order repay flow may sound weird, but it's common for me to desigin flows like that.

I'm trying to design a workflow that user can handle an event then supervisor recheck it. If the event handled before has some wrong, the supervisor can turn the flow to previous flow again. so that the previous user can handle it again until the supervisor recheck pass.

At now, I can't archive this goal graceful without backwards support. Do you have some suggestions?

image

lastchiliarch avatar Aug 29 '22 12:08 lastchiliarch

Hi @lastchiliarch if I got you right, then you want to go backward, like in a loop? I mean basically, what does this loop BPMN shown below?

That is likely not supported in the current version ... but I will think about, how to implement it 👍

Thank you for your feedback. Also, PRs are always welcome :)

Screenshot 2022-08-29 at 14 47 04

nitram509 avatar Aug 29 '22 12:08 nitram509

Yes, that's what i want. I'm new to bpmn so the implement is diffcult for me . But I'd like to help doing a part of job of this one or anything I can.

lastchiliarch avatar Aug 30 '22 02:08 lastchiliarch

Hi @lastchiliarch

I've implemented a test, that show "backwards" works in this simple loop example above. See https://github.com/nitram509/lib-bpmn-engine/blob/532d2d18be8253b71d643fabd679d48e2170076a/pkg/bpmn_engine/engine_jobs_test.go#L23

nitram509 avatar Sep 02 '22 19:09 nitram509

I re-read your initial post and I have to admit, I don't understand your business requirement. What I can say, BPMN does not support "reset" == if you design some condition after the "confirm receipt", the bpmn flow will continue a second round.

This is likeley not what you want in business, I guess. I would rather suggest, to model a separate BPMN process for refunding.

A typical refind process could look like:

  1. find existing order process
  2. cancel existing order process (lib-bpmn-engine does not support cancelling yet, but BPMN itself is desgined for that)
  3. validate refund conditions
  4. subtract refund fees
  5. make payment order, so the user gets its money back

The benefit of having two processes are

  • more transparency, what was happen, when you "link" the processes by their keys
  • easier to understand
  • easier to distribute

How does this resonate with you?

nitram509 avatar Sep 02 '22 19:09 nitram509

re-read your initial post and I have to admit, I don't understand your business requirement. What I can say, BPMN does not support "reset" == if you design some condition after the "confirm receipt", the bpmn flow will continue a second round.

Yes, I want reset function.

Some of my workflow likes below. image

  1. user try to apply some auth
  2. reviewer audit the application and tag some label like city
  3. supervisor make the final confirm
  4. if supervisor think some label is not correct , he will ask the reviewer to re-audit it.
  5. reviewr re-aduit the tag updated label
  6. supervisor check the applcation again like step 4
  7. approve the auth if everything goes well

Since the enginer does not support reset, it will be traped into infinite loop if I don't clear the op variable in notifySupervisor. So What I want is clearing the processInfo where event is trigged in backwards case. If bpmn is not designed to support this, I'd like to have some functiones to clear or delete the processInfo variables manually to archive this goal.

Some is some of my test code and bpmn file.

var (
	bpmnEngine = New("auth_apply")
	process *ProcessInfo
	instance *ProcessInstanceInfo
)

func Test_apply_auth_loop(t *testing.T) {
	// setup
	var err error
	process, err = bpmnEngine.LoadFromFile("../../test-cases/auth_apply.bpmn")
	bpmnEngine.AddTaskHandler("apply_auth", applyAuth)
	bpmnEngine.AddTaskHandler("audit", audit)
	bpmnEngine.AddTaskHandler("prove_auth", proveAuth)
	bpmnEngine.AddTaskHandler("notify_supervisor", notifySupervisor)

	instance, err = bpmnEngine.CreateInstance(process.ProcessKey, nil)
	fmt.Println(err)
	bpmnEngine.RunOrContinueInstance(instance.GetInstanceKey())
	then.AssertThat(t, instance.GetVariable("city"), is.EqualTo("Beijing"))
	then.AssertThat(t, instance.state, is.EqualTo(process_instance.COMPLETED))
}

func applyAuth(job ActivatedJob)  {
	log.Println("start to applyAuth")
	job.Complete()
}

func audit(job ActivatedJob)  {
	log.Println("start to audit")
	op := job.GetVariable("op")
	if op == nil {
		job.SetVariable("city", "London")
		bpmnEngine.PublishEventForInstance(instance.GetInstanceKey(), "audit_by_reviewer")
		job.Complete()
		return
	}
	auditop := op.(string)
	if auditop != "" {
		job.SetVariable("city", "Beijing")
		bpmnEngine.PublishEventForInstance(instance.GetInstanceKey(), "audit_by_reviewer")
		job.Complete()
	} else {
		job.SetVariable("city", "London")
		bpmnEngine.PublishEventForInstance(instance.GetInstanceKey(), "audit_by_reviewer")
		job.Complete()
	}
}

func proveAuth(job ActivatedJob)  {
	log.Println("start to proveAuth")
	job.Complete()
}

func notifySupervisor(job ActivatedJob)  {
	log.Println("start to notifySupervisor")
	city := job.GetVariable("city").(string)
	if city != "Beijing" {
		job.SetVariable("op", "re-audit")
	} else {
		job.SetVariable("op", "")
	}
	bpmnEngine.PublishEventForInstance(instance.GetInstanceKey(), "confirm_by_supervisor")
	job.Complete()
}
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:zeebe="http://camunda.org/schema/zeebe/1.0" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_1l2k2zy" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.2.0" modeler:executionPlatform="Camunda Cloud" modeler:executionPlatformVersion="8.0.0">
  <bpmn:process id="Process_16ue9kj" isExecutable="true">
    <bpmn:startEvent id="StartEvent_1">
      <bpmn:outgoing>Flow_0fwz2ln</bpmn:outgoing>
    </bpmn:startEvent>
    <bpmn:serviceTask id="prove_auth" name="prove_auth">
      <bpmn:extensionElements>
        <zeebe:taskDefinition type="prove_auth" />
      </bpmn:extensionElements>
      <bpmn:incoming>Flow_0bqwngy</bpmn:incoming>
      <bpmn:outgoing>Flow_0l4cgrv</bpmn:outgoing>
    </bpmn:serviceTask>
    <bpmn:endEvent id="Event_0jv5wxq">
      <bpmn:incoming>Flow_0l4cgrv</bpmn:incoming>
    </bpmn:endEvent>
    <bpmn:sequenceFlow id="Flow_0bqwngy" sourceRef="Gateway_1dmvvpa" targetRef="prove_auth" />
    <bpmn:serviceTask id="apply_auth" name="apply_auth">
      <bpmn:extensionElements>
        <zeebe:taskDefinition type="apply_auth" />
      </bpmn:extensionElements>
      <bpmn:incoming>Flow_0fwz2ln</bpmn:incoming>
      <bpmn:outgoing>Flow_1lr5ur1</bpmn:outgoing>
    </bpmn:serviceTask>
    <bpmn:intermediateCatchEvent id="confirm_by_supervisor" name="confirm_by_supervisor">
      <bpmn:incoming>Flow_177ps1y</bpmn:incoming>
      <bpmn:outgoing>Flow_0wvvvjc</bpmn:outgoing>
      <bpmn:messageEventDefinition id="MessageEventDefinition_1mr6cvo" messageRef="Message_1agfbps" />
    </bpmn:intermediateCatchEvent>
    <bpmn:intermediateCatchEvent id="audit_by_reviewer" name="audit_by_reviewer">
      <bpmn:incoming>Flow_1grwq17</bpmn:incoming>
      <bpmn:outgoing>Flow_11yboz4</bpmn:outgoing>
      <bpmn:messageEventDefinition id="MessageEventDefinition_0j6i8wh" messageRef="Message_27ugmpv" />
    </bpmn:intermediateCatchEvent>
    <bpmn:sequenceFlow id="Flow_0wvvvjc" sourceRef="confirm_by_supervisor" targetRef="Gateway_1dmvvpa" />
    <bpmn:parallelGateway id="Gateway_07ivcgb">
      <bpmn:incoming>Flow_11yboz4</bpmn:incoming>
      <bpmn:outgoing>Flow_06la618</bpmn:outgoing>
    </bpmn:parallelGateway>
    <bpmn:sequenceFlow id="Flow_11yboz4" sourceRef="audit_by_reviewer" targetRef="Gateway_07ivcgb" />
    <bpmn:eventBasedGateway id="Gateway_1t2zx79">
      <bpmn:incoming>Flow_0e397yl</bpmn:incoming>
      <bpmn:outgoing>Flow_1grwq17</bpmn:outgoing>
    </bpmn:eventBasedGateway>
    <bpmn:sequenceFlow id="Flow_1grwq17" sourceRef="Gateway_1t2zx79" targetRef="audit_by_reviewer" />
    <bpmn:serviceTask id="notify_supervisor" name="notify_supervisor">
      <bpmn:extensionElements>
        <zeebe:taskDefinition type="notify_supervisor" />
      </bpmn:extensionElements>
      <bpmn:incoming>Flow_06la618</bpmn:incoming>
      <bpmn:outgoing>Flow_0c8hcd4</bpmn:outgoing>
    </bpmn:serviceTask>
    <bpmn:sequenceFlow id="Flow_06la618" sourceRef="Gateway_07ivcgb" targetRef="notify_supervisor" />
    <bpmn:eventBasedGateway id="Gateway_00r4evy">
      <bpmn:incoming>Flow_0c8hcd4</bpmn:incoming>
      <bpmn:outgoing>Flow_177ps1y</bpmn:outgoing>
    </bpmn:eventBasedGateway>
    <bpmn:sequenceFlow id="Flow_0c8hcd4" sourceRef="notify_supervisor" targetRef="Gateway_00r4evy" />
    <bpmn:sequenceFlow id="Flow_177ps1y" sourceRef="Gateway_00r4evy" targetRef="confirm_by_supervisor" />
    <bpmn:sequenceFlow id="Flow_0l4cgrv" sourceRef="prove_auth" targetRef="Event_0jv5wxq" />
    <bpmn:sequenceFlow id="Flow_1lr5ur1" sourceRef="apply_auth" targetRef="audit" />
    <bpmn:sequenceFlow id="Flow_0e397yl" sourceRef="audit" targetRef="Gateway_1t2zx79" />
    <bpmn:sequenceFlow id="re_audit" name="op=&#39;re-audit&#34;" sourceRef="Gateway_1dmvvpa" targetRef="audit">
      <bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=op matches "re-audit"</bpmn:conditionExpression>
    </bpmn:sequenceFlow>
    <bpmn:serviceTask id="audit" name="audit">
      <bpmn:extensionElements>
        <zeebe:taskDefinition type="audit" />
      </bpmn:extensionElements>
      <bpmn:incoming>Flow_1lr5ur1</bpmn:incoming>
      <bpmn:incoming>re_audit</bpmn:incoming>
      <bpmn:outgoing>Flow_0e397yl</bpmn:outgoing>
    </bpmn:serviceTask>
    <bpmn:sequenceFlow id="Flow_0fwz2ln" sourceRef="StartEvent_1" targetRef="apply_auth" />
    <bpmn:exclusiveGateway id="Gateway_1dmvvpa">
      <bpmn:incoming>Flow_0wvvvjc</bpmn:incoming>
      <bpmn:outgoing>Flow_0bqwngy</bpmn:outgoing>
      <bpmn:outgoing>re_audit</bpmn:outgoing>
    </bpmn:exclusiveGateway>
  </bpmn:process>
  <bpmn:message id="Message_27ugmpv" name="audit_by_reviewer">
    <bpmn:extensionElements>
      <zeebe:subscription correlationKey="=key" />
    </bpmn:extensionElements>
  </bpmn:message>
  <bpmn:message id="Message_067eof3" name="Message_067eof3">
    <bpmn:extensionElements>
      <zeebe:subscription correlationKey="=key" />
    </bpmn:extensionElements>
  </bpmn:message>
  <bpmn:message id="Message_1agfbps" name="confirm_by_supervisor">
    <bpmn:extensionElements>
      <zeebe:subscription correlationKey="=key" />
    </bpmn:extensionElements>
  </bpmn:message>
  <bpmndi:BPMNDiagram id="BPMNDiagram_1">
    <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_16ue9kj">
      <bpmndi:BPMNEdge id="Flow_0fwz2ln_di" bpmnElement="Flow_0fwz2ln">
        <di:waypoint x="168" y="177" />
        <di:waypoint x="200" y="177" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_199ps6v_di" bpmnElement="re_audit">
        <di:waypoint x="1000" y="152" />
        <di:waypoint x="1000" y="90" />
        <di:waypoint x="390" y="90" />
        <di:waypoint x="390" y="137" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="665" y="72" width="63" height="14" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_0e397yl_di" bpmnElement="Flow_0e397yl">
        <di:waypoint x="440" y="177" />
        <di:waypoint x="475" y="177" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_1lr5ur1_di" bpmnElement="Flow_1lr5ur1">
        <di:waypoint x="300" y="177" />
        <di:waypoint x="340" y="177" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_0l4cgrv_di" bpmnElement="Flow_0l4cgrv">
        <di:waypoint x="1190" y="177" />
        <di:waypoint x="1272" y="177" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_177ps1y_di" bpmnElement="Flow_177ps1y">
        <di:waypoint x="865" y="177" />
        <di:waypoint x="892" y="177" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_0c8hcd4_di" bpmnElement="Flow_0c8hcd4">
        <di:waypoint x="790" y="177" />
        <di:waypoint x="815" y="177" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_06la618_di" bpmnElement="Flow_06la618">
        <di:waypoint x="665" y="177" />
        <di:waypoint x="690" y="177" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_1grwq17_di" bpmnElement="Flow_1grwq17">
        <di:waypoint x="525" y="177" />
        <di:waypoint x="552" y="177" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_11yboz4_di" bpmnElement="Flow_11yboz4">
        <di:waypoint x="588" y="177" />
        <di:waypoint x="615" y="177" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_0wvvvjc_di" bpmnElement="Flow_0wvvvjc">
        <di:waypoint x="928" y="177" />
        <di:waypoint x="975" y="177" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_0bqwngy_di" bpmnElement="Flow_0bqwngy">
        <di:waypoint x="1025" y="177" />
        <di:waypoint x="1090" y="177" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
        <dc:Bounds x="132" y="159" width="36" height="36" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_0d43kkb_di" bpmnElement="apply_auth">
        <dc:Bounds x="200" y="137" width="100" height="80" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Event_05mf2rz_di" bpmnElement="confirm_by_supervisor">
        <dc:Bounds x="892" y="159" width="36" height="36" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="865" y="121.5" width="89" height="27" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Event_001ghjo_di" bpmnElement="audit_by_reviewer">
        <dc:Bounds x="552" y="159" width="36" height="36" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="527" y="202" width="86" height="27" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Gateway_1mfbkhy_di" bpmnElement="Gateway_07ivcgb">
        <dc:Bounds x="615" y="152" width="50" height="50" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Gateway_1u2qqcb_di" bpmnElement="Gateway_1t2zx79">
        <dc:Bounds x="475" y="152" width="50" height="50" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_1be98t1_di" bpmnElement="notify_supervisor">
        <dc:Bounds x="690" y="137" width="100" height="80" />
        <bpmndi:BPMNLabel />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Gateway_1kgw3tw_di" bpmnElement="Gateway_00r4evy">
        <dc:Bounds x="815" y="152" width="50" height="50" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_1doq7r3_di" bpmnElement="audit">
        <dc:Bounds x="340" y="137" width="100" height="80" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Event_0jv5wxq_di" bpmnElement="Event_0jv5wxq">
        <dc:Bounds x="1272" y="159" width="36" height="36" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_0zsfe4z_di" bpmnElement="prove_auth">
        <dc:Bounds x="1090" y="137" width="100" height="80" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Gateway_0vfgvoa_di" bpmnElement="Gateway_1dmvvpa" isMarkerVisible="true">
        <dc:Bounds x="975" y="152" width="50" height="50" />
      </bpmndi:BPMNShape>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</bpmn:definitions>

lastchiliarch avatar Sep 06 '22 07:09 lastchiliarch

Got your point ... there seems to be another bug, working on it.

nitram509 avatar Sep 07 '22 08:09 nitram509

Hi siconghuang,

I did just a minute ago post some fixes, which were still present, for the bug mentioned above.

I did some modifications as well, to make the code + BPMN more simple.

  • IF there's just a single intermediate message catch event in the flow, you don't need a event gateway in front neither an exclusive gateway afterward
  • you should not send events from inside a service task
    • that might be technically possible, but is not necessary code, the engine does the process execution for you
    • it's very difficult to read/understand such code
  • from your example, I think it would be more convenient to have the #41 implemented, BUT as you can see, the "validate supervisor result" task takes care of updating the proper variable within the instance.

Screenshot 2022-09-07 at 21 41 55

import (
	"fmt"
	"github.com/corbym/gocrest/is"
	"github.com/corbym/gocrest/then"
	"github.com/nitram509/lib-bpmn-engine/pkg/bpmn_engine"
	"github.com/nitram509/lib-bpmn-engine/pkg/spec/BPMN20/process_instance"
	"log"
	"testing"
)

var (
	bpmnEngine = bpmn_engine.New("auth_apply")
	process    *bpmn_engine.ProcessInfo
	instance   *bpmn_engine.ProcessInstanceInfo
)

// these test-* variables do mimic/simulate external events or manual actions
var testAuditAttempt = 0

const BEIJING = "Beijing"
const LONDON = "London"

func Test_apply_auth_loop(t *testing.T) {
	// setup
	var err error
	process, err = bpmnEngine.LoadFromFile("auth_apply.bpmn")
	bpmnEngine.AddTaskHandler("apply_auth", applyAuth)
	bpmnEngine.AddTaskHandler("audit", audit)
	bpmnEngine.AddTaskHandler("notify_supervisor", notifySupervisor)
	bpmnEngine.AddTaskHandler("validate_supervisor_result", validateSupervisorResult)
	bpmnEngine.AddTaskHandler("prove_auth", proveAuth)

	instance, err = bpmnEngine.CreateInstance(process.ProcessKey, nil)
	fmt.Println(err)

	// HINT: BPMN spec allows message published with variables, but the engine does not (yet) support that
	// so ideally, bpmnEngine.PublishEventForInstance() should support variables to pass in

	bpmnEngine.RunOrContinueInstance(instance.GetInstanceKey())
	then.AssertThat(t, instance.GetVariable("city"), is.EqualTo(LONDON))

	bpmnEngine.PublishEventForInstance(instance.GetInstanceKey(), "supervision_reported") // first response from supervisor
	bpmnEngine.RunOrContinueInstance(instance.GetInstanceKey())                           // will loop back and executes the audit as well, stops because of second intermediate catch event

	bpmnEngine.PublishEventForInstance(instance.GetInstanceKey(), "supervision_reported") // first response from supervisor
	bpmnEngine.RunOrContinueInstance(instance.GetInstanceKey())                           // will catch up the second event and exits the instance
	then.AssertThat(t, instance.GetVariable("city"), is.EqualTo(BEIJING))

	then.AssertThat(t, instance.GetState(), is.EqualTo(process_instance.COMPLETED))
}

func validateSupervisorResult(job bpmn_engine.ActivatedJob) {
	reAudit := testAuditAttempt <= 1 // for testing purpose, the first attempt is always wrong
	log.Println(fmt.Sprintf("validateSupervisorResult, reAudit=%t", reAudit))
	job.SetVariable("reAudit", reAudit)
	job.Complete()
}

func applyAuth(job bpmn_engine.ActivatedJob) {
	log.Println("start to applyAuth")
	job.Complete()
}

func audit(job bpmn_engine.ActivatedJob) {
	log.Println("start to audit")
	if testAuditAttempt < 1 {
		job.SetVariable("city", LONDON)
	} else {
		job.SetVariable("city", BEIJING)
	}
	testAuditAttempt = testAuditAttempt + 1 // change audit result for next time
	job.Complete()
}

func proveAuth(job bpmn_engine.ActivatedJob) {
	log.Println("start to proveAuth")
	job.Complete()
}

func notifySupervisor(job bpmn_engine.ActivatedJob) {
	city := job.GetVariable("city").(string)
	log.Println(fmt.Sprintf("start to notifySupervisor; city=%s", city))
	job.Complete()
}

BPMN Source ... auth_apply.bpmn.txt

nitram509 avatar Sep 07 '22 19:09 nitram509

@lastchiliarch does my above proposal help?

nitram509 avatar Sep 07 '22 19:09 nitram509

@lastchiliarch does my above proposal help?

Thanks for your help and hard work. It's diffcult for the "validate supervisor result" task to control the flow, seems message event could be a more graceful approach.

lastchiliarch avatar Sep 08 '22 07:09 lastchiliarch