How to handle or avoid an optimistic locking exception

I have an interesting scenario using 5.21 (moving to 5.22 and perhaps Flowable 6 soon) where I’m getting an ActivitiOptimisticLockingException. I’d like to figure out the best way to handle it or avoid it entirely.

I have a process that has a user task that on completion has Java service task that calls an API. The process then continues on to an end step. Pretty simple.

However, as a result of the API call, several other instances of the same process may be invalidated and I want to remove them. This gets triggered by the server the API call is made to sending out a JMS message, and the processing of the message looking up the related process instances by a specific variable/value. Presently, the process instance being completed shows up as a related process too (it has the same variable/value as the others).

This message and processing is all asynchronous to the process instance completion and happens quickly enough that the message processing tries to delete the process instance that the user completed the task for before the engine has fully finished the completion, and I get an ActivitiOptimisticLockingException, usually on the thread completing the task. Specifically:

org.activiti.engine.ActivitiOptimisticLockingException: HistoricVariableInstanceEntity[id=2647, name=euid2, revision=0, type=string, textValue=1000002002] was updated by another transaction concurrently
at org.activiti.engine.impl.db.DbSqlSession.flushUpdates(DbSqlSession.java:880)
at org.activiti.engine.impl.db.DbSqlSession.flush(DbSqlSession.java:619)
at org.activiti.engine.impl.interceptor.CommandContext.flushSessions(CommandContext.java:212)
at org.activiti.engine.impl.interceptor.CommandContext.close(CommandContext.java:138)
at org.activiti.engine.impl.interceptor.CommandContextInterceptor.execute(CommandContextInterceptor.java:66)
at org.activiti.engine.impl.interceptor.JtaTransactionInterceptor.execute(JtaTransactionInterceptor.java:65)
at org.activiti.engine.impl.interceptor.LogInterceptor.execute(LogInterceptor.java:31)
at org.activiti.engine.impl.cfg.CommandExecutorImpl.execute(CommandExecutorImpl.java:40)
at org.activiti.engine.impl.cfg.CommandExecutorImpl.execute(CommandExecutorImpl.java:35)
at org.activiti.engine.impl.FormServiceImpl.submitTaskFormData(FormServiceImpl.java:70)

Now, this occurs late enough in the task completion that the task itself is actually completed, so I suspect it is just historical variables that may not be fully populated correctly, but I’d still like to avoid it happening.

So, is there any way to avoid a simultaneous process instance deletion from affecting the processing of the same process instance being completed? Does 5.22 have anything to help with this? Will version 6 change this behavior at all? Or do I need to build something into my process to detect and avoid this? Or are there any engine settings that would alter this behavior?

One thought I had was in the message processing doing the delete, to look for any tasks for the process instance and if there are none, that would indicate the process instance is the one we want to let complete. The others we want to delete would still be at the user task.

Or do I need to set a process variable before the API call in the process that identifies that process instance as the one to not delete and use it when looking up the related process instances?

Thanks for any insight or suggestions.

Hi,

When there are multiple threads working with the same process instance, there is the chance of optimistic lock exceptions, but this means that the first one “wins”, and the second transaction is just rolled back. So in this case you get an exception, but there’s probably no need for retry.

If you would like to prevent the optimistic lock exception from happening you could have a look at making the async continuations exclusive and see if that helps in your case.

A process diagram of the process definition you are getting these errors on would help to get more insight in what you are trying to do and what possible solutions are available.

Best regards,

Tijs

Thanks for the reply.

Can you elaborate on making the continuations exclusive? What is that or how would I do that?

And yes, I see the behavior you are describing that one or the other “wins”. If I could reliably have the delete be the one that fails, that would actually be ok.

When doing the delete, if there was a way for the engine to tell me the instance was “in process” that would be useful too, but I don’t see a way to do that. I actually tried having the delete lookup the tasks for the instance first to see if there were any, hoping that there being none would be an indication that the instance was “in process”, but it appears at least in some cases the task being completed has not been committed yet so that isn’t a reliable indication.

I could also design into my process setting a variable on the instance being completed that could be used as a filter, but I’m afraid that would suffer from the same issue, it may not be committed yet so the delete thread would not see it to use as a filter.

My process is pretty simple for the path taken. There is a user task where a decision is made to perform a merge or not. If the user elects to do a merge, the path taken has a Java service task that calls an API to perform the merge and then goes to an end event. So there isn’t a lot of time between the API being called (which triggers the notification which when received results in the delete logic) and the engine being finished completing everything required for the process instance, but sometimes there is enough.

Sometimes the failure is on the delete with an exception like this:

org.activiti.engine.ActivitiOptimisticLockingException: VariableInstanceEntity[id=22625, name=_summary, type=string, textValue=EUID 1000103004: Kevin, Schmidt, 09/2…] was updated by another transaction concurrently
at org.activiti.engine.impl.db.DbSqlSession$CheckedDeleteOperation.execute(DbSqlSession.java:295)
at org.activiti.engine.impl.db.DbSqlSession.flushRegularDeletes(DbSqlSession.java:897)
at org.activiti.engine.impl.db.DbSqlSession.flushDeletes(DbSqlSession.java:890)
at org.activiti.engine.impl.db.DbSqlSession.flush(DbSqlSession.java:617)
at org.activiti.engine.impl.interceptor.CommandContext.flushSessions(CommandContext.java:212)
at org.activiti.engine.impl.interceptor.CommandContext.close(CommandContext.java:138)
at org.activiti.engine.impl.interceptor.CommandContextInterceptor.execute(CommandContextInterceptor.java:66)
at org.activiti.engine.impl.interceptor.JtaTransactionInterceptor.execute(JtaTransactionInterceptor.java:65)
at org.activiti.engine.impl.interceptor.LogInterceptor.execute(LogInterceptor.java:31)
at org.activiti.engine.impl.cfg.CommandExecutorImpl.execute(CommandExecutorImpl.java:40)
at org.activiti.engine.impl.cfg.CommandExecutorImpl.execute(CommandExecutorImpl.java:35)
at org.activiti.engine.impl.RuntimeServiceImpl.deleteProcessInstance(RuntimeServiceImpl.java:122)

Other times the failure is back to the client that completed the task, the exception in the log being this:

org.activiti.engine.ActivitiOptimisticLockingException: HistoricVariableInstanceEntity[id=2647, name=euid2, revision=0, type=string, textValue=1000002002] was updated by another transaction concurrently
at org.activiti.engine.impl.db.DbSqlSession.flushUpdates(DbSqlSession.java:880)
at org.activiti.engine.impl.db.DbSqlSession.flush(DbSqlSession.java:619)
at org.activiti.engine.impl.interceptor.CommandContext.flushSessions(CommandContext.java:212)
at org.activiti.engine.impl.interceptor.CommandContext.close(CommandContext.java:138)
at org.activiti.engine.impl.interceptor.CommandContextInterceptor.execute(CommandContextInterceptor.java:66)
at org.activiti.engine.impl.interceptor.JtaTransactionInterceptor.execute(JtaTransactionInterceptor.java:65)
at org.activiti.engine.impl.interceptor.LogInterceptor.execute(LogInterceptor.java:31)
at org.activiti.engine.impl.cfg.CommandExecutorImpl.execute(CommandExecutorImpl.java:40)
at org.activiti.engine.impl.cfg.CommandExecutorImpl.execute(CommandExecutorImpl.java:35)
at org.activiti.engine.impl.FormServiceImpl.submitTaskFormData(FormServiceImpl.java:70)

Hi,

I still don’t understand the process definition completely. Could you share a process diagram of the process definition?

From what I understand from the description, making the async continuations exclusive won’t make a difference, and they are exclusive by default, so probably this is already the case.

For these kind of issues to think of a solution it’s really important to understand the whole process definition flow, and a process diagram helps a lot with that.

Best regards,

Tijs

Here is the relevant part of the diagram.

A user is making a decision in the user task, if they elect to merge two records together the path shown is taken. The API that is called from the Java service task is what triggers the notification that in turn when processed is making the call to delete the process instance.

You are right that making the Java service task exclusive is the default and so that doesn’t change anything. What I did try that seemed to change behavior was making the Java service task async. By doing this, the user task transaction is completed right away and the API call from the Java service task is done in a separate transaction. This means when the code doing the delete is executed, it can check to see if the process instances it finds have a user task (the one we don’t want to delete and let finish won’t) and use that as the filter. I could also add setting a process variable to the process definition before the Java service task to use to filter on too.

So that is the approach I’m taking for now.