The Actor Model is based on the premise of small independent processes working in isolation and where the state can be updated only via message passing. The actors hold the state within themselves, but the asynchronous message passing means there is no guarantee that a stable view of the state can be provided to the calling components. For transactional systems like banking where account deposit and withdrawal need to be atomic, this is a bad fit with an Actor Model.So, if your Akka applications need to be implementing a shared state model and providing a consensual, stable view of the state across the calling components, Software Transactional Memory (STM) provides the answer.
STM provides a concurrency-control mechanism for managing access to shared memory. STM makes use of two concepts – optimism and transactions to manage the shared concurrency control.
- Optimism means that we run multiple atomic blocks in parallel, assuming there will be no errors. When we are done, we check for any problems. If no problems are found, we update the state variables in the atomic block. If we find problems then we roll back and retry. Optimistic concurrency typically provides better scalability options than any other alternate approaches.
- Secondly, STM is modeled on similar lines of database transaction handling. In the case of STM, the Java heap is the transactional data set with begin/commit and rollback constructs. As the objects hold the state in memory, the transaction only implements the following characteristics – atomicity, consistency, and isolation.
To manage multiple transactions running on separate threads as a single atomic block, the concept of CommitBarrier is used.
CommitBarrier is a synchronization aid that is used as a single, common barrier point by all the transactions across multiple threads. Once the barrier is reached, all the transactions commit automatically. It is based on the Java’s CountDownLatch.
Akka transactors are based on CommitBarrier, where the atomic blocks of each actor (member) participating are treated as one, big single unit. Each actor will block until everyone participating in the transaction has completed. This means that all the actions executed as part of the atomic blocks by members of the CommitBarrier will appear to occur as a single atomic action even though the members may be spread across multiple threads. If any of the atomic blocks throw an exception or a conflict happens, all the CommitBarrier members will roll back.
Akka provides a construct for coordinating transactions across actors called coordinated.coordinated, which is used to define the transaction boundary in terms of where the transaction starts, and the coordinated.coordinate() method is used to add all the members that will participate in the same transaction context.
Money transfer between two accounts
Let’s take an example and see how the actors can participate in the transactions. We will use the classic example of transfer of funds between two bank accounts . We have an AccountActor that holds the account balance and account number information. It has two operations – credit (add money to the account) and debit (take money away from the account). In addition, we have the TransferActor object that will hold the two AccountActor objects and then invoke the debit and credit operations on the account objects.
To make sure that the money transfer in the account happens in a synchronized way, we need to implement the following:
- In the account object, the state variable that needs to participate in the transaction should be of transaction reference type.
- The credit and debit operations in the account object need to be atomic.
- In the transfer object, the transaction boundary needs to be defined and the account objects need to participate in the same transaction context.
- In addition, we define the supervisor policy in TransferActor and BankActor to handle the transaction exceptions:
Parts of the post are excerpts from the book – Akka Essentials. For more details on the example, please refer to the book.