Table of Contents

Persisting models

This guide helps you integrate DCR.Workflow into your host application. The guide relies on concepts introduced in the earlier guides, specifically the Working with models and Working with side-effects.

Your application chooses how to (a) import models from the DCR Repository or DCR Designer, and (b) how to persist these graphs. In either case, the on-the-wire format should be DCR XML.

For details on how to serialize/deserialize to/from DCR XML, including how to export from the DCR Designer and Repository, refer to the Working with models guide.

Concurrency

The DCR model encapsulates the current state of your workflow. As such, your persistence mechanism should handle conflicting updates. (If you are an experienced developer, you very likely already know and understand this. You may still find it helpful to know exactly how such conflicting updates manifests in the context of DCR models with side-effects. )

In the common case, like the above example, you serialize DCR models to XML, and store that XML in a database. In this setting, conflicting updates arise when two separate servers simultaneously retrieve a model, make mutually incompatible updates to it, and attempts to write their change back to the database. Without concurrency controls, the last write will overwrite the first one, and the first set of changes will be lost.

For a DCR model that does not have post-execution side effects, losing the first update may be acceptable behaviour. For a DCR model that does have post-execution side effects, losing the first update is not acceptable behaviour, since your model may now have lost the continuation after a post-execution side effect.

As a concrete example, consider this model, "M0":

 DCR model concurrency example

In this model, we imagine a sample effect DCR.Workflow.Effect.Example.Payment, which execute a bank transfer of the given amount between accounts specified in configuration. (We do not give that configuration here.) The activities labelled Payout failed and Payout succeeded are specified as the [continuations](~/articles/effects.md#special parameters) after executing the effect.

Note the response from Payout failed to Start payout: The model requires that the payout is re-tried until it succeeds.

Imagine this sequence of events

# DB Server 1 Server 2
0 M0
1 Load M0
2 Load M0
3 Execute Start Payout
4 Execute Payout succeeded
5 Write back new model M1
6 M1
7 Execute Unrelated audit
8 Write back new model M2
9 M2

Here is M1:

Note that Start payout and Payout succeeded are both executed. The caseworker will now from the model that the payout has succeded. Money has been transferred.

However, the end-state M2 in the database looks like this:

Note that this model has lost the information that Start payout was executed, and worse, that Payout succeeded happened. This workflow does not now that money was in fact transferred.

Prevent conflicting updates

You have two options to prevent the above situation.

  1. Locking models in the database (pessimistic concurrency control).
  2. Allow Runtime to read and write models from the database (optimistic concurrency control).

Locking (1) is simpler to implement, but allows failure modes you may find unacceptable. Allowing database access (2) requires more work on your part, but will for most models radically increase concurrency.

Locking

Simply take out a database lock on the model object in question before executing Runtime.Execute. Refer to your database systems documentation for details on how to obtain a lock.

Continuing the above example, your code should look like this:

    DCR.Workflow.Runtime middleware = ... // Somehow acquire Runtime.
    Model model = ...                        // Load "model" from persistent storage, obtaining a write-lock. 
    middleware.Execute(model, activity);     /* Update state of model by executing "activity". 
                                                "Execute" runs side-effects and executes
                                                additional activities as required by "model".
                                              */
    // Now write back "model" to persistent storage, relasing the write-lock. 

If you use locking, you may want to set a timeout on the lock. In this case, make sure to configure DCR.Runtime to have a timeout.

Optimistic concurrency control

You provide an implementation of IModelStore which allows the DCR middleware to both query and update a particular model in persistent storage on demand.

Your implementation must do optimistic concurrency control, that is DCR.Workflow.IModelStore.TryWriteAsync(DCR.Workflow.Model)) must detect an attempt to update a model that has a newer version in persistent storage.

Tip

If you are using EF Core, use a database-generated concurrency token.

Provide your to via dependency injection. Runtime.Execute will then persist the model as appropriate.

    IModelStore modelStore = ...             // Implement IModelStore.
    DCR.Workflow.Runtime middleware = ... // Somehow acquire Runtime, providing modelStore to the constructor or as a service.
    Model model = ...                        // Load "model" from persistent storage.
    model = middleware.Execute(model, activity);     
                                             /* Update state of model by executing "activity". 
                                                "Execute" runs side-effects and executes
                                                additional activities as required by "model".
                                                Because IModelStore was provided, persistent storage is 
                                                now updated.
                                              */
    // model may be a new object as this point; 
    // make sure the host application uses this new `model` throughout. 

Termination

The Runtime.Execute family of methods may execute more than one activity in the following circumstances:

  1. When an activity effect has a [continuation](effects.md#special parameters), the appropriate continuation is executed if enabled.
  2. When the model has [robot activities], these are executed if enabled and pending.

In both cases, executing the extra activity may have side-effects or make robots pending, causing yet more activities to execute. It is not difficult to accidentally make a model which in principle requires infinite execution of robots. Here is a canonical example.

To avoid accidentally taking production systems into infinite loops, the DCR.Runtime puts a finite bound on how many activities may execute as part of one call to Execute. If this bound is exceeded, no further robot events (2) are executed. However, continuations of effects (1) are still executed, to avoid a situation like the example above.

This bound can be set in one of two ways:

  • By setting the configuration key DCR.Runtime.MaxRobots (to a number, default 256). A call to Execute will then execute at most that number of additional Robot events.

  • By setting the configuration key DCR.Runtime.Timeout (to a number of seconds). A call to Execute will then stop executing additional robot events after that number of seconds has elapsed.

Use DCR.Runtime.MaxRobots to prevent bad models only. We are unaware of models requiring MaxRobots larger than 128, and most models would do fine with 4.

Use DCR.Runtime.Timeout if you wish to be reasonably certain that Execute completes within some timeframe, e.g., for setting timeouts on database locks. However, do note that if continuation activities remain at the point of timeout, they will still be executed, but any effects they might have will fail.

Caution

Setting DCR.Runtime.Timeout does not guarantee that the call will terminate in exactly the given amount of time. DCR.Workflow is not a soft realtime system and makes no guarantees about execution times.

Caution

Setting DCR.Runtime.Timeout when also using may cause one or more db write operations to happen after the timeout, in where at the timeout, one or more effect continuations were waiting to execute. In practice, choose a timeout value low enough that you also has time to wait for that additional db write.