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

io.helidon.integrations.jta.jdbc.JtaDataSource Maven / Gradle / Ivy

/*
 * Copyright (c) 2021, 2023 Oracle and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.helidon.integrations.jta.jdbc;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Supplier;

import javax.sql.DataSource;

import io.helidon.integrations.jdbc.AbstractDataSource;
import io.helidon.integrations.jdbc.ConditionallyCloseableConnection;

import jakarta.transaction.Status;
import jakarta.transaction.Synchronization;

/**
 * An {@link AbstractDataSource} and a {@link Synchronization} that wraps another {@link DataSource} that is known to
 * not behave correctly in the presence of JTA transaction management, such as one supplied by any of several freely and
 * commercially available connection pools, and that makes such a non-JTA-aware {@link DataSource} behave as sensibly as
 * possible in the presence of a JTA-managed transaction.
 *
 * 

Thread Safety

* *

Instances of this class are safe for concurrent use by multiple threads. No such guarantee obviously can be made * about the {@link DataSource} wrapped by any given instance of this class.

* *

Note that the JDBC specification places no requirement on any implementor to make any implementations of any JDBC * constructs thread-safe. Nevertheless, a certain amount of unspecified thread safety must exist in all JDBC * implementations or their constructs could never be enrolled in JTA-compliant transactions.

* * @deprecated This class is slated for removal. It makes incorrect assumptions about threading in a JTA environment. * Specifically, this class' implementation incorrectly assumes that the {@link #afterCompletion(int)} method will be * invoked on the same thread as the governing transaction, which is not necessarily the case, especially in the case of * asynchronous rollbacks. As a result, {@link Connection}s acquired by instances of this class may not be closed * properly. */ @Deprecated(forRemoval = true, since = "3.0.3") public final class JtaDataSource extends AbstractDataSource implements Synchronization { /* * Static fields. */ private static final Object UNAUTHENTICATED_CONNECTION_IDENTIFIER = new Object(); private static final ThreadLocal>> CONNECTIONS_TL = ThreadLocal.withInitial(HashMap::new); /* * Instance fields. */ private final Supplier delegateSupplier; private final BooleanSupplier transactionIsActiveSupplier; /* * Constructors. */ /** * Creates a new {@link JtaDataSource}. * * @param dataSource the {@link DataSource} instance to which operations will be delegated; must not be {@code null} * * @param transactionIsActiveSupplier a {@link BooleanSupplier} that returns {@code true} only if the current * transaction, if any, is active; must not be {@code null} * * @exception NullPointerException if either parameter is {@code null} * * @see #JtaDataSource(Supplier, BooleanSupplier) */ public JtaDataSource(DataSource dataSource, BooleanSupplier transactionIsActiveSupplier) { this(() -> dataSource, transactionIsActiveSupplier); } /** * Creates a new {@link JtaDataSource}. * * @param delegateSupplier a {@link Supplier} of {@link DataSource} instances to which operations will be delegated; * must not be {@code null} * * @param transactionIsActiveSupplier an {@link BooleanSupplier} that returns {@code true} only if the current * transaction, if any, is active; must not be {@code null} * * @exception NullPointerException if either parameter is {@code null} */ public JtaDataSource(Supplier delegateSupplier, BooleanSupplier transactionIsActiveSupplier) { super(); this.delegateSupplier = Objects.requireNonNull(delegateSupplier, "delegateSupplier"); this.transactionIsActiveSupplier = Objects.requireNonNull(transactionIsActiveSupplier, "transactionIsActiveSupplier"); } /* * Instance methods. */ /** * If there is an active transaction, registers this {@link JtaDataSource} with the supplied registrar, which is * most commonly—but is not required to be—a reference to the {@link * jakarta.transaction.TransactionSynchronizationRegistry#registerInterposedSynchronization(Synchronization)} * method. * *

If there is no currently active transaction, no action is taken.

* * @param registrar a {@link Consumer} that may {@linkplain Consumer#accept(Object) accept} this {@link * JtaDataSource} if there is a currently active transaction; must not be {@code null} * * @return {@code true} if registration occurred; {@code false} otherwise * * @exception NullPointerException if {@code registrar} is {@code null} * * @exception RuntimeException if the supplied {@code registrar}'s {@link Consumer#accept(Object) accept} method * throws a {@link RuntimeException} * * @deprecated This method is slated for removal with no replacement. */ @Deprecated(forRemoval = true) public boolean registerWith(Consumer registrar) { if (this.transactionIsActiveSupplier.getAsBoolean()) { registrar.accept(this); return true; } return false; } /** * Implements the {@link Synchronization#beforeCompletion()} method to do nothing. */ @Override // Synchronization public void beforeCompletion() { } /** * Ensures that any thread-associated connections are properly committed, restored to their initial states, closed * where appropriate and removed from the system when a definitionally thread-scoped JTA transaction commits or * rolls back. * * @param status the status of the transaction after completion; must be either {@link Status#STATUS_COMMITTED} or * {@link Status#STATUS_ROLLEDBACK} * * @exception IllegalArgumentException if {@code status} is neither {@link Status#STATUS_COMMITTED} nor {@link * Status#STATUS_ROLLEDBACK} */ @Override // Synchronization public void afterCompletion(int status) { // Validate the status coming in, but make sure that no matter what we remove any transaction-specific // connections from the ThreadLocal storing such connections. Doing this right is the reason for deferring any // throwing of an IllegalArgumentException in invalid cases. IllegalArgumentException badStatusException; CheckedConsumer consumer; switch (status) { case Status.STATUS_COMMITTED: badStatusException = null; consumer = Connection::commit; break; case Status.STATUS_ROLLEDBACK: badStatusException = null; consumer = Connection::rollback; break; default: badStatusException = new IllegalArgumentException("Unexpected transaction status after completion: " + status); consumer = null; break; } // Get all of the TransactionSpecificConnections we have released into the world via our getConnection() and // getConnection(String, String) methods, and inform them that the transaction is over. Then remove them from // the map since the transaction is over. These particular Connections out in the world will not participate in // future JTA transactions, even if such transactions are started on this thread. // // TO DO: this assumes that afterCompletion(int) will be called on the same thread as the transaction. This is // not the case. An asynchronous rollback is possible, and in this case, this thread local map will be empty. Map extantConnectionsMap = CONNECTIONS_TL.get().get(this); if (extantConnectionsMap != null) { Collection extantConnections = extantConnectionsMap.values(); try { if (badStatusException == null) { complete(extantConnections, consumer); } else { throw badStatusException; } } finally { extantConnections.clear(); } } } /** * Given an {@link Iterable} of {@link TransactionSpecificConnection} instances and a {@link CheckedConsumer} of * {@link Connection} instances, ensures that the {@link CheckedConsumer#accept(Object)} method is invoked on each * reachable {@link TransactionSpecificConnection}, properly handling all exceptional conditions. * *

The {@link TransactionSpecificConnection} instances will have their auto-commit statuses reset and their * closeable statuses set to {@code true}, even in the presence of exceptional conditions.

* *

The {@link TransactionSpecificConnection}s will also be closed if a caller has requested their closing prior * to this method executing.

* *

If a user has not requested their closing prior to this method executing, the {@link * TransactionSpecificConnection}s will not be closed, but will become closeable by the end user (allowing them to * be released back to any backing connection pool that might exist). They will no longer take part in any new JTA * transactions from this point forward (a new {@link Connection} will have to be acquired while a JTA transaction * is active for that behavior).

* * @param connections an {@link Iterable} of {@link TransactionSpecificConnection} instances; must not be {@code * null} * * @param consumer a {@link CheckedConsumer} that will be invoked on each connection, even in the presence of * exceptional conditions; must not be {@code null} * * @exception NullPointerException if {@code connections} or {@code consumer} is {@code null} * * @exception IllegalStateException if an error occurs */ private static void complete(Iterable connections, CheckedConsumer consumer) { RuntimeException runtimeException = null; for (TransactionSpecificConnection connection : connections) { try { consumer.accept(connection); } catch (RuntimeException exception) { if (runtimeException == null) { runtimeException = exception; } else { runtimeException.addSuppressed(exception); } } catch (Exception exception) { if (runtimeException == null) { runtimeException = new IllegalStateException(exception.getMessage(), exception); } else { runtimeException.addSuppressed(exception); } } finally { try { connection.restoreAutoCommit(); } catch (RuntimeException exception) { if (runtimeException == null) { runtimeException = exception; } else { runtimeException.addSuppressed(exception); } } catch (SQLException sqlException) { if (runtimeException == null) { runtimeException = new IllegalStateException(sqlException.getMessage(), sqlException); } else { runtimeException.addSuppressed(sqlException); } } finally { connection.setCloseable(true); try { if (connection.isCloseCalled()) { connection.close(); } } catch (SQLException sqlException) { if (runtimeException == null) { runtimeException = new IllegalStateException(sqlException.getMessage(), sqlException); } else { runtimeException.addSuppressed(sqlException); } } } } } if (runtimeException != null) { throw runtimeException; } } /** * Returns a special kind of {@link Connection} that is sourced from an underlying {@link DataSource}. * *

The {@link Connection} returned by this method:

* *
    * *
  • is never {@code null} (unless the underlying {@link DataSource} is not JDBC-compliant)
  • * *
  • is exactly the {@link Connection} returned by the underlying {@link DataSource} when there is no JTA * transaction in effect at the time that this method is invoked
  • * *
* *

Otherwise, when a JTA transaction is in effect, the {@link Connection} returned by this method:

* *
    * *
  • is the same {@link Connection} returned by prior invocations of this method on the same thread during the * lifespan of a JTA transaction. That is, the {@link Connection} is "pinned" to the current thread for the * lifespan of the transaction.
  • * *
  • is not actually closeable when a JTA transaction is in effect. The {@link Connection#close()} method will * behave from the standpoint of the caller as if it functions normally, but its invocation will not actually be * propagated to the underlying {@link DataSource}'s connection. The fact that it was in fact invoked will be * stored, and at such time that the JTA transaction completes this {@link Connection} will be closed at that * point.
  • * *
  • has its autocommit status set to {@code false}
  • * *
  • will have {@link Connection#commit()} called on it when the JTA transaction commits
  • * *
  • will have {@link Connection#rollback()} called on it when the JTA transaction rolls back
  • * *
  • will have its autocommit status restored to its original value after the transaction completes
  • * *
* * @return a non-{@code null} {@link Connection} * * @exception SQLException if an error occurs * * @exception RuntimeException if the {@link BooleanSupplier} supplied at construction time that reports a * transaction's status throws a {@link RuntimeException}, or if the {@link Supplier} supplied at construction time * that retrieves a delegate {@link DataSource} throws a {@link RuntimeException} * * @see DataSource#getConnection() * * @see DataSource#getConnection(String, String) */ @Override // AbstractDataSource public Connection getConnection() throws SQLException { return this.getConnection(null, null, true); } /** * Returns a special kind of {@link Connection} that is sourced from an underlying {@link DataSource}. * *

The {@link Connection} returned by this method:

* *
    * *
  • is never {@code null} (unless the underlying {@link DataSource} is not JDBC-compliant)
  • * *
  • is exactly the {@link Connection} returned by the underlying {@link DataSource} when there is no JTA * transaction in effect at the time that this method is invoked
  • * *
* *

Otherwise, when a JTA transaction is in effect, the {@link Connection} returned by this method:

* *
    * *
  • is the same {@link Connection} returned by prior invocations of this method with the same credentials (or no * credentials) on the same thread during the lifespan of a JTA transaction. That is, the {@link Connection} is * "pinned" to the current thread for the lifespan of the transaction
  • * *
  • is not actually closeable when a JTA transaction is in effect. The {@link Connection#close()} method will * behave from the standpoint of the caller as if it functions normally, but its invocation will not actually be * propagated to the underlying {@link DataSource}'s connection. The fact that it was in fact invoked will be * stored, and at such time that the JTA transaction completes this {@link Connection} will be closed at that * point.
  • * *
  • has its autocommit status set to {@code false}
  • * *
  • will have {@link Connection#commit()} called on it when the JTA transaction commits
  • * *
  • will have {@link Connection#rollback()} called on it when the JTA transaction rolls back
  • * *
  • will have its autocommit status restored to its original value after the transaction completes
  • * *
* * @param username the username to use to acquire an underlying {@link Connection}; may be {@code null} * * @param password the password to use to acquire an underlying {@link Connection}; may be {@code null} * * @return a non-{@code null} {@link Connection} * * @exception SQLException if an error occurs * * @exception RuntimeException if the {@link BooleanSupplier} supplied at construction time that reports a * transaction's status throws a {@link RuntimeException}, or if the {@link Supplier} supplied at construction time * that retrieves a delegate {@link DataSource} throws a {@link RuntimeException} * * @see DataSource#getConnection() * * @see DataSource#getConnection(String, String) */ @Override // AbstractDataSource public Connection getConnection(String username, String password) throws SQLException { return this.getConnection(username, password, false); } /** * Returns a special kind of {@link Connection} that is sourced from an underlying {@link DataSource}. * *

The {@link Connection} returned by this method:

* *
    * *
  • is never {@code null} (unless the underlying {@link DataSource} is not JDBC-compliant)
  • * *
  • is exactly the {@link Connection} returned by the underlying {@link DataSource} when there is no JTA * transaction in effect at the time that this method is invoked
  • * *
* *

Otherwise, when a JTA transaction is in effect, the {@link Connection} returned by this method:

* *
    * *
  • is the same {@link Connection} returned by prior invocations of this method with the same credentials (or no * credentials) on the same thread during the lifespan of a JTA transaction. That is, the {@link Connection} is * "pinned" to the current thread for the lifespan of the transaction.
  • * *
  • is not actually closeable when a JTA transaction is in effect. The {@link Connection#close()} method will * behave from the standpoint of the caller as if it functions normally, but its invocation will not actually be * propagated to the underlying {@link DataSource}'s connection. The fact that it was in fact invoked will be * stored, and at such time that the JTA transaction completes this {@link Connection} will be closed at that * point.
  • * *
  • has its autocommit status set to {@code false}
  • * *
  • will have {@link Connection#commit()} called on it when the JTA transaction commits
  • * *
  • will have {@link Connection#rollback()} called on it when the JTA transaction rolls back
  • * *
  • will have its autocommit status restored to its original value after the transaction completes
  • * *
* * @param username the username to use to acquire an underlying {@link Connection}; may be {@code null} * * @param password the password to use to acquire an underlying {@link Connection}; may be {@code null} * * @param useZeroArgumentForm whether the underlying {@link DataSource}'s {@link DataSource#getConnection()} method * should be called * * @return a non-{@code null} {@link Connection} * * @exception SQLException if an error occurs * * @exception RuntimeException if the {@link BooleanSupplier} supplied at construction time that reports a * transaction's status throws a {@link RuntimeException}, or if the {@link Supplier} supplied at construction time * that retrieves a delegate {@link DataSource} throws a {@link RuntimeException} * * @see DataSource#getConnection() * * @see DataSource#getConnection(String, String) */ private Connection getConnection(String username, String password, boolean useZeroArgumentForm) throws SQLException { Connection returnValue; if (this.transactionIsActiveSupplier.getAsBoolean()) { Map extantConnections = CONNECTIONS_TL.get().computeIfAbsent(this, k -> new HashMap<>()); Object id; if (useZeroArgumentForm) { id = UNAUTHENTICATED_CONNECTION_IDENTIFIER; } else { id = new AuthenticatedConnectionIdentifier(username, password); } TransactionSpecificConnection tsc = extantConnections.get(id); if (tsc == null) { if (useZeroArgumentForm) { tsc = new TransactionSpecificConnection(this.delegateSupplier.get().getConnection()); } else { tsc = new TransactionSpecificConnection(this.delegateSupplier.get().getConnection(username, password)); } extantConnections.put(id, tsc); } else { tsc.setCloseCalled(false); } returnValue = tsc; } else if (useZeroArgumentForm) { returnValue = this.delegateSupplier.get().getConnection(); } else { returnValue = this.delegateSupplier.get().getConnection(username, password); } return returnValue; } /* * Inner and nested classes. */ /** * A functional interface that accepts a payload and may throw an {@link Exception} as a result. * * @param the type of payload * * @see #accept(Object) */ @FunctionalInterface private interface CheckedConsumer { /** * Accepts the supplied payload in some way. * * @param payload the object to accept; may be {@code null} * * @exception Exception if an error occurs */ void accept(T payload) throws Exception; } /** * A {@link ConditionallyCloseableConnection} that tracks when the {@link #close()} method has been called and that * handles auto-commit gracefully. * * @deprecated This class is slated for removal. See the {@link ConditionallyCloseableConnection} class for an * alternative. */ @Deprecated(forRemoval = true, since = "3.0.3") static final class TransactionSpecificConnection extends ConditionallyCloseableConnection { private static final boolean STRICT_CLOSED_CHECKING = Boolean.getBoolean("helidon.jta.strict.closed.checking"); private final boolean oldAutoCommit; private boolean closeCalled; TransactionSpecificConnection(Connection delegate) throws SQLException { super(delegate, false, // not closeable STRICT_CLOSED_CHECKING); this.oldAutoCommit = this.getAutoCommit(); this.setAutoCommit(false); } private void restoreAutoCommit() throws SQLException { this.setAutoCommit(this.oldAutoCommit); } @Override // ConditionallyCloseableConnection public void close() throws SQLException { this.setCloseCalled(true); super.close(); } boolean isCloseCalled() throws SQLException { return this.closeCalled || this.isClosed(); } private void setCloseCalled(boolean closeCalled) { this.closeCalled = closeCalled; } } @Deprecated(forRemoval = true, since = "3.0.3") private static final class AuthenticatedConnectionIdentifier { private final String username; private final String password; private AuthenticatedConnectionIdentifier(String username, String password) { super(); this.username = username; this.password = password; } @Override // Object public int hashCode() { return Objects.hash(this.username, this.password); } @Override // Object public boolean equals(Object other) { if (other == this) { return true; } else if (other != null && other.getClass().equals(AuthenticatedConnectionIdentifier.class)) { AuthenticatedConnectionIdentifier her = (AuthenticatedConnectionIdentifier) other; return Objects.equals(this.username, her.username) && Objects.equals(this.password, her.password); } else { return false; } } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy