All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ibm.fhir.persistence.jdbc.connection.FHIRUserTransactionAdapter Maven / Gradle / Ivy

There is a newer version: 4.11.1
Show newest version
/*
 * (C) Copyright IBM Corp. 2020, 2021
 *
 * SPDX-License-Identifier: Apache-2.0
 */

package com.ibm.fhir.persistence.jdbc.connection;

import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.transaction.Status;
import javax.transaction.TransactionSynchronizationRegistry;
import javax.transaction.UserTransaction;

import com.ibm.fhir.config.FHIRRequestContext;
import com.ibm.fhir.persistence.FHIRPersistenceTransaction;
import com.ibm.fhir.persistence.exception.FHIRPersistenceException;
import com.ibm.fhir.persistence.jdbc.FHIRPersistenceJDBCCache;
import com.ibm.fhir.persistence.jdbc.exception.FHIRPersistenceDataAccessException;
import com.ibm.fhir.persistence.jdbc.impl.CacheTransactionSync;


/**
 * Adapter to simplify transaction handling. This object is returned by
 * FHIRPersistenceJDBCImpl and can be used instead of the old FHIRTransactionHelper
 */
public class FHIRUserTransactionAdapter implements FHIRPersistenceTransaction {
    private static final Logger log = Logger.getLogger(FHIRUserTransactionAdapter.class.getName());

    // The connection strategy handling the underlying transaction object
    private final UserTransaction userTransaction;

    // The sync registry to send tx commit events to the cache
    private final TransactionSynchronizationRegistry syncRegistry;

    // The cache which needs to be notified when a commit completes
    private final FHIRPersistenceJDBCCache cache;

    // The resource key used for the TransactionData object in held in the TransactionSynchronizationRegistry
    private final String transactionDataKey;

    // Did this instance start the transaction?
    private boolean startedByThis;

    // support nesting by tracking the number of begin/end requests
    private int startCount;

    /**
     * Public constructor
     * @param tx
     */
    public FHIRUserTransactionAdapter(UserTransaction tx, TransactionSynchronizationRegistry syncRegistry, FHIRPersistenceJDBCCache cache,
        String transactionDataKey) {
        this.userTransaction = tx;
        this.syncRegistry = syncRegistry;
        this.cache = cache;
        this.transactionDataKey = transactionDataKey;
        startedByThis = false;
    }

    /**
     * If a transaction has not yet been started on this thread, then start one.
     * @throws FHIRPersistenceException
     */
    @Override
    public void begin() throws FHIRPersistenceException {

        FHIRRequestContext ctx = FHIRRequestContext.get();
        boolean bulk = ctx != null && ctx.isBulk();

        int status = getStatus();
        if (isNoTransaction(status)) {
            // This instance will be responsible for starting and ending
            // the transaction
            log.fine("No current transaction. Starting new transaction on current thread.");
            try {
                userTransaction.begin();
                startedByThis = true;
                this.startCount++;

                // On starting a new transaction, we need to register a callback so that
                // the cache is informed when the transaction commits it can promote thread-local
                // ids to the shared caches.
                syncRegistry.registerInterposedSynchronization(new CacheTransactionSync(this.syncRegistry, this.cache, this.transactionDataKey));

            } catch (Exception x) {
                log.log(Level.SEVERE, "failed to start transaction", x);
                throw new FHIRPersistenceDataAccessException("Start global transaction failed. See server log for details");
            }
        } else if (isMarkedForRollback(status)) {
            // the transaction is active but has already been marked for rollback. This is OK,
            // we just behave as though we're a nested transaction
            this.startCount++;
        } else if (isActive(status) && bulk) {
            // On starting a bulk transaction, we need to register a callback so that
            // the cache is informed when the transaction commits it can promote thread-local
            // ids to the shared caches.
            syncRegistry.registerInterposedSynchronization(new CacheTransactionSync(this.syncRegistry, this.cache, this.transactionDataKey));

            // transaction is already active, so this is a nested request
            this.startCount++;
        } else if (isActive(status)) {
            // transaction is already active, so this is a nested request
            this.startCount++;
        } else {
            // any other status means that we can't begin a new transaction here
            throw new FHIRPersistenceDataAccessException("Cannot begin transaction. Status=" + status);
        }
    }

    /**
     * If we previously started a transaction on this thread using this helper instance,
     * then commit it now.
     * @throws FHIRPersistenceException
     */
    @Override
    public void end() throws FHIRPersistenceException {
        // If we previously started a transaction with this instance and we are the outermost begin/end pair, then commit now.
        if (--startCount == 0 && startedByThis) {
            int status = getStatus();
            if (isActive(status)) {
                // transaction started by this instance and is active
                log.fine("Committing transaction on current thread...");

                try {
                    userTransaction.commit();
                } catch (Exception x) {
                    log.log(Level.SEVERE, "failed to commit transaction", x);
                    throw new FHIRPersistenceDataAccessException("Commit global transaction failed. See server log for details");
                } finally {
                    startedByThis = false;
                }
            } else if (isMarkedForRollback(status)) {
                // transaction started by this instance but has been marked for rollback
                log.fine("Rolling back transaction on current thread...");
                try {
                    userTransaction.rollback();
                } catch (Exception x) {
                    log.log(Level.SEVERE, "failed to rollback transaction", x);
                    throw new FHIRPersistenceDataAccessException("Rollback global transaction failed. See server log for details");
                } finally {
                    startedByThis = false;
                }
            } else {
                // Did not expect the transaction to be in this state
                throw new FHIRPersistenceException("unexpected transaction status: " + status);
            }
        } else {
            // The commit will be handled by the first instance which started the transaction, not this one
            log.fine("This FHIRPersistenceTransaction instance did not start the transaction, so skipping commit");
        }
    }

    @Override
    public void setRollbackOnly() throws FHIRPersistenceException {
        // trap door - mark for rollback, *even* if we didn't start the transaction
        int status = getStatus();
        try {
            // Only mark the transaction for rollback if we started it. This is necessary
            // to correctly handle nested calls in the current JDBC persistence layer.
            // This behavior should be reconsidered so that any layer can mark the
            // transaction for rollback if the error is fatal. Needs a closer look
            // at what constitutes an expected vs. unexpected failure
            if (this.startedByThis && startCount == 1) {
                if (isActive(status)) {
                    log.fine("Marking transaction for rollback.");
                    this.userTransaction.setRollbackOnly();
                } else if (!isMarkedForRollback(status)) {
                    log.warning("mark for rollback - transaction not active, status=" + status);
                    throw new FHIRPersistenceDataAccessException("No current transaction to mark for rollback");
                }
            } else {
                log.fine("Transaction not started by this so ignoring setRollbackOnly");
            }
        } catch (Exception x) {
            log.log(Level.SEVERE, "failed to start transaction, status=" + status, x);
            throw new FHIRPersistenceDataAccessException("Start global transaction failed. See server log for details");
        }
    }

    @Override
    public boolean isTransactional() {

        // well, yes of course we are
        return true;
    }

    /**
     * Is there a transaction currently on this thread?
     * @return true if a global transaction is active on the current thread
     */
    protected boolean isActive(int status) {
        return status == Status.STATUS_ACTIVE;
    }

    /**
     * Has this transaction been marked for rollback?
     * @return
     */
    protected boolean isMarkedForRollback(int status) {
        return status == Status.STATUS_MARKED_ROLLBACK;
    }

    /**
     * Are we in the NO TRANSACTION state?
     * @param status
     * @return
     */
    protected boolean isNoTransaction(int status) {
        return status == Status.STATUS_NO_TRANSACTION;
    }

    /**
     * Get the {@link Status} of the global transaction
     * @return
     * @throws FHIRPersistenceDataAccessException
     */
    protected int getStatus() throws FHIRPersistenceDataAccessException {
        try {
            return userTransaction.getStatus();
        } catch (Throwable e) {
            String msg = "An unexpected error occurred while examining transactional status.";
            FHIRPersistenceDataAccessException fx = new FHIRPersistenceDataAccessException(msg);
            log.log(Level.SEVERE, fx.getMessage(), e);
            throw fx;
        }
    }

    /**
     * Call the supplier function within a begin/end
     * @param 
     * @param s
     * @return
     */
     T func(Supplier s) throws FHIRPersistenceException {
        try {
            begin();
            return s.get();
        } finally {
            end();
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy