BizTalk orchestrations can be designed to run discrete pieces of work, following the classic 'ACID' concept of a transaction. These discrete or atomic units of work, when performed, move the business process from one consistent state to a new, consistent and durable state, and that are isolated from other units of work. This is typically done by using the Scope construct that encapsulates the units of work with the transactional semantics. The entire orchestration can also be defined as an atomic transaction without the use of scopes. The scopes, however, cannot be marked as transactional unless the orchestration itself is marked as a long running or atomic transaction type.
Atomic transactions behave like classic MSDTC transactions, but they are not explicit DTC transactions by default. The following isolation levels are supported by the atomic transactions:
-
Read Committed
Shared locks are held while the data is being read to avoid dirty reads, but the data can be changed before the end of the transaction, resulting in non-repeatable reads or phantom data.
-
Repeatable Read
Locks are placed on all data that is used in a query, preventing other users from updating the data. This prevents non-repeatable reads, but phantom rows are still possible.
-
Serializable
A range lock is placed preventing other users from updating or inserting rows into the database until the transaction is complete.
When the atomic transaction fails, all states are reset as if the orchestration instance never entered the scope. The rule BizTalk has for atomic transactions is that all variables (not just local to the scope) participate in the transaction. All non-serializable variables and messages used in an atomic transaction should be declared local to the scope; otherwise, the compiler will give the "variable….is not marked as serializable" error. All atomic scopes are assumed to be "synchronized" and the orchestration compiler will provide a warning for redundant usage, if indeed the synchronized keyword is used for atomic scopes. The synchronization for the shared data extends from the beginning of the scope until the successful completion of the scope (including the state persistence at the end of the scope) or to the completion of the exception handler (in case of errors). The synchronization domain does not extend to the compensation handler.
Atomic transactions can be associated with timeout values at which point the orchestration will stop the transaction and the instance will be suspended. If an atomic transaction contains a Receive shape, a Send shape, or a Start Orchestration shape, the corresponding actions will not take place until the transaction has committed.
An atomic transaction retries when the user deliberately throws a RetryTransactionException or if a PersistenceException is raised at the time that the atomic transaction tries to commit. The latter could happen if for instance, the atomic transaction was part of a distributed DTC transaction and some other participant in that transaction stopped the transaction. Likewise, if there were database connectivity problems at the time when the transaction was trying to commit, there would also be a PersistenceException raised. If this happens for an atomic scope that has Retry=True, then it will retry up to 21 times. The delay between each retry is two seconds by default (but that is modifiable). After 21 retries are exhausted, if the transaction is still unable to commit, the whole orchestration instance is suspended. It can be manually resumed and it will start over again, with a fresh counter, from the beginning of the offending atomic scope.
Note |
|---|
|
Only the RetryTransactionException and PersistenceException can cause an atomic scope to retry. Any other exception raised in an atomic scope cannot cause it to retry, even if the Retry property is set to True. The two-second default delay between each two consecutive retries can be overridden by using a public property on RetryTransactionException (if that is the exception that is being used to cause the retries). |
The atomic scopes have restrictions on nesting other transactions that cannot nest other transactions or include Catch - Exception blocks. The following scenarios describe the use of atomic transactions.
Scenario 1 - An Atomic Transaction with COM+ ServicedComponent
The following orchestration shows how to use the RetryTransactionException with atomic transactions. Although exception handlers cannot be directly included for an atomic scope, the scope can include a non-transactional scope that can have an exception handler. The ServicedComponent enlists in the same DTC transaction and any exception raised by the component is caught and re-thrown as RetryTransactionException. (This assumes that the Retry property is set to True for the atomic scope).
Note that the orchestration would have been suspended and the action in the MessageAssignment shape would have been rolled back even if the RetryTransactionException is not thrown. This pattern, however, allows building resilience in the application where the retries occur automatically.
Figure 5 An atomic transaction with COM+ ServicedComponent
Scenario 2 - Using Transacted Adapters with Atomic Transactions
The following orchestration shows how to use the atomic transactions with the SQL adapter. The whole orchestration is marked as long running with individual atomic transactions for the two logical pieces of work, Inserting a new Customer and Insert Order details for the customer. If for whatever reason, the Order Insert fails, the Customer Insert should be rolled back. The sample uses the SQL adapter to do the database work. As mentioned earlier, the scope associated with an atomic transaction completes when the message is sent to the MessageBox database. This implies that after the engine is successful in sending the message in the Scope_InsertCustomer and Scope_InsertOrder scopes, each one of the scopes commits. The SQL adapter creates a new transaction for the actual Insert of the Customer or the order. The Ports have a property “Delivery Notification” for validating that the message has been successfully sent via the Sent Port. When the Delivery Notification property is set to “Transmitted”, a receive subscription is placed before the Transactional commit point of the Send Operation. However in case of Atomic Scopes, the receive subscription is placed in the enclosing Parent scope. In the scenario where the InsertOrder SQL transaction fails, a "Nack" will be sent back and the "Scope_InsertOrder" commits. After the Sent Port exhausts the configured retries, a DeliveryFailureException will be raised This exception will be caught by the default exception handler, which will run the default compensation process. This will invoke the compensation handler associated with the Scope_InsertCustomer, causing the undo operation of inserting the customer information.
Note |
|---|
|
Nesting the two scopes in a long running scope, and invoking the Compensate shape (targeting the long running transaction) from the exception handler for the long running scope will result in the same behavior as described above. The whole orchestration could not be marked atomic as atomic transactions do not allow nested transactions. |
Figure 6 Transacted adapters with atomic transactions