...
In multi-user applications, different users can modify the same data simultaneously. To deal with reads and updates of the same data sets happening in parallel, transaction subsystems of products such as Ignite implement optimistic and pessimistic locking. In the pessimistic mode, an application will acquire locks for all the data it plans to change and will apply the changes after all the locks are owned exclusively while in the optimistic mode lock acquisition is postponed to a later phase when a transaction is being committed.
Lock acquisition time also depends on the type of isolation level. Let's start with the review of isolation levels in conjunction with the pessimistic mode.
In pessimistic and read committed mode, locks are acquired before the changes brought by write operations such (as put or putAll) are applied, as shown in Figure 6.
The pessimistic mode holds locks until the transaction is finished and prevents access to locked data from other transactions. Optimistic transactions in Ignite might increase the throughput of an application by lowering contention among transactions by moving lock acquisition to a later phase.
In optimistic transactions, locks are acquired on primary nodes during the "prepare" phase, then promoted to backup nodes and released once the transaction is committed. Depending on an isolation level, if Ignite detects that a version of an entry has been changed since the time it was requested by a transaction, then the transaction will fail at the "prepare" phase and it will be up to an application to decide whether to restart the transaction or not. This is exactly how optimistic and serializable transactions (also known as deadlock-free transactions) work in Ignite, as shown in Figure 8.
On the other hand, repeatable read and read committed optimistic transactions never check if a version of an entry is changed. This mode (Figure 9) might bring extra performance benefits but does not give any atomicity guarantees and, thus, is rarely used in practice.
Now let's review the entire lifecycle of a transaction in Ignite. We will assume that the cluster is stable and no outages happen.
A unique transaction identifier is generated.
The start time of the transaction is recorded.
Current topology version/state is recorded.
Next, the transaction status is set to "active" and Ignite starts executing read/write operations that are a part of the transaction using the rules of either optimistic or pessimistic mode and specific isolation levels.
When the application executes the tx.commit() method (Step 9 in Figure 10) the near node (transaction coordinator) initiates the 2-phase commit protocol by preparing the "prepare" message enclosing information about the transaction's context into it.
As a part of the "prepare" phase, every primary node receives information about all updated or new key-value pairs and about the order the locks have to be acquired (the latter depends on a combination of locking modes and isolation levels).
The primary nodes in their turn perform the following in response to the "prepare" message:
Check that a version of the cluster topology recorded in the transaction's context matches the current topology version.
Obtain all the required locks.
Create a DHT context for the transaction and store all the necessary data therein.
Depending upon the cache configuration parameters, wait or skip waiting while backup nodes confirm that the "phase" phase is over.
Inform the near node that it's time to execute the "commit" phase.
After that, the near node sends the "commit" message, waits for an acknowledgment and moves the transaction status to "committed".
If the transaction was rolled back (tx.rollback() is called by the application), then, in the pessimistic mode, Ignite would be required to release all the acquired locks and delete the transaction's context. In the optimistic mode the locks are acquired when tx.commit() is called by the application, therefore, Ignite would simply clean out the transaction's context on the transaction coordinator (near node).
Ignite allows setting a timeout for a transaction. If transaction's execution time exceeds the timeout, then the transaction will be aborted.
In the pessimistic mode, the timeout is compared to the current total execution time every time an entry lock is acquired and when the "prepare" phase is triggered. In the optimistic mode, the timeout is compared only in the "prepare" phase. Figure 11 shows a summary.
Figure 11.
If the total execution time has exceeded the timeout on any of the participating nodes, then a primary node, where the timeout elapsed, sets a special flag instructing the transaction coordinator to initiate a transaction cancellation.
The section explains how Ignite tackles failover situations or outages that might happen while transactions are being executed.
Figure 12.
The simplest failure scenario is when a backup node fails on either the "prepare" or the "commit" phase. Nothing has to be done by the Ignite transactional subsystem. Transaction modifications will be applied to the remaining primary and backup nodes (Figure 12) and a new backup node for missing partitions will be elected after the transaction is over and that will not preload all the up-to-date data from a respective primary node.
Figure 13.
If a primary node failed before or on the "Prepare" phase, then the transaction coordinator raises an exception (Figure 13) and it's up to the application to decide what to do next - restart the transaction or process this exception differently.
If a primary node failed after the "prepare" phase, then the transaction coordinator will be waiting for an extra NodeFailureDetection response from respective backup nodes (Figure 14).
Figure 14.
Once the backup nodes detect the failure they will send a message to the transaction coordinator confirming that they successfully committed the changes and no data loss happened because there is still an extra backup copy available for application usage.
Next, the transaction coordinator finishes the transaction, the topology is changed (due to the primary node loss) and the cluster will elect a new primary for the partitions that were stored on the previous one.
Handling of transaction coordinator failures is a bit trickier because every remote node (primary and backup) is aware of the transaction's context related to it and doesn't know the overall transaction state. It is also possible that some of the nodes have already received "commit" message while the others haven't, as shown in Figure 15.
To solve this situation, the primary nodes exchange internal status with each other to find out the overall transaction state. For instance, if one of the nodes responds that it hasn't received a "commit" message, then the transaction will be rolled back globally.