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

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

/*
 * Copyright (c) 2022, 2024 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.EnumSet;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;

import io.helidon.integrations.jdbc.SQLRunnable;
import io.helidon.integrations.jdbc.UncheckedSQLException;
import io.helidon.integrations.jta.jdbc.ExceptionConverter.XARoutine;

import static javax.transaction.xa.XAException.XAER_DUPID;
import static javax.transaction.xa.XAException.XAER_INVAL;
import static javax.transaction.xa.XAException.XAER_NOTA;
import static javax.transaction.xa.XAException.XAER_PROTO;
import static javax.transaction.xa.XAException.XAER_RMERR;
import static javax.transaction.xa.XAException.XAER_RMFAIL;
import static javax.transaction.xa.XAException.XA_RBROLLBACK;

/**
 * An {@link XAResource} that adapts an ordinary arbitrary {@link Connection} as much as possible to the {@link
 * XAResource} contract.
 *
 * 

Note: instances of this class are lossless in the presence of one-phase commit operations and * potentially lossy in the presence of two-phase commit operations.

* *

Instances of this class are safe for concurrent use by multiple threads.

*/ final class LocalXAResource implements XAResource { /* * Static fields. */ private static final Logger LOGGER = Logger.getLogger(LocalXAResource.class.getName()); private static final Xid[] EMPTY_XID_ARRAY = new Xid[0]; // package-protected for testing only. static final ConcurrentMap ASSOCIATIONS = new ConcurrentHashMap<>(); /* * Instance fields. */ private final Function connectionFunction; private final ExceptionConverter exceptionConverter; /* * Constructors. */ /** * Creates a new {@link LocalXAResource}. * * @param connectionFunction a {@link Function} that accepts a {@link Xid} (supplied by the {@link #start(Xid, int)} * method) and returns a {@link Connection} to associate with the global transaction; must not be {@code null}; must * never return {@code null}; must be safe for concurrent use by multiple threads; will never be invoked with a * {@code null} {@link Xid} * * @param exceptionConverter a {@link ExceptionConverter} that accepts a {@link XARoutine} and a {@link * SQLException} and converts the {@link SQLException} to an appropriate {@link XAException} following the * rules defined by the XA Specification as * interpreted by the specification of the {@code javax.transaction.xa} package and its classes; may be {@code null} * in which case a default implementation will be used instead * * @see #start(Xid, int) */ LocalXAResource(Function connectionFunction, ExceptionConverter exceptionConverter) { super(); this.connectionFunction = Objects.requireNonNull(connectionFunction, "connectionFunction"); this.exceptionConverter = exceptionConverter == null ? LocalXAResource::convert0 : exceptionConverter; } /* * Instance methods. */ @Override // XAResource public void start(Xid xid, int flags) throws XAException { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(this.getClass().getName(), "start", new Object[] {xid, flagsToString(flags)}); } requireNonNullXid(xid); BiFunction remappingFunction; switch (flags) { case TMJOIN: remappingFunction = LocalXAResource::join; break; case TMNOFLAGS: remappingFunction = this::start; break; case TMRESUME: remappingFunction = LocalXAResource::resume; break; default: // Bad flags. throw (XAException) new XAException(XAER_INVAL) .initCause(new IllegalArgumentException("xid: " + xid + "; flags: " + flagsToString(flags))); } this.computeAssociation(XARoutine.START, xid, remappingFunction); if (LOGGER.isLoggable(Level.FINER)) { LOGGER.exiting(this.getClass().getName(), "start"); } } // (Remapping BiFunction, used in start() above and supplied to computeAssociation() below.) private Association start(Xid x, Association a) { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(this.getClass().getName(), "start", new Object[] {x, a}); } assert x != null; // x has already been vetted and is known to be non-null if (a != null) { throw new UncheckedXAException((XAException) new XAException(XAER_DUPID) .initCause(new IllegalArgumentException("xid: " + x + "; association: " + a))); } Connection c; try { c = this.connectionFunction.apply(x); } catch (RuntimeException e) { // Weirdly, XAER_RMERR seems to be the "please retry" error code in this one case, not XAER_RMFAIL: // https://github.com/jbosstm/narayana/blob/8ccaf0f85c7a76c227941d26cc3aa3fa9f05b160/ArjunaJTA/jta/classes/com/arjuna/ats/internal/jta/transaction/arjunacore/TransactionImple.java#L682-L689. throw new UncheckedXAException((XAException) new XAException(XAER_RMERR).initCause(e)); } if (c == null) { // Weirdly, XAER_RMERR seems to be the "please retry" error code in this one case, not XAER_RMFAIL: // https://github.com/jbosstm/narayana/blob/8ccaf0f85c7a76c227941d26cc3aa3fa9f05b160/ArjunaJTA/jta/classes/com/arjuna/ats/internal/jta/transaction/arjunacore/TransactionImple.java#L682-L689. throw new UncheckedXAException((XAException) new XAException(XAER_RMERR) .initCause(new NullPointerException("connectionFunction.apply(" + x + ")"))); } a = new Association(Association.BranchState.ACTIVE, x, c); if (LOGGER.isLoggable(Level.FINE)) { LOGGER.logp(Level.FINE, this.getClass().getName(), "start", "Created new Association ({0}) for connection ({1}) in state ACTIVE", new Object[] {a, c}); } if (LOGGER.isLoggable(Level.FINER)) { LOGGER.exiting(this.getClass().getName(), "start", a); } return a; } @Override // XAResource public void end(Xid xid, int flags) throws XAException { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(this.getClass().getName(), "end", new Object[] {xid, flagsToString(flags)}); } requireNonNullXid(xid); BiFunction remappingFunction; switch (flags) { case TMFAIL: case TMSUCCESS: remappingFunction = LocalXAResource::activeToIdle; break; case TMSUSPEND: remappingFunction = LocalXAResource::suspend; break; default: // Bad flags. throw (XAException) new XAException(XAER_INVAL) .initCause(new IllegalArgumentException("xid: " + xid + "; flags: " + flagsToString(flags))); } // Any XAException thrown can have any error code. The transaction will be marked as rollback only. See // https://github.com/jbosstm/narayana/blob/8ccaf0f85c7a76c227941d26cc3aa3fa9f05b160/ArjunaJTA/jta/classes/com/arjuna/ats/internal/jta/transaction/arjunacore/TransactionImple.java#L978-L992. this.computeAssociation(XARoutine.END, xid, remappingFunction); if (LOGGER.isLoggable(Level.FINER)) { LOGGER.exiting(this.getClass().getName(), "end"); } } @Override // XAResource public int prepare(Xid xid) throws XAException { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(this.getClass().getName(), "prepare", xid); } requireNonNullXid(xid); // Any XAException thrown can have basically any error code. See // https://github.com/jbosstm/narayana/blob/c5f02d07edb34964b64341974ab689ea44536603/ArjunaJTA/jta/classes/com/arjuna/ats/internal/jta/resources/arjunacore/XAResourceRecord.java#L227-L261. Object association = this.computeAssociation(XARoutine.PREPARE, xid, EnumSet.of(Association.BranchState.IDLE), LocalXAResource::prepare, false); // don't remove association on error int returnValue = association == null ? XA_RDONLY : XA_OK; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.exiting(this.getClass().getName(), "prepare", returnValue); } return returnValue; } @Override // XAResource public void commit(Xid xid, boolean onePhase) throws XAException { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(this.getClass().getName(), "commit", new Object[] {xid, onePhase}); } requireNonNullXid(xid); // Error handling needs to be extraordinarily specific. XAER_RMERR indicates catastrophic failure (like if a // local rollback(), issued in response to a local commit() failure, occurs). XAER_RMFAIL indicates a transient // error, i.e. we tried to Do The Thing but for now it Didn't Work. // // Concrete examples: You can see that XAER_RMERR and (the completely non-transient) XAER_PROTO (for example) // are both treated as Equally Bad Things: // https://github.com/jbosstm/narayana/blob/c5f02d07edb34964b64341974ab689ea44536603/ArjunaJTA/jta/classes/com/arjuna/ats/internal/jta/resources/arjunacore/XAResourceRecord.java#L512-L514 // // You can also see that XAER_RMFAIL does something different and is no different from XA_RETRY: // https://github.com/jbosstm/narayana/blob/c5f02d07edb34964b64341974ab689ea44536603/ArjunaJTA/jta/classes/com/arjuna/ats/internal/jta/resources/arjunacore/XAResourceRecord.java#L525-L534 // // Finally, if an error happens during commit, we issue a SQL/JDBC/local transaction rollback() command. If // that works, then we return XA_RB*. If that fails, chances are we return XAER_RMERR, but this.computeAssociation(XARoutine.COMMIT, xid, EnumSet.of(Association.BranchState.IDLE, Association.BranchState.PREPARED), a -> commitAndReset(a, onePhase), true); // do remove association on error if (LOGGER.isLoggable(Level.FINER)) { LOGGER.exiting(this.getClass().getName(), "commit"); } } @Override // XAResource public void rollback(Xid xid) throws XAException { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(this.getClass().getName(), "rollback", xid); } requireNonNullXid(xid); // An error during rollback is bad; in a two-phase situation where we've already prepared, then returning // XAER_RMERR will put us in HEURISTIC_HAZARD // (https://github.com/jbosstm/narayana/blob/c5f02d07edb34964b64341974ab689ea44536603/ArjunaJTA/jta/classes/com/arjuna/ats/internal/jta/resources/arjunacore/XAResourceRecord.java#L379-L420). // Doing XAER_RMFAIL will put us in FINISH_ERROR. this.computeAssociation(XARoutine.ROLLBACK, xid, EnumSet.of(Association.BranchState.IDLE, Association.BranchState.PREPARED, Association.BranchState.ROLLBACK_ONLY), LocalXAResource::rollbackAndReset, true); // do remove association on error if (LOGGER.isLoggable(Level.FINER)) { LOGGER.exiting(this.getClass().getName(), "rollback"); } } @Override // XAResource public void forget(Xid xid) throws XAException { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(this.getClass().getName(), "forget", xid); } requireNonNullXid(xid); this.computeAssociation(XARoutine.FORGET, xid, EnumSet.of(Association.BranchState.HEURISTICALLY_COMPLETED), LocalXAResource::forgetAndReset, false); // don't remove association on error if (LOGGER.isLoggable(Level.FINER)) { LOGGER.exiting(this.getClass().getName(), "forget"); } } @Override // XAResource public Xid[] recover(int flags) throws XAException { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(this.getClass().getName(), "recover", flagsToString(flags)); } switch (flags) { case TMENDRSCAN: case TMNOFLAGS: case TMSTARTRSCAN: break; default: // Bad flags. throw (XAException) new XAException(XAER_INVAL) .initCause(new IllegalArgumentException("flags: " + flagsToString(flags))); } if (LOGGER.isLoggable(Level.FINER)) { LOGGER.exiting(this.getClass().getName(), "recover", EMPTY_XID_ARRAY); } return EMPTY_XID_ARRAY; } @Override // XAResource public boolean isSameRM(XAResource xaResource) throws XAException { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(this.getClass().getName(), "isSameRM", xaResource); LOGGER.exiting(this.getClass().getName(), "isSameRM", this == xaResource); } return this == xaResource; } @Override // XAResource public int getTransactionTimeout() { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(this.getClass().getName(), "getTransactionTimeout"); LOGGER.exiting(this.getClass().getName(), "getTransactionTimeout", 0); } return 0; } @Override // XAResource public boolean setTransactionTimeout(int transactionTimeoutInSeconds) { // (Interesting note: this is the first method that is called by the TransactionManager, i.e. before #start(Xid, // int).) if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(this.getClass().getName(), "setTransactionTimeout", transactionTimeoutInSeconds); LOGGER.exiting(this.getClass().getName(), "setTransactionTimeout", false); } return false; } private Association computeAssociation(XARoutine xaRoutine, Xid xid, BiFunction f) throws XAException { try { return ASSOCIATIONS.compute(xid, f); } catch (RuntimeException e) { throw this.convert(xaRoutine, e); } } private Association computeAssociation(XARoutine xaRoutine, Xid xid, EnumSet legalBranchStates, UnaryOperator f, boolean removeAssociationOnError) throws XAException { try { return ASSOCIATIONS.compute(xid, (x, a) -> remap(x, a, legalBranchStates, f)); } catch (RuntimeException e) { if (removeAssociationOnError) { ASSOCIATIONS.remove(xid); } throw this.convert(xaRoutine, e); } } private XAException convert(XARoutine xaRoutine, Throwable e) { XAException returnValue; if (e == null) { returnValue = new XAException(XAER_RMERR); } else if (e instanceof XAException xaException) { // Obviously if e is an XAException no conversion is necessary. returnValue = xaException; } else { Throwable cause = e.getCause(); if (cause instanceof XAException xaException) { // No matter what, if the cause was an XAException then it is canonical. returnValue = xaException; } else if (e instanceof IllegalTransitionException) { // Any IllegalTransitionException is by definition an XA protocol problem. returnValue = (XAException) new XAException(XAER_PROTO).initCause(e); } else if (e instanceof SQLException sqlException) { returnValue = this.exceptionConverter.convert(xaRoutine, sqlException); } else if (cause instanceof SQLException sqlException) { returnValue = this.exceptionConverter.convert(xaRoutine, sqlException); } else { returnValue = (XAException) new XAException(XAER_RMERR).initCause(e); } } if (returnValue == null) { returnValue = (XAException) new XAException(XAER_RMERR).initCause(e); } return returnValue; } /* * Static methods. */ private static void requireNonNullXid(Xid xid) throws XAException { if (xid == null) { throw (XAException) new XAException(XAER_INVAL).initCause(new NullPointerException("xid")); } } // (Used via method reference only when an exceptionConverter was not supplied at construction time. This is the // default implementation.) private static XAException convert0(XARoutine xaRoutine, Exception e) { if (e == null) { return new XAException(XAER_RMERR); } else if (e instanceof XAException xaException) { return xaException; } else { Throwable cause = e.getCause(); if (cause instanceof XAException xaException) { // No matter what, if the cause was an XAException then it is canonical. return xaException; } else { SQLException sqlException; if (e instanceof SQLException s) { sqlException = s; } else if (cause instanceof SQLException s) { sqlException = s; } else { sqlException = null; } if (sqlException != null) { String sqlState = sqlException.getSQLState(); if (sqlState != null && (sqlState.startsWith("080") || sqlState.equalsIgnoreCase("08S01") // ("ess" not "five") || sqlState.equalsIgnoreCase("JZ006"))) { // Connection-related database error; might be transient; use XAER_RMFAIL instead of XAER_RMERR, // apparently. See, for example, // https://github.com/pgjdbc/pgjdbc/commit/e5aab1cd3e49051f46298d8f1fd9f66af1731299. Also see // https://github.com/pgjdbc/pgjdbc/blob/98c04a0c903e90f2d5d10a09baf1f753747b2556/pgjdbc/src/main/java/org/postgresql/xa/PGXAConnection.java#L651-L657 // and // https://github.com/pgjdbc/pgjdbc/blob/98c04a0c903e90f2d5d10a09baf1f753747b2556/pgjdbc/src/main/java/org/postgresql/xa/PGXAConnection.java#L553. Also // see // https://github.com/ironjacamar/ironjacamar/blob/ff62b8b23f59f9fbb9c15be40fef38efb872c436/core/src/main/java/org/jboss/jca/core/tx/jbossts/LocalConnectableXAResourceImpl.java#L55-L61. // // But also note XAER_RMERR vs. XAER_RMFAIL changes semantics depending on the routine (start, end, // commit, rollback, prepare, forget, recover). return (XAException) new XAException(XAER_RMFAIL).initCause(e); } } } } return (XAException) new XAException(XAER_RMERR).initCause(e); } // (Invoked only in context of a remapping BiFunction, from computeAssociation().) private static Association remap(Xid xid, Association a, EnumSet legalBranchStates, UnaryOperator remapOperator) { if (a == null) { throw new UncheckedXAException((XAException) new XAException(XAER_NOTA) .initCause(new NullPointerException("xid: " + xid + "; association: null"))); } else if (!legalBranchStates.contains(a.branchState())) { throw new UncheckedXAException((XAException) new XAException(XAER_PROTO) .initCause(new IllegalStateException("xid: " + xid + "; association: " + a))); } return remapOperator.apply(a); } // (Remapping BiFunction. Used in end() above.) private static Association activeToIdle(Xid x, Association a) { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(Association.class.getName(), "activeToIdle", new Object[] {x, a}); } a = a.activeToIdle(); if (LOGGER.isLoggable(Level.FINER)) { LOGGER.exiting(Association.class.getName(), "activeToIdle", a); } return a; } // (Remapping BiFunction. Used in end() above.) private static Association suspend(Xid x, Association a) { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(Association.class.getName(), "suspend", new Object[] {x, a}); } a = a.suspend(); if (LOGGER.isLoggable(Level.FINER)) { LOGGER.exiting(Association.class.getName(), "suspend", a); } return a; } // (Remapping BiFunction. Used in start() above.) private static Association join(Xid x, Association a) { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(Association.class.getName(), "join", new Object[] {x, a}); } if (a == null) { throw new UncheckedXAException((XAException) new XAException(XAER_NOTA) .initCause(new NullPointerException("xid: " + x + "; association: null"))); } else if (a.suspended()) { assert a.branchState() == Association.BranchState.IDLE; throw new UncheckedXAException((XAException) new XAException(XAER_PROTO) .initCause(new IllegalStateException("xid: " + x + "; association: " + a))); } switch (a.branchState()) { case ACTIVE: if (LOGGER.isLoggable(Level.FINE)) { LOGGER.logp(Level.FINE, Association.class.getName(), "join", "Joining Association ({0}) in state ACTIVE", a); } break; case IDLE: if (LOGGER.isLoggable(Level.FINE)) { LOGGER.logp(Level.FINE, Association.class.getName(), "join", "Joining Association ({0}) and transitioning it from state IDLE to state ACTIVE", a); } a = a.idleToActive(); break; default: throw new IllegalTransitionException("xid: " + x + "; association: " + a); } if (LOGGER.isLoggable(Level.FINER)) { LOGGER.exiting(Association.class.getName(), "join", a); } return a; } // (Remapping BiFunction. Used in start() above.) private static Association resume(Xid x, Association a) { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(Association.class.getName(), "resume", new Object[] {x, a}); } if (a == null) { throw new UncheckedXAException((XAException) new XAException(XAER_NOTA) .initCause(new NullPointerException("xid: " + x + "; association: null"))); } a = a.resume(); if (LOGGER.isLoggable(Level.FINER)) { LOGGER.exiting(Association.class.getName(), "resume", a); } return a; } // (Invoked during remap() above. Similar to the UnaryOperator-like methods, but not invoked via method reference.) private static Association commitAndReset(Association a, boolean onePhase) { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(Association.class.getName(), "commitAndReset", new Object[] {a, onePhase}); } assert a != null; // already vetted try { a = a.commitAndReset(onePhase); } catch (SQLException e) { throw new UncheckedSQLException(e); } catch (XAException e) { throw new UncheckedXAException(e); } assert a.branchState() == Association.BranchState.NON_EXISTENT_TRANSACTION; if (LOGGER.isLoggable(Level.FINE)) { LOGGER.logp(Level.FINE, Association.class.getName(), "commitAndReset", "Removing association {0}", a); } if (LOGGER.isLoggable(Level.FINER)) { LOGGER.exiting(Association.class.getName(), "commitAndReset", "null"); } // Critically important: remove the association. return null; } // (UnaryOperator for supplying via method reference to remap() above.) private static Association rollbackAndReset(Association a) { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(Association.class.getName(), "rollbackAndReset", a); } assert a != null; // already vetted try { a = a.rollbackAndReset(); } catch (SQLException sqlException) { throw new UncheckedSQLException(sqlException); } assert a.branchState() == Association.BranchState.NON_EXISTENT_TRANSACTION; if (LOGGER.isLoggable(Level.FINE)) { LOGGER.logp(Level.FINE, Association.class.getName(), "rollbackAndReset", "Removing association {0}", a); } if (LOGGER.isLoggable(Level.FINER)) { LOGGER.exiting(Association.class.getName(), "rollbackAndReset", "null"); } // Critically important: remove the association. return null; } // (UnaryOperator for supplying via method reference to remap() above.) private static Association prepare(Association a) { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(Association.class.getName(), "prepare", a); } assert a != null; // already vetted assert !a.suspended(); // can't be in T2 try { if (a.connection().isReadOnly()) { a = a.reset(); assert a.branchState() == Association.BranchState.NON_EXISTENT_TRANSACTION; if (LOGGER.isLoggable(Level.FINE)) { LOGGER.logp(Level.FINE, Association.class.getName(), "prepare", "Removing association {0}", a); } // Critically important: remove the association. a = null; } } catch (SQLException e) { throw new UncheckedSQLException(e); } if (LOGGER.isLoggable(Level.FINER)) { LOGGER.exiting(Association.class.getName(), "prepare", a); } return a; } // (UnaryOperator for supplying via method reference to remap() above.) private static Association forgetAndReset(Association a) { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(Association.class.getName(), "forgetAndReset", a); } assert a != null; // already vetted try { a = a.forgetAndReset(); } catch (SQLException e) { throw new UncheckedSQLException(e); } assert a.branchState() == Association.BranchState.NON_EXISTENT_TRANSACTION; if (LOGGER.isLoggable(Level.FINE)) { LOGGER.logp(Level.FINE, Association.class.getName(), "forgetAndReset", "Removing association {0}", a); } if (LOGGER.isLoggable(Level.FINER)) { LOGGER.exiting(Association.class.getName(), "forgetAndReset", "null"); } // Critically important: remove the association. return null; } private static String flagsToString(int flags) { switch (flags) { case TMENDRSCAN: return "TMENDRSCAN (" + flags + ")"; case TMFAIL: return "TMFAIL (" + flags + ")"; case TMJOIN: return "TMJOIN (" + flags + ")"; case TMNOFLAGS: return "TMNOFLAGS (" + flags + ")"; case TMONEPHASE: return "TMONEPHASE (" + flags + ")"; case TMRESUME: return "TMRESUME (" + flags + ")"; case TMSTARTRSCAN: return "TMSTARTRSCAN (" + flags + ")"; case TMSUCCESS: return "TMSUCCESS (" + flags + ")"; case TMSUSPEND: return "TMSUSPEND (" + flags + ")"; default: return String.valueOf(flags); } } /* * Inner and nested classes. */ record Association(BranchState branchState, Xid xid, boolean suspended, Connection connection, boolean priorAutoCommit) { private static final Logger LOGGER = Logger.getLogger(Association.class.getName()); // Branch Association States: (XA specification, table 6-2) // T0: Not Associated // T1: Associated // T2: Association Suspended // Branch States: (XA specification, table 6-4) // S0: Non-existent Transaction // S1: Active // S2: Idle // S3: Prepared // S4: Rollback Only // S5: Heuristically Completed Association(BranchState branchState, Xid xid, Connection connection) { this(branchState, xid, false, connection, autoCommit(connection)); } Association { Objects.requireNonNull(xid, "xid"); boolean autoCommit = false; switch (branchState) { case IDLE: break; case ACTIVE: case HEURISTICALLY_COMPLETED: case PREPARED: case ROLLBACK_ONLY: if (suspended) { throw new IllegalArgumentException("suspended"); } break; case NON_EXISTENT_TRANSACTION: if (suspended) { throw new IllegalArgumentException("suspended"); } autoCommit = priorAutoCommit; break; default: throw new IllegalArgumentException("branchState: " + branchState); } try { if (connection.getAutoCommit() != autoCommit) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.logp(Level.FINE, Association.class.getName(), "", "Setting autoCommit to {0} on connection {1}", new Object[] {autoCommit, connection}); } connection.setAutoCommit(autoCommit); } } catch (SQLException sqlException) { throw new UncheckedSQLException(sqlException); } // T0, T1 or T2; S0 or S2 } public boolean suspended() { assert this.suspended ? this.branchState() == BranchState.IDLE : true; return this.suspended; } private Association activeToIdle() { if (!this.suspended()) { switch (this.branchState()) { case ACTIVE: // OK; end(*) was called and didn't fail with an XAER_RB* code and we are not suspended // // Associated -> Associated (T1 -> T1; unchanged) // Active -> Idle (S1 -> S2) if (LOGGER.isLoggable(Level.FINE)) { LOGGER.logp(Level.FINE, Association.class.getName(), "activeToIdle", "Transitioning Association ({0}) from state ACTIVE to state IDLE", this); } return new Association(BranchState.IDLE, this.xid(), false, this.connection(), this.priorAutoCommit()); default: break; } } throw new IllegalTransitionException(this.toString()); } private Association activeToRollbackOnly() { if (!this.suspended()) { switch (this.branchState()) { case ACTIVE: // OK; end(*) was called and failed with an XAER_RB* code and we are not suspended // // Associated -> Associated (T1 -> T1; unchanged) // Active -> Rollback Only (S1 -> S4) if (LOGGER.isLoggable(Level.FINE)) { LOGGER.logp(Level.FINE, Association.class.getName(), "activeToRollbackOnly", "Transitioning Association ({0}) from state ACTIVE to state ROLLBACK_ONLY", this); } return new Association(BranchState.ROLLBACK_ONLY, this.xid(), false, this.connection(), this.priorAutoCommit()); default: break; } } throw new IllegalTransitionException(this.toString()); } private Association idleToActive() { if (!this.suspended()) { switch (this.branchState()) { case IDLE: // OK; start(TMJOIN) was called and didn't fail with an XAER_RB* code and we are not suspended // // Associated -> Associated (T1 -> T1; unchanged) // Idle -> Active (S2 -> S1) if (LOGGER.isLoggable(Level.FINE)) { LOGGER.logp(Level.FINE, Association.class.getName(), "idleToActive", "Transitioning Association ({0}) from state IDLE to state ACTIVE", this); } return new Association(BranchState.ACTIVE, this.xid(), false, this.connection(), this.priorAutoCommit()); default: break; } } throw new IllegalTransitionException(this.toString()); } private Association idleToRollbackOnly() { if (!this.suspended()) { switch (this.branchState()) { case IDLE: // OK; start(*) was called and failed with an XAER_RB* code and we are not suspended // // Associated -> Associated (T1 -> T1; unchanged) // Idle -> Rollback Only (S2 -> S4) if (LOGGER.isLoggable(Level.FINE)) { LOGGER.logp(Level.FINE, Association.class.getName(), "idleToRollbackOnly", "Transitioning Association ({0}) from state IDLE to state ROLLBACK_ONLY", this); } return new Association(BranchState.ROLLBACK_ONLY, this.xid(), false, this.connection(), this.priorAutoCommit()); default: break; } } throw new IllegalTransitionException(this.toString()); } private Association suspend() { if (!this.suspended()) { switch (this.branchState()) { case ACTIVE: // OK; end(TMSUSPEND) was called and we are not suspended // // Associated -> Association Suspended (T1 -> T2) // Active -> Idle (S1 -> S2) if (LOGGER.isLoggable(Level.FINE)) { LOGGER.logp(Level.FINE, Association.class.getName(), "suspend", "Suspending Association ({0}) and transitioning from state ACTIVE to state IDLE", this); } return new Association(BranchState.IDLE, this.xid(), true, this.connection(), this.priorAutoCommit()); default: break; } } throw new IllegalTransitionException(this.toString()); } private Association resume() { if (this.suspended()) { switch (this.branchState()) { case IDLE: // OK; start(TMRESUME) was called and we are suspended // // Association Suspended -> Associated (T2 -> T1) // Idle -> Active (S2 -> S1) if (LOGGER.isLoggable(Level.FINE)) { LOGGER.logp(Level.FINE, Association.class.getName(), "resume", "Resuming Association ({0}) from state IDLE to state ACTIVE", this); } return new Association(BranchState.ACTIVE, this.xid(), false, this.connection(), this.priorAutoCommit()); default: break; } } throw new IllegalTransitionException(this.toString()); } private Association commitAndReset(boolean onePhase) throws SQLException, XAException { Connection c = this.connection(); return this.runAndReset(c::commit, c::rollback, onePhase); } private Association rollbackAndReset() throws SQLException { try { return this.runAndReset(this.connection()::rollback, null, false); } catch (XAException e) { throw new AssertionError(e.getMessage(), e); } } private Association runAndReset(SQLRunnable r, SQLRunnable rollbackRunnable, boolean onePhaseCommit) throws SQLException, XAException { // If rollbackRunnable is non-null, then we're doing a commit. Association a; SQLException sqlException = null; try { r.run(); if (LOGGER.isLoggable(Level.FINE)) { boolean committed = rollbackRunnable != null; StringBuilder message = new StringBuilder(committed ? "Committed " : "Rolled back "); message.append("connection (").append(this.connection()).append(") "); if (committed) { message.append("with ").append(onePhaseCommit ? "one-phase " : "two-phase ").append("semantics "); } message.append("in Association ").append(this); LOGGER.logp(Level.FINE, this.getClass().getName(), "runAndReset", message.toString()); } } catch (SQLException e) { sqlException = e; if (rollbackRunnable != null) { try { rollbackRunnable.run(); if (LOGGER.isLoggable(Level.FINE)) { LOGGER.logp(Level.FINE, this.getClass().getName(), "runAndReset", "Rolled back connection ({0}) in Association {2}", new Object[] {this.connection(), this}); } if (onePhaseCommit) { // localXAResource.commit(someXid, true) caused us to try to call // someConnection.commit(). That failed, and we successfully rolled back. Now we have to // throw an XAException that indicates all this. throw (XAException) new XAException(XA_RBROLLBACK).initCause(e); } } catch (SQLException e2) { e.setNextException(e2); } } } finally { try { a = this.reset(); } catch (SQLException e) { a = null; if (sqlException == null) { sqlException = e; } else if (sqlException != e) { sqlException.setNextException(e); } } finally { if (sqlException != null) { throw sqlException; } } } return a; } private Association forgetAndReset() throws SQLException { return this.reset(); } private Association reset() throws SQLException { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.logp(Level.FINE, this.getClass().getName(), "reset", "Transitioning Association {0} from state {1} to state NON_EXISTENT_TRANSACTION", new Object[] {this, this.branchState()}); } return new Association(BranchState.NON_EXISTENT_TRANSACTION, this.xid(), false, connection, this.priorAutoCommit()); } private static boolean autoCommit(Connection c) { try { return c.getAutoCommit(); } catch (SQLException e) { throw new UncheckedSQLException(e); } } // Transaction Branch States (XA specification, table 6-4): // S0: Non-existent Transaction // S1: Active // S2: Idle // S3: Prepared // S4: Rollback Only // S5: Heuristically Completed enum BranchState { NON_EXISTENT_TRANSACTION, // S0 ACTIVE, // S1 IDLE, // S2 PREPARED, // S3 ROLLBACK_ONLY, // S4 HEURISTICALLY_COMPLETED; // S5 } } private static final class IllegalTransitionException extends IllegalStateException { private static final long serialVersionUID = 1L; private IllegalTransitionException(String message) { super(message); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy