Idempotent Start Process Patterns

Hi,

heres two process patterns Ive used to provide pseudo idempotent process start semantics. I say pseudo as these patterns don’t actually prevent starting a duplicate process instance, however they can enable detection of a duplicate process and thus early evasive action.

So whats the problem?
When a remote client uses the REST API to start a process instance a transient failure could occur. Hence the client cannot tell if the error resulted in a lost request or a lost response. The easiest way for the client to recover is to resubmit the request until it receives positive acknowledgement of receipt of the request. However this can be a problem if the process is not idempotent - consider a process which results in a debit or credit to a bank account.

Why not just enforce uniqueness on the business key?
The engine can be configured to enforce uniqueness of the business key. This is a feasible and possibly the simplest solution, however there are some characteristics to be aware of. This solution can be brittle. Consider the case when a client may actually recycle business keys and thus can only ensure uniqueness over a moving window. Also consider the case where to recover from an error, its easiest to reprocess the request. Hence enforcing uniqueness on the business key in these circumstances can be problematic.

Pattern 1
This pattern uses a history query to identify any prior instances with the same business key. This pattern relies on breaking the process into an initiation and a continuation phase. Thus there must be an asynchronous continuation to force a database flush to the history tables. Hence in the continuation phase, the process performs a history query to retrieve the earliest process instance with the current business key. If the historic instance ID is the same as the current execution, then continue. Otherwise the current execution is a duplicate. A sample process is shown below;

A groovy implementation of the script to perform the idempotency logic is shown below;
import org.camunda.bpm.engine.delegate.BpmnError;

//
// Perform an idempotency check based on businessKey - if we have seen the business key in the last period (1 day), throw BpmnError.
// Query for prior instances and order by start date. If the first one is this instance, then continue. Otherwise we have a duplicate...
//

def instanceList = execution.getProcessEngineServices().getHistoryService().createHistoricProcessInstanceQuery().processDefinitionKey(execution.getProcessDefinitionId().split(':')[0]).startedAfter((new Date()) - 1).processInstanceBusinessKey(execution.getProcessBusinessKey()).orderByProcessInstanceStartTime().asc().list()

if (instanceList.size() > 0)
{
	if (execution.getProcessInstanceId() != instanceList.get(0).getProcessInstanceId())
    		throw new BpmnError("IDEMPOTENCY_ERROR","A request with a duplicate business key (" + execution.getProcessBusinessKey() + ") within the last day has been received!")
}

Hence this code detects duplicate business keys within a time window (1 day) and thus can be less brittle than the first approach. Also note that this does not prevent a duplicate, it just enables detection of a duplicate and thus the process can take evasive action. However this can lead to a down side. The client may not know the process instance ID of the actual process instance which completes the process.

Pattern 2
This pattern uses a threeway handshake. Hence the client initiates and the engine responds with the process instance ID. The client then acknowledges the process instance ID back to the engine. This pattern is shown below;

Note the Camunda message correlate API has an option to deliver a message to all correlated process instances. This pattern relies on this behaviour. This pattern can be useful if your client needs to know the process instance ID which handles the process request. Once again this pattern does not prevent duplicates, however it enables the process to take evasive action.

Feedback and comments most welcome,

regards

Rob

4 Likes