Fire-and-Forget: The Power of Sessionless Transactions

New in Oracle Database 23.6, Sessionless Transactions let your apps run database operations with no persistent client-side session or connection. With Stateless Transactions, you can start a transaction, suspend it during idle time, and resume it later using a Global Transaction ID or GTRID.

Why use Sessionless Transactions?

  • Scalability: No need to hold server resources during extended processing, well-suited for asynchronous requests
  • ☁️ Great for microservices: Works well with REST APIs, event-driven triggers, or cloud functions where keeping state is undesirable.
  • 🔒 Safer by design: When you use Sessionless Transactions, the database internally coordinates the two-phase commit (2PC) protocol, without the need for any application-side logic.

In this article, we’ll build a simple JDBC app that leverages Sessionless Transactions to split work between two threads with separate database connections, demonstrating efficient and disposable database resource usage.

Just looking for the code? Check out my Sessionless Transactions module on GitHub. I recommend cloning this module if you want to follow along.

Building the JDBC App

Our app will handle order processing events, using Sessionless Transactions to incrementally update order status with an order service. This pattern fits the “sessionless” model, as order processing has some intermittent wait time – think an external request, or time-consuming processing where we may not want to hold a connection persistently open.

The app uses a simple table to demonstrate sessionless transactions, order_processing. You can find the DDL here or in the orders.sql file. Run this script against your database instance:

create table order_processing (
    id        number(10) generated always as identity primary key,
    status    varchar2(20) check (status in ('created', 'inventory_reserved', 'completed', 'failed')),
    gtrid     raw(16),
    timestamp timestamp default systimestamp
);

Implementing the OrderService

The OrderService class handles order creation, validation, and finalization using Sessionless Transactions. It does so in three sequential methods: startOrder, validateInventory, and finishOrder. Let’s look at each of these, to get an idea of how Sessionless Transactions work over JDBC.

startOrder

The startOrder method uses the oracleConnection.startTransaction method to create a GTRID, represented in Java as a byte array. We use the generated GTRID to track the transaction over the lifetime of the order.

In this code, a row with the GTRID is inserted into the order_processing table, indicating that the order was created. After the insert, we suspend the transaction and close the connection.

    private byte[] startOrder() throws SQLException {
        final String sql = "insert into order_processing (status, gtrid) values('created', ?)";
        try (OracleConnection conn = (OracleConnection) dataSource.getConnection();
             PreparedStatement stmt = conn.prepareStatement(sql)) {
            conn.setAutoCommit(false);
            // Start a connection with a timeout of 30 seconds (default 60 seconds).
            // The JDBC driver generates a GTRID for us.
            byte[] gtrid = conn.startTransaction(30);
            stmt.setBytes(1, gtrid);
            stmt.executeUpdate();
            conn.suspendTransactionImmediately();
            return gtrid;
        }
    }

validateInventory

The validateInventory method uses the GTRID to resume the transaction, and inserts a row indicating whether the validation check succeeded or failed. If the check failed, we commit the transaction and end our order processing.

    private boolean validateInventory(byte[] gtrid) throws SQLException {
        final String sql = "insert into order_processing (status, gtrid) values(?, ?)";
        try (OracleConnection conn = (OracleConnection) dataSource.getConnection();
             PreparedStatement stmt = conn.prepareStatement(sql)) {
            conn.setAutoCommit(false);
            // Resume the transaction using the GTRID.
            conn.resumeTransaction(gtrid);
            boolean isInventoryOK = simulateExternalCall();
            stmt.setString(1, isInventoryOK ? "inventory_reserved" : "failed");
            stmt.setBytes(2, gtrid);
            stmt.executeUpdate();
            if (isInventoryOK) {
                conn.suspendTransactionImmediately();
            } else {
                conn.commit();
            }
            return isInventoryOK;
        }
    }

finishOrder

Next, we have the finishOrder method, which resumes the transaction using the GTRID, and completes the order successfully. We commit the transaction after the last insert.

    private void finishOrder(byte[] gtrid) throws SQLException {
        final String sql = "insert into order_processing (status, gtrid) values('completed', ?)";
        try (OracleConnection conn = (OracleConnection) dataSource.getConnection();
             PreparedStatement stmt = conn.prepareStatement(sql)) {
            conn.setAutoCommit(false);
            // Resume the transaction using the GTRID.
            conn.resumeTransaction(gtrid);
            stmt.setBytes(1, gtrid);
            stmt.executeUpdate();
            conn.commit();
        }
    }

Finally, we tie these methods together with a processOrder method, that calls each sequentially:

    public void processOrder() {
        try {
            byte[] gtrid = startOrder();
            if (!validateInventory(gtrid)) {
                return;
            }
            finishOrder(gtrid);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

Main Class

Our SessionlessTransactions Main Class creates a datasource connected to an Oracle Database Free container, and then fires off 10 virtual threads to asynchronously process orders.

package com.example.txn;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

import javax.sql.DataSource;
import oracle.ucp.jdbc.PoolDataSource;
import oracle.ucp.jdbc.PoolDataSourceImpl;

import static java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor;

public class SessionlessTransactions {

    private static DataSource getDataSource() throws SQLException {
        PoolDataSource ds = new PoolDataSourceImpl();
        ds.setURL("jdbc:oracle:thin:@localhost:1521/freepdb1");
        ds.setConnectionFactoryClassName("oracle.jdbc.pool.OracleDataSource");
        ds.setConnectionPoolName("SessionlessTransactionsExample");
        ds.setUser("testuser");
        ds.setPassword("testpwd");
        ds.setMaxPoolSize(30);
        ds.setInitialPoolSize(10);
        ds.setMinPoolSize(1);
        return ds;
    }

    public static void main(String[] args) throws Exception {
        // Get a datasource to a local container database
        DataSource ds = getDataSource();
        OrderService orderService = new OrderService(ds);

        // Start 10 order processing tasks
        ExecutorService executor = newVirtualThreadPerTaskExecutor();
        List<Future<?>> tasks = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            System.out.println("Starting Order " + i);
            tasks.add(executor.submit(orderService::processOrder));
        }
        // Wait for order processing to complete
        for (Future<?> task : tasks) {
            task.get();
        }
        System.out.println("Orders processed");
    }
}

To run the main class, use the following Maven command from the sessionless-transactions directory:

mvn exec:java

After the app finishes running (it should only take a few seconds), you can query the database to view order records. It should look something like this:

select status as order_status, gtrid, timestamp
from order_processing
order by gtrid, timestamp;
Database table displaying order processing status, including columns for order status, GTRID, and timestamp.

That’s all! You should have a solid understanding of how to create, suspend, resume, and commit Sessionless Transactions with the Oracle JDBC driver!

Restrictions

While Sessionless Transactions can be used in a single-instance configuration or multi-instance Real Application Clusters (RAC) configuration, they are only applicable to a single database instance.

It’s also important to note that some database features are not supported in Sessionless Transactions, like DB Links or promoting a Sessionless Transaction to an XA transaction. Refer to the Sessionless Transactions Restrictions for a complete list.

References

Leave a Reply

Discover more from andersswanson.dev

Subscribe now to keep reading and get access to the full archive.

Continue reading