Compensation

Sometimes the unexpected happens: for example, a customer changes his mind and wants to cancel an order or the stock are running low. Actually, in real world: the change is the only constant!

Workflow developers start defining the happy path, then they have to deal with unexpected results (out of stock, order cancellation events, etc). A workflow could define a long chain of activities and under certain conditions, it might be necessary to define an exit strategy: warning all parties involved that they must undo what has been done before.

When you have short running transactions (during milliseconds), you could aspire to the convenient atomic transaction, but as long as your tasks span over multiple systems this option brings so many drawbacks which make it unrealistic, especially when they run in a cloud context. In fact, in the microservices architecture, the Saga Pattern is a well know practice when dealing with a cancellation.

When you deal with long running transactions, as known as business transactions, that spans of over minutes, hours or days, the only option available is to revert what was done previously, this could have consequences: for example, Hotels usually applies a cancellation fee when the cancellation is too near to the reservation day.

A workflow might involve multiple logical steps, there are conditional paths that depends on specific instance data. When the business logic requires a cancellation of the workflow instance, you need to reverse the flow of actions, undoing what was previously done. In the workflow nomenclature, this capability is called Compensation.

The good news is that the Serverless Workflow engine helps you in this difficult situations! In fact, the workflow developer can define for each action a compensation action that will only be invoked if necessary. Behind the scenes, the workflow engine, when an action is completed, records the corresponding compensation action in a stack. If the workflow subsequently encounters conditions that require compensation, only then does the engine retrace the stack of compensation activities to close everything correctly.

In short, Serverless Workflow Compensation is very handy whenever dealing with cancellation!

Cancelling Event Sprint

In our use case, we have to deal with situations where customers have to cancel their orders. Specifically, if the cancellation event arrives before the shipping one, it is necessary to call for compensation: the concrete compensation action depends on the previous execution path, if the item was available, the compensation action will call the internal shipping department otherwise the compensation action will deal with the external supplier.

The following picture shows an high level design:

high level flow 3

Given the simplistic nature of the example, one might consider replacing compensation with a gateway followed by compensatory actions. However, it should be borne in mind that in an even slightly more complex flow, implementing compensation in imperative mode would make the implementation complex and error prone.

Below, we will add the definition of the new event type CancelEventType and a forking that distinguishes between the two possible input events:

  • shippingEvent

  • cancelEvent

Open the swf file:

  1. Inside the event section add the following declaration:

    {
      "name": "cancelEvent",
      "kind": "consumed",
      "type": "CancelEventType",
      "source": "Client",
      "correlation": [
        {
          "contextAttributeName": "orderid"
        }
      ]
    }
  2. In the state section, locate the Order Shipped event node and overwrite it with the following snippet:

    {
      "name": "Order Shipped or Cancelled",
      "type": "event",
      "transition": "Is Shipped?",
      "exclusive": true,
      "onEvents": [
        {
          "eventRefs": [
            "shippingEvent"
          ]
        },
        {
          "eventRefs": [
            "cancelEvent"
          ],
          "eventDataFilter": {
            "data": "{cancel:true}"
          }
        }
      ]
    },

    The new event state definition is able to listen for two different events. At the cancellation event, we can see the statement eventDataFilter the effect of which is to introduce a new Boolean data: cancel:true. In such a way, we will later be able to know which event has arrived.

  3. Update the transition the states Prepare for Shipping and Forward to External Supplier to point to the new name Order Shipped or Cancelled

  4. Add the following switch state after the Order Shipped or Cancelled state:

    {
        "name": "Is Shipped?",
        "type": "switch",
        "dataConditions": [
        {
            "name": "order cancelled",
            "condition": ".cancel == true",
            "transition": "Compensate Order"
        }
        ],
        "defaultCondition": {
            "transition": "Notify Customer"
        }
    },
  5. After the previous state, Add an operation to handle the cancel event:

    {
        "name": "Compensate Order",
        "type": "operation",
        "actions": [
        {
            "name": "printAction",
            "functionRef": {
            "refName": "printMessage",
            "arguments": {
                "message": "\"Compensate Order\""
            }
            }
        }
        ],
        "end": {
            "terminate": true,
            "compensate": true
        }
    },
  6. Locate the Notify Customer operation state and adjust the end property in this way:

    "end": {
        "terminate": true
    }

    This measure forces the workflow termination.

The following picture shows the result of the above procedure on the workflow diagram:

cancel event

You can compare your resulting workflow with the expected solution at this stage in solution/04-cancelling

Test the cancelling event

In this section, we are going to test that the workflow is able to resume the execution when two types of CloudEvents arrives:

  • ShippingEventType

  • CancelEventType

Then, depending on the incoming message, the workflow continues in one of the two corresponding branches.

Even if you trigger the compensation no compensation activity is defined, so at this stage of the exercise you can only look for the Compensate Order string in the logs.

Based on the previous test activities, you should be able to figure out how to test this scenario. Please, note that in the probe.http file, you already have the cancelEvent rest request.

Compensation Activities Sprint

In this section, you will add two compensation activities for Prepare for Shipping and Forward to External Supplier.

Afterward, you will check that the runtime will call them when compensation is triggered by the cancel event.

Compensation activities are operation states, they could be placed anywhere in the state array. However, we suggest to place the definition nearby the corresponding primary state so it’s easier to orientate.
  1. Locate Prepare for Shipping and append right afterward:

    {
        "name": "Restore Inventory",
        "type": "operation",
        "usedForCompensation": true,
        "actions": [
        {
            "name": "printAction",
            "functionRef": {
            "refName": "printMessage",
            "arguments": {
                "message": "\"Restore Inventory\""
            }
            }
        }
        ]
    },
  2. Inside Prepare for Shipping add the following property:

    "compensatedBy": "Restore Inventory",

    In the workflow diagram you should see the two states linked by a yellow dotted line:

    restore inventory
  3. Locate Forward to External Supplier and append right afterward:

    {
        "name": "Cancel Supplier Order",
        "type": "operation",
        "usedForCompensation": true,
        "actions": [
        {
            "name": "printAction",
            "functionRef": {
            "refName": "printMessage",
            "arguments": {
                "message": "\"Cancel Supplier Order\""
            }
            }
        }
        ]
    },
  4. Inside Forward to External Supplier add the following property:

    "compensatedBy": "Cancel Supplier Order",

In the workflow diagram you should see the following situation:

compensation activities

At the time of writing, the diagram rendering has a bug that shows overlapping lines, so the final outcome is slightly confusing.

You can compare your resulting workflow with the expected solution at this stage in solution/05-compensation

Testing the compensation

To test the compensation you have to send the following events:

  1. New order (OrderEventType)

  2. Cancel the order (CancelEventType)

Then you can check if the compensation activity is triggered.

Unless, you changed it before, the rest call provided in probe.http contains the item id 1110 which is supposed to trigger the internal shipping path. So when you trigger the cancellation you should see in the log the following line:

Restore Inventory

Repeat the test using an item id with no 0 (e.g. 1234), upon cancellation you should see in the log the following line:

Cancel Supplier Order

CONGRATULATION!!! Your Serverless Workflow compensates wisely!