Transactions

Transactions ensure that manipulations of data are either done correctly or not at all. The typical example is money transfer. When some person A pays person B, then A’s balance has to be reduced and B’s balance has to be increased. Without transactions, it would be possible that A’s balance were reduced while an error occurred increasing B’s balance, causing a loss of money. With transactions in place, the transaction becomes atomic. Either A’s balance is reduced and B’s balance is increased, or both A’s balance and B’s balance are unmodified in case of an error.

Transactions are supported by databases and by queues. In Frank configurations, it is possible to manipulate the transactions applied by an underlying database. In addition, it is possible to work with XA transactions, transactions that run over multiple databases and queues. That will be covered in Error Store and XA Transactions. This section focuses on transactions that apply to a single database.

Transactions are configured in a <Receiver> or in a <Pipeline>, in both of them using attribute transactionAttribute. Possible values of this attribute are Required, RequiresNew, Mandatory, NotSupported, Supports and Never. The exact meaning of these values can be found in the Frank!Doc. When the receiver of an <Adapter> has transactionAttribute="Required", then all data manipulations done by the receiver itself and by the pipeline are meant to happen in the same (database) transaction. When another adapter is called, the logic in that sub-adapter can happen in the same transaction depending on the transactionAttribute configured in the sub-adapter’s <Receiver> or <Pipeline>. When the transactionAttribute is set on the <Pipeline>, the data manipulations done by the receiver are excluded from the transaction.

Tutorial

The following exercise may help you to understand transactions:

  1. Start with the same example as used in the previous section, the example Frank application. Undo possible modifications you may have done or extract the zip file again.

  2. In the Frank!Console, stop adapter writeTableOtherMessage.

  3. Using your API client, send a HTTP request to the application - this time a plain-text body and header Message-Id for the message id.

  4. Using JDBC | Execute query, check that table Message has been updated but not table otherMessage. This is what we want to avoid with transactions.

  5. Stop the application and modify it to set transactionAttribute as shown below:

    <Configuration
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="../FrankConfig.xsd"
        >
        <Adapter name="writeDb">
            <Receiver checkForDuplicates="true" processResultCacheSize="0" transactionAttribute="Required">
                <ApiListener uriPattern="/write" method="POST" allowAllParams="false"/>
                <JdbcMessageLog slotId="write-db"/>
            </Receiver>
            <Pipeline>
                <SenderPipe name="writeTableMessage">
                    <FrankSender name="writeTableMessage" target="writeTableMessage" />
                </SenderPipe>
                <EchoPipe name="originalMessage" getInputFromSessionKey="originalMessage" />
                <SenderPipe name="writeTableOtherMessage">
                    <FrankSender name="writeTableOtherMessage" target="writeTableOtherMessage" />
                </SenderPipe>
            </Pipeline>
        </Adapter>
        <Adapter name="writeTableMessage">
            <Receiver transactionAttribute="Mandatory">
                <JavaListener name="writeTableMessage" serviceName="writeTableMessage" />
            </Receiver>
            <Pipeline>
                <SenderPipe name="writeTableMessage">
                    <FixedQuerySender query="INSERT INTO &quot;message&quot;(message) VALUES(?)">
                        <Param name="message" type="string" defaultValueMethods="input" />
                    </FixedQuerySender>
                </SenderPipe>
            </Pipeline>
        </Adapter>
        <Adapter name="writeTableOtherMessage">
            <Receiver transactionAttribute="Mandatory">
                <JavaListener name="writeTableOtherMessage" serviceName="writeTableOtherMessage" />
            </Receiver>
            <Pipeline>
                <SenderPipe name="writeTableOtherMessage">
                    <FixedQuerySender query="INSERT INTO &quot;otherMessage&quot;(message) VALUES(?)">
                        <Param name="message" type="string" defaultValueMethods="input" />
                    </FixedQuerySender>
                </SenderPipe>
            </Pipeline>
        </Adapter>
    </Configuration>
    
  6. In the Frank!Console, stop adapter writeTableOtherMessage.

  7. Using your API client, send a HTTP request to the application.

  8. Check that there is a Ladybug report for handling this HTTP request. It should be red to indicate that an error occurred. It should also show that adapter writeTableMessage ran successfully.

  9. Using JDBC | Execute Query, check that neither table Message nor table otherMessage has been updated, even though the adapter that initially updated table Message was successful. This demonstrates that the transaction was rolled back.

  10. Stop the application and modify it as follows. Remove the attribute transactionAttribute="Required" from adapter writeDb, but leave the transactionAttribute="Mandatory" of the two sub-adapters.

  11. Start the application again and make sure that all adapters are up and running.

  12. Using your API client, issue a HTTP request to the application.

  13. It should fail. Check the messages in the Frank!Console. It has failed because adapters writeTableMessage and writeTableOtherMessage were called outside of a transaction. The attribute transactionAttribute="Mandatory" means that calling this receiver (or pipeline) outside of a transaction should be flagged as an error.

Note

Attribute transactionAttribute has a default value, which is Supports. This means that the above example works without setting transactionAttribute on the sub-adapters. Setting transactionAttribute="Required" in the receiver of adapter writeDb is sufficient. This lets the sub-adapters support the transaction that was initiated by the calling adapter.