Email Reminders using two boundary event timers


#1

This question is regarding a pattern discussed in Timer Boundary Event => error but using the Fluent Builder.

Using the Fluent Builder I am attempting to place two boundary timer events on a user task. One is non-interrupting and just periodically sends reminders. The other is interrupting and abandons the task. When I attempt to build the pattern as shown in this example:

    override fun buildModel(processBuilder: org.camunda.bpm.model.bpmn.builder.ProcessBuilder): EndEventBuilder {
        return processBuilder
                .name("Reminder Demo")
                .startEvent()
                .userTask("readEmail")
                .boundaryEvent()
                    .timerWithDuration("PT1H")
                    .cancelActivity(false)
                .boundaryEvent()
                    .timerWithCycle("R3/PT10M")
                .endEvent()
    }

Currently this results in an unparsable bpmn file. Are multiple boundary events supported in the fluent builder yet? How might I incorporate this use case?

Thanks!

Dan


#2

@dan

 BpmnModelInstance model = Bpmn.createExecutableProcess('model')
                .name("Reminder Demo")
                .startEvent()
                .userTask('readEmail')
                    .boundaryEvent()
                        .timerWithDuration("PT1H")
                        .cancelActivity(false)
                        .manualTask()
                            .name('do something')
                        .endEvent()
                        .moveToActivity('readEmail')
                .boundaryEvent()
                    .timerWithCycle("R3/PT10M")
                    .manualTask()
                        .name('do something else')
                    .endEvent()
                    .moveToActivity('readEmail')
                .endEvent()
                .done()

creates:
57%20PM

then with some manual cleanup:

17%20PM


#3

Take a look at:

and

and run the test. It will output the bpmns into the target folder.


#4

@dan did this work for you?


#5

Hey @StephenOTT! Thanks for the reply. This is definitely how to solve this problem. It worked, but I wound up going down a rabbit hole regarding how repeated events work, and timers…

Maybe this will be helpful for future readers, since I changed around my logic a little bit:

 BpmnModelInstance model = Bpmn.createExecutableProcess('model')
                .name("Reminder Demo")
                .startEvent()
                .userTask('readEmail')
                    .boundaryEvent()
                        .timerWithDuration("PT1H")
                        .cancelActivity(true)
                        .endEvent()
                        .moveToActivity('readEmail')
                .boundaryEvent()
                    .timerWithCycle("R3/PT10M")
                    .serviceTask()
                        .name('reminderSent')
                        .cancelActivity(false)
                    .endEvent()
                    .moveToActivity('readEmail')
                .endEvent()
                .done()

When I read this, I expect that the reading the email has a one-hour-timer and no matter what after that hour the process will move on. I also expect the reminder code to fire off every ten minutes, three times in total.

An example might be helpful:
10:00AM: “readEmail” starts
10:10AM: “reminderSent”
10:20AM: “reminderSent”
10:30AM: “reminderSent”
11:00AM: “readEmail” is cancelled, process moves on.

How do you read the above?


#6

Okay so:

Your Model has a few minor issues; There is a error with the reminderSent section, where you have the Cancel Activity as part of the ServiceTask, and the servicetask does not have proper config.

If you are using a IDE, you should see these errors with the code-completion, as well as that model will throw a error when trying to deploy/execute the process in the engine.

  1. If i fix up the model with:
        BpmnModelInstance model = Bpmn.createExecutableProcess('model')
                .name("Reminder Demo")
                .startEvent()
                .userTask('readEmail')
                    .boundaryEvent()
                    .timerWithDuration("PT1H")
                    .cancelActivity(true)
                    .endEvent()
                    .moveToActivity('readEmail')
                .boundaryEvent()
                    .timerWithCycle("R3/PT10M")
                    .cancelActivity(false)
                    .serviceTask()
                        .name('reminderSent')
                        .implementation('expression')
                        .camundaExpression('${1+1}')
                    .endEvent()
                    .moveToActivity('readEmail')
                .endEvent()
                .done()
        return model

which is a fully deployable model, it outputs the following image:

model1
(post visual adjustment)

Assuming “move on” means that the process will 'end; then yes after atleast ~1h (there is of course possible delay due to job executor picking up the job), from when the user task is created, the task will no longer be available; as per your PT1H.

and your non-interrupting timer is: R3/PT10M, which would execute every 10 minutes for a max total of 3 times. (less if the user task is complete before that). So your logic appears correct.

Assuming you are not having a single task as your process, you might be looking for something like this:

        BpmnModelInstance model = Bpmn.createExecutableProcess('model')
                .name("Reminder Demo")
                .startEvent()
                .userTask('readEmail')
                    .boundaryEvent('killusertask')
                    .timerWithDuration("PT1H")
                    .cancelActivity(true)
                    .moveToActivity('readEmail')
                .boundaryEvent()
                    .timerWithCycle("R3/PT10M")
                    .cancelActivity(false)
                    .serviceTask()
                        .name('reminderSent')
                        .implementation('expression')
                        .camundaExpression('${1+1}')
                    .endEvent()
                    .moveToActivity('readEmail')
                .manualTask('manual1').name('do something')
                .moveToNode('killusertask').connectTo('manual1')
                //.moveToActivity('killusertask').connectTo('manual1') This does not work. Must use the moveToNode()
                .manualTask('manual2').name('do something else')
                .endEvent()
                .done()
        return model

edit: also take note about where the .modeToNode() line is in the code; It is after we have created the ‘do something’ task, as the API appears to move in order, thus you are unable to connect the timer to a task that does not exist yet in the flow, because you have not reached it in the fluent api
The moveToNode() has to be used because the connectTo() is part of the FlowNode system rather than the activity.

which will create (after visual cleanup):

model1


#7

I really appreciate your feedback on this. I was “translating” my problem to something simpler, and I think I messed up the above snippet in translation. I figure maybe looking at the actual code I am using would be helpful.

I followed your second example, and while it produces viable BPMN, I believe that because I am in a subprocess that something else is going awry.

Here is my actual problemspace. I send a user a link to order lunch, and remind them to do so every ten minutes

.subProcess()
.embeddedSubProcess()
.startEvent()

// This just makes sure there is a 'placedOrder' in the process
.serviceTask()
    .withClass(InstantiateOrderPlacedDelegate::class)

//Assign them an order
.userTask("placeOrders")
    .name("Place your order at: #{orderLink}")
    .userForm("placedOrder", "I placed my order!", UserFormType.BOOLEAN.type, "false")
    .assignee("#{lunchGetter.id}")


    .boundaryEvent("killUserTask")
    .timerWithDuration("PT1H")
        .cancelActivity(true)
        .moveToActivity<UserTaskBuilder>("placeOrders")

    .boundaryEvent()
        .timerWithCycle("R3/PT10M")
        .cancelActivity(false)
        .serviceTask("reminderIfNeeded")
        .withClass(OrderLunchReminderDelegate::class)
        .endEvent()
        .moveToActivity<UserTaskBuilder>("placeOrders")

.serviceTask("timesUpOrOrderComplete")
    .name("timesUpOrOrderComplete")
    .implementation("expression")
    .camundaExpression("\${1 + 1}")

.moveToNode("killUserTask").connectTo("timeUpOrOrderComplete")

.endEvent()
.subProcessDone()
    .multiInstance()
        .parallel()
        .camundaCollection("#{gettingLunch}")
        .camundaElementVariable("lunchGetter")
    .multiInstanceDone<SubProcessBuilder>()

What I am seeing is the reminders get generated many more than the three I expect, and something just run until my JVM runs out of memory.

I think process wise what you’ve shown should work, but something under the hood of Camunda is behaving strangely. Any ideas on if subprocesses and timecycles have strange interactions?


#8

Can you post proper executable fluent api code. The exmaple you are giving is not valid


#9

So here is executable code:

       BpmnModelInstance model = Bpmn.createExecutableProcess('model')
                .startEvent()
                .subProcess()
                    .embeddedSubProcess()
                    .startEvent()
                    .manualTask()
                    .userTask("placeOrders")
                        .name("Place your order at: 1234")
                        .camundaAssignee("someUser")
                        .boundaryEvent("killUserTask")
                            .timerWithDuration("PT1H")
                            .cancelActivity(true)
                            .moveToActivity("placeOrders")
                        .boundaryEvent()
                            .timerWithCycle("R3/PT10M")
                            .cancelActivity(false)
                            .manualTask("reminderIfNeeded")
                            .endEvent()
                            .moveToActivity("placeOrders")
                    .serviceTask("timesUpOrOrderComplete")
                        .name("timesUpOrOrderComplete")
                        .implementation("expression")
                        .camundaExpression("\${1 + 1}")
                    .moveToNode("killUserTask").connectTo("timesUpOrOrderComplete")
                    .endEvent()
                .subProcessDone()
                    .multiInstance()
                    .parallel()
                    .camundaCollection("#{gettingLunch}")
                    .camundaElementVariable("lunchGetter")
                    .multiInstanceDone()
                .endEvent()
                .done()

which generates

and the config looks fine:


Can you explain what"many more times" means. Provide very specific example please?

What does the collection you are sending into the subprocess “#{gettingLunch}” look like? You dont have the same user in the collection multiple times?

edit: you also have a typo in your “timesUpOrOrderComplete” task id and the .moveToNode("killUserTask").connectTo("timesUpOrOrderComplete") line. You had different spelling.


#10

Sorry about that, I am using Kotlin, and some of the things I wrote are extension functions so I needed to ‘unpack’ them back into the Fluent Builder.

class ForumPost(override val key: String) : AbstractProcess() {

    override fun buildModel(processBuilder: ProcessBuilder): EndEventBuilder {
        return processBuilder
                .name("Lunch Process")
                .startEvent("lunchOps")
                .serviceTask("initializeGettingLunchList")
                .camundaClass(InitializeGettingLunchListDelegate::class.java)


                .subProcess()
                .embeddedSubProcess()
                .startEvent()

                // This just makes sure there is a 'placedOrder' in the process
                .serviceTask()
                .camundaClass(InstantiateOrderPlacedDelegate::class.java)

                //Assign them an order
                .userTask("placeOrders")
                .name("Place your order at: #{orderLink}")

                .camundaFormField()
                .camundaId("placedOrder")
                .camundaLabel("I placed my order!")
                .camundaType(UserFormType.BOOLEAN.type)
                .camundaDefaultValue("false")
                .camundaFormFieldDone()
                .camundaAssignee("#{lunchGetter.id}")


                .boundaryEvent("killUserTask")
                .timerWithDuration("PT1H")
                .cancelActivity(true)
                .moveToActivity<UserTaskBuilder>("placeOrders")

                .boundaryEvent()
                .timerWithCycle("R3/PT10M")
                .cancelActivity(false)
                .serviceTask("reminderIfNeeded")
                .camundaClass(OrderLunchReminderDelegate::class.java)
                .endEvent()
                .moveToActivity<UserTaskBuilder>("placeOrders")

                .serviceTask("timesUpOrOrderComplete")
                .name("timesUpOrOrderComplete")
                .implementation("expression")
                .camundaExpression("\${1 + 1}")

                .moveToNode("killUserTask").connectTo("timeUpOrOrderComplete")

                .endEvent()
                .subProcessDone()
                .multiInstance()
                .parallel()
                .camundaCollection("#{gettingLunch}")
                .camundaElementVariable("lunchGetter")
                .multiInstanceDone<SubProcessBuilder>()

                .endEvent()
    }
}

#11

That is still not executable. There is no way that compiles :wink:

You still have the error i listed above:

edit: you also have a typo in your “timesUpOrOrderComplete” task id and the .moveToNode(“killUserTask”).connectTo(“timesUpOrOrderComplete”) line. You had different spelling.


#12

@dan looking at the compiled code and bpmn file i posted above, it looks like your issue is likely with the collection you are injecting into the sub-process. Can you try with a single user being injected into the sub process? And just to confirm, you are only running a single engine? (not clustering or anything like that)


#13

Thanks for the fixes, I missed that typo! (timesUpOrOrderComplete)


RE: “That is still not executable.”

I wonder what the differences are, I definitely have some additional support code, because sigh and I can’t believe I’m saying this, “it works(compiles) on my machine” :tired_face:

What are you using to assess the validity of this business process? Because I am running, line-for-line what was posted. I build this process and run it using the Spring Boot Starter skeleton. I’m not 100% sure if that changes the deployment to be multi-engine, I wouldn’t think so.

The list is a collection of users, I use it elsewhere in the codebase and am sure that it has a finite number of elements in it, but I’ll whittle it down to one and see what happens.


RE: “many more times”

Setup:
I have debug output in the OrderLunchReminderDelegate class to print out the current user.

Expected: I have 10 users in that collection, for a repeating reminder “R3/PT10M” I’d expect a total of 30 executions of that code. I should see the following three times: All 10 users in that list should be output.

Actual:
I’m seeing upwards of 500+ prints of my current user.

I see my first three users repeated and typically when the killUserTask hits then I see the rest of my users print out once. ex; 1 2 3 - 1 2 3 - 1 2 3 -…- 1 2 3 4 5 6 7 8 9 10


#14

Can you export the bpmn xml text/string that is generated by your fluent API code. And share that bpmn file.


#15

That might be prudent at this point in time. :grinning:

lunch.bpmn20.xml (13.4 KB)


#16

OKay!

So see the problem.

I was able to recreate your issue with the several hundred instances of “reminder” being created on my first deployment.

There are a few problems going on here:

  1. The default job wait period of 1 min. see: Timer Execution too slow. Basically in your “test” you have your timers set for 30S and 10S which are too small for the default setup. But it does “feel” like a bug… where the job executor is racing with it self and creating hundreds of reminders. It feels like a uncaught exception/outlier condition @thorben;

  2. If you want short timers like this, look to modify the job executor settings

  3. The other problem is if you increase the killTask timer to say a 5min, but you set the reminder timer to 30sec, it is still possible the second or third repeats will be missed. I was able to recreate this in a quick little test. The way that the Timers / Business Calendars work is: A Timer is created with a due date whcih is stored as a Job. When that Job is executed, if it was a recurring date/timer it re-evaluates the timer logic and determines the next date. What is happening in this scenario of a short repeat is 1 or more cycles are being missed because by the time the job is executed, previously periods have already passed. @thorben might be able to explain or clarify this further.

So long story short: For your test if you increase your Kill Task timer to say 30min, and set your reminder to say 5 mins, you should have no issues.
You can also decrease the max wait period for the job executor (tell the job executor to look for new jobs on a more frequent basis) with the link above.


#17

Ya seems like a uncaught scenario of job executor racing with it self:

Run 1:

Run 2:

The instances dont match the “expected” result because of the bad job executor config, but that sort of makes is “Execpted”.

The real issue here is that the issue seems to happen only on the first run of the process def after a deployment. On a second, third, fourth run, it does not occur.

@thorben

Simple BPMN for testing
lunch.bpmn20.bpmn (10.7 KB)


#18

I had those set short for testing, I was shooting myself in the foot! :scream:

I can confirm that using 5M/1M timers works as expected. Wow, this is super helpful and informative. Thank you so much for sticking with me through this issue!


#19

@thorben

Some more examples for showing the issue

Both are “first run” after a deployment (through rest api)

Exact same bpmn in all cases, no changes


#20

@dan really easy to fix your test if you are using the spring boot. Just modify the job executor settings in the application.yaml file