Advanced Planning
This section covers some advanced planning topics.
Failover
The built in failover feature of Paraflow provides a way to handle a failed goal with a failover goal which is only executed if the primary goal fails. Failover is specified in the rule body with the following syntax:
<goal-failover> := <simple-goal-instance> "else" <simple-goal-instance>
<simple-goal-instance> := <goal> <call-bindings>
For example, if the !AttemptSomething goal in the following were to fail then the !Recover goal will be executed. Otherwise, if !AttemptSomething does not fail then the !Recover goal is skipped.
rule !Task($id) plan {
!AttemptSomething($id) else !Recover($id);
}
Rollback
Often when an goal is broken down into a number of sub-goals, the whole collection needs to be treated as a transaction. That is, if at some point one the top-level goal fails due to a sub-goal failure, there may be other sub-goals that have already completed and had side-effects on the state or in the environment that should be undone. This is accomplished by implementing a compensating action for each side-effect which reverses the side-effect. This scenario is sometimes called the Saga design pattern.
There are two steps to implementing this type of rollback in Paraflow:
- Each task that has side-effects that need to be reversed should have a
rollbackclause which implements the reverse operation. - The rule that represents the transaction scope needs to include the
auto-rollbackflag.
The transaction scope determines which goals are part of the transaction. All the sub-goals of the rule with the auto-rollback flag, and recursively those sub-goals' sub-goals are in the transaction scope. When a goal in the transaction scope fails,
and as a result, the scope's top-level goal would fail the a rollback in initiated. All uncompleted goals in scope are canceled. All previously completed goals in scope that were executed by a task with a rollback clause are then rolled back in reverse order. On the other hand, if the scope's top-level goal reaches a completed state then it is committed. Any other goal failures (i.e. outside the transaction scope) have no effect.
Here is an example of a task with rollback:
task !UpdateBalance($acct_id, $amount) {
update user_account(id == $acct_id, balance: incrBy($amount));
} rollback {
update user_account(id == $acct_id, balance: decrBy($amount));
}
If this task fails, the rollback clause is always executed. If this task succeeds, the rollback clause would only be executed according to the rules described above for transaction scopes.
This is an example of transaction scope:
rule !HandleVenmoSendMoney($acct_id, $venmo_id, $amount) plan {
!SendMoney($acct_id, $venmo_id, $amount);
!NotifyUser($acct_id, message -> "Money sent");
}
rule !SendMoney($acct_id, $venmo_id, $amount) auto-rollback plan {
!UpdateBalance($acct_id, amount -> -$amount);
!VenmoSendRequest($venmo_id, $amount);
}
In this example of sending money, the user's balance is first decreased and then the attempt to send is made. If the send fails, the rollback code of !UpdateBalance is executed because it is in the scope of the !SendMoney rule which has the auto-rollback flag. However, if the !SendMoney completes and the !NotifyUser fails, the rollback code is not executed because the !NotifyUser is outside the scope of the transaction.
Rollback and Catch
If a task has both a catch clause and a rollback clause then the catch has priority. If the catch handles the error then the rollback code will not be executed.