001/* 002 003 Licensed to the Apache Software Foundation (ASF) under one or more 004 contributor license agreements. See the NOTICE file distributed with 005 this work for additional information regarding copyright ownership. 006 The ASF licenses this file to You under the Apache License, Version 2.0 007 (the "License"); you may not use this file except in compliance with 008 the License. You may obtain a copy of the License at 009 010 http://www.apache.org/licenses/LICENSE-2.0 011 012 Unless required by applicable law or agreed to in writing, software 013 distributed under the License is distributed on an "AS IS" BASIS, 014 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 See the License for the specific language governing permissions and 016 limitations under the License. 017 */ 018package org.apache.commons.dbcp2.managed; 019 020import java.sql.Connection; 021import java.sql.SQLException; 022import java.util.concurrent.locks.Lock; 023import java.util.concurrent.locks.ReentrantLock; 024 025import org.apache.commons.dbcp2.DelegatingConnection; 026import org.apache.commons.pool2.ObjectPool; 027 028/** 029 * ManagedConnection is responsible for managing a database connection in a transactional environment (typically called 030 * "Container Managed"). A managed connection operates like any other connection when no global transaction (a.k.a. XA 031 * transaction or JTA Transaction) is in progress. When a global transaction is active a single physical connection to 032 * the database is used by all ManagedConnections accessed in the scope of the transaction. Connection sharing means 033 * that all data access during a transaction has a consistent view of the database. When the global transaction is 034 * committed or rolled back the enlisted connections are committed or rolled back. Typically upon transaction 035 * completion, a connection returns to the auto commit setting in effect before being enlisted in the transaction, but 036 * some vendors do not properly implement this. 037 * <p> 038 * When enlisted in a transaction the setAutoCommit(), commit(), rollback(), and setReadOnly() methods throw a 039 * SQLException. This is necessary to assure that the transaction completes as a single unit. 040 * </p> 041 * 042 * @param <C> 043 * the Connection type 044 * 045 * @since 2.0 046 */ 047public class ManagedConnection<C extends Connection> extends DelegatingConnection<C> { 048 049 /** 050 * Delegates to {@link ManagedConnection#transactionComplete()} for transaction completion events. 051 * 052 * @since 2.0 053 */ 054 protected class CompletionListener implements TransactionContextListener { 055 @Override 056 public void afterCompletion(final TransactionContext completedContext, final boolean committed) { 057 if (completedContext == transactionContext) { 058 transactionComplete(); 059 } 060 } 061 } 062 private final ObjectPool<C> pool; 063 private final TransactionRegistry transactionRegistry; 064 private final boolean accessToUnderlyingConnectionAllowed; 065 private TransactionContext transactionContext; 066 private boolean isSharedConnection; 067 068 private final Lock lock; 069 070 /** 071 * Constructs a new instance responsible for managing a database connection in a transactional environment. 072 * 073 * @param pool 074 * The connection pool. 075 * @param transactionRegistry 076 * The transaction registry. 077 * @param accessToUnderlyingConnectionAllowed 078 * Whether or not to allow access to the underlying Connection. 079 * @throws SQLException 080 * Thrown when there is problem managing transactions. 081 */ 082 public ManagedConnection(final ObjectPool<C> pool, final TransactionRegistry transactionRegistry, 083 final boolean accessToUnderlyingConnectionAllowed) throws SQLException { 084 super(null); 085 this.pool = pool; 086 this.transactionRegistry = transactionRegistry; 087 this.accessToUnderlyingConnectionAllowed = accessToUnderlyingConnectionAllowed; 088 this.lock = new ReentrantLock(); 089 updateTransactionStatus(); 090 } 091 092 @Override 093 protected void checkOpen() throws SQLException { 094 super.checkOpen(); 095 updateTransactionStatus(); 096 } 097 098 @Override 099 public void close() throws SQLException { 100 if (!isClosedInternal()) { 101 // Don't actually close the connection if in a transaction. The 102 // connection will be closed by the transactionComplete method. 103 // 104 // DBCP-484 we need to make sure setClosedInternal(true) being 105 // invoked if transactionContext is not null as this value will 106 // be modified by the transactionComplete method which could run 107 // in the different thread with the transaction calling back. 108 lock.lock(); 109 try { 110 if (transactionContext == null || transactionContext.isTransactionComplete()) { 111 super.close(); 112 } 113 } finally { 114 try { 115 setClosedInternal(true); 116 } finally { 117 lock.unlock(); 118 } 119 } 120 } 121 } 122 123 @Override 124 public void commit() throws SQLException { 125 if (transactionContext != null) { 126 throw new SQLException("Commit can not be set while enrolled in a transaction"); 127 } 128 super.commit(); 129 } 130 131 @Override 132 public C getDelegate() { 133 if (isAccessToUnderlyingConnectionAllowed()) { 134 return getDelegateInternal(); 135 } 136 return null; 137 } 138 139 // 140 // The following methods can't be used while enlisted in a transaction 141 // 142 143 @Override 144 public Connection getInnermostDelegate() { 145 if (isAccessToUnderlyingConnectionAllowed()) { 146 return super.getInnermostDelegateInternal(); 147 } 148 return null; 149 } 150 151 /** 152 * @return The transaction context. 153 * @since 2.6.0 154 */ 155 public TransactionContext getTransactionContext() { 156 return transactionContext; 157 } 158 159 /** 160 * @return The transaction registry. 161 * @since 2.6.0 162 */ 163 public TransactionRegistry getTransactionRegistry() { 164 return transactionRegistry; 165 } 166 167 /** 168 * If false, getDelegate() and getInnermostDelegate() will return null. 169 * 170 * @return if false, getDelegate() and getInnermostDelegate() will return null 171 */ 172 public boolean isAccessToUnderlyingConnectionAllowed() { 173 return accessToUnderlyingConnectionAllowed; 174 } 175 176 // 177 // Methods for accessing the delegate connection 178 // 179 180 @Override 181 public void rollback() throws SQLException { 182 if (transactionContext != null) { 183 throw new SQLException("Commit can not be set while enrolled in a transaction"); 184 } 185 super.rollback(); 186 } 187 188 @Override 189 public void setAutoCommit(final boolean autoCommit) throws SQLException { 190 if (transactionContext != null) { 191 throw new SQLException("Auto-commit can not be set while enrolled in a transaction"); 192 } 193 super.setAutoCommit(autoCommit); 194 } 195 196 @Override 197 public void setReadOnly(final boolean readOnly) throws SQLException { 198 if (transactionContext != null) { 199 throw new SQLException("Read-only can not be set while enrolled in a transaction"); 200 } 201 super.setReadOnly(readOnly); 202 } 203 204 /** 205 * Completes the transaction. 206 */ 207 protected void transactionComplete() { 208 lock.lock(); 209 try { 210 transactionContext.completeTransaction(); 211 } finally { 212 lock.unlock(); 213 } 214 215 // If we were using a shared connection, clear the reference now that 216 // the transaction has completed 217 if (isSharedConnection) { 218 setDelegate(null); 219 isSharedConnection = false; 220 } 221 222 // autoCommit may have been changed directly on the underlying connection 223 clearCachedState(); 224 225 // If this connection was closed during the transaction and there is 226 // still a delegate present close it 227 final Connection delegate = getDelegateInternal(); 228 if (isClosedInternal() && delegate != null) { 229 try { 230 setDelegate(null); 231 232 if (!delegate.isClosed()) { 233 delegate.close(); 234 } 235 } catch (final SQLException ignored) { 236 // Not a whole lot we can do here as connection is closed 237 // and this is a transaction callback so there is no 238 // way to report the error. 239 } 240 } 241 } 242 243 private void updateTransactionStatus() throws SQLException { 244 // if there is a is an active transaction context, assure the transaction context hasn't changed 245 if (transactionContext != null && !transactionContext.isTransactionComplete()) { 246 if (transactionContext.isActive()) { 247 if (transactionContext != transactionRegistry.getActiveTransactionContext()) { 248 throw new SQLException("Connection can not be used while enlisted in another transaction"); 249 } 250 return; 251 } 252 // transaction should have been cleared up by TransactionContextListener, but in 253 // rare cases another lister could have registered which uses the connection before 254 // our listener is called. In that rare case, trigger the transaction complete call now 255 transactionComplete(); 256 } 257 258 // the existing transaction context ended (or we didn't have one), get the active transaction context 259 transactionContext = transactionRegistry.getActiveTransactionContext(); 260 261 // if there is an active transaction context and it already has a shared connection, use it 262 if (transactionContext != null && transactionContext.getSharedConnection() != null) { 263 // A connection for the connection factory has already been enrolled 264 // in the transaction, replace our delegate with the enrolled connection 265 266 // return current connection to the pool 267 @SuppressWarnings("resource") 268 final C connection = getDelegateInternal(); 269 setDelegate(null); 270 if (connection != null && transactionContext.getSharedConnection() != connection) { 271 try { 272 pool.returnObject(connection); 273 } catch (final Exception ignored) { 274 // whatever... try to invalidate the connection 275 try { 276 pool.invalidateObject(connection); 277 } catch (final Exception ignore) { 278 // no big deal 279 } 280 } 281 } 282 283 // add a listener to the transaction context 284 transactionContext.addTransactionContextListener(new CompletionListener()); 285 286 // Set our delegate to the shared connection. Note that this will 287 // always be of type C since it has been shared by another 288 // connection from the same pool. 289 @SuppressWarnings("unchecked") 290 final C shared = (C) transactionContext.getSharedConnection(); 291 setDelegate(shared); 292 293 // remember that we are using a shared connection so it can be cleared after the 294 // transaction completes 295 isSharedConnection = true; 296 } else { 297 C connection = getDelegateInternal(); 298 // if our delegate is null, create one 299 if (connection == null) { 300 try { 301 // borrow a new connection from the pool 302 connection = pool.borrowObject(); 303 setDelegate(connection); 304 } catch (final Exception e) { 305 throw new SQLException("Unable to acquire a new connection from the pool", e); 306 } 307 } 308 309 // if we have a transaction, out delegate becomes the shared delegate 310 if (transactionContext != null) { 311 // add a listener to the transaction context 312 transactionContext.addTransactionContextListener(new CompletionListener()); 313 314 // register our connection as the shared connection 315 try { 316 transactionContext.setSharedConnection(connection); 317 } catch (final SQLException e) { 318 // transaction is hosed 319 transactionContext = null; 320 try { 321 pool.invalidateObject(connection); 322 } catch (final Exception e1) { 323 // we are try but no luck 324 } 325 throw e; 326 } 327 } 328 } 329 // autoCommit may have been changed directly on the underlying 330 // connection 331 clearCachedState(); 332 } 333}