1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.commons.dbcp2.managed;
18
19 import java.sql.Connection;
20 import java.sql.SQLException;
21 import java.util.concurrent.locks.Lock;
22 import java.util.concurrent.locks.ReentrantLock;
23
24 import org.apache.commons.dbcp2.DelegatingConnection;
25 import org.apache.commons.pool2.ObjectPool;
26
27 /**
28 * ManagedConnection is responsible for managing a database connection in a transactional environment (typically called
29 * "Container Managed"). A managed connection operates like any other connection when no global transaction (a.k.a. XA
30 * transaction or JTA Transaction) is in progress. When a global transaction is active a single physical connection to
31 * the database is used by all ManagedConnections accessed in the scope of the transaction. Connection sharing means
32 * that all data access during a transaction has a consistent view of the database. When the global transaction is
33 * committed or rolled back the enlisted connections are committed or rolled back. Typically, upon transaction
34 * completion, a connection returns to the auto commit setting in effect before being enlisted in the transaction, but
35 * some vendors do not properly implement this.
36 * <p>
37 * When enlisted in a transaction the setAutoCommit(), commit(), rollback(), and setReadOnly() methods throw a
38 * SQLException. This is necessary to assure that the transaction completes as a single unit.
39 * </p>
40 *
41 * @param <C>
42 * the Connection type
43 *
44 * @since 2.0
45 */
46 public class ManagedConnection<C extends Connection> extends DelegatingConnection<C> {
47
48 /**
49 * Delegates to {@link ManagedConnection#transactionComplete()} for transaction completion events.
50 *
51 * @since 2.0
52 */
53 protected class CompletionListener implements TransactionContextListener {
54
55 /**
56 * Constructs a new instance.
57 */
58 public CompletionListener() {
59 // empty
60 }
61
62 @Override
63 public void afterCompletion(final TransactionContext completedContext, final boolean committed) {
64 if (completedContext == transactionContext) {
65 transactionComplete();
66 }
67 }
68 }
69
70 private final ObjectPool<C> pool;
71 private final TransactionRegistry transactionRegistry;
72 private final boolean accessToUnderlyingConnectionAllowed;
73 private TransactionContext transactionContext;
74 private boolean isSharedConnection;
75 private final Lock lock;
76
77 /**
78 * Constructs a new instance responsible for managing a database connection in a transactional environment.
79 *
80 * @param pool
81 * The connection pool.
82 * @param transactionRegistry
83 * The transaction registry.
84 * @param accessToUnderlyingConnectionAllowed
85 * Whether or not to allow access to the underlying Connection.
86 * @throws SQLException
87 * Thrown when there is problem managing transactions.
88 */
89 public ManagedConnection(final ObjectPool<C> pool, final TransactionRegistry transactionRegistry,
90 final boolean accessToUnderlyingConnectionAllowed) throws SQLException {
91 super(null);
92 this.pool = pool;
93 this.transactionRegistry = transactionRegistry;
94 this.accessToUnderlyingConnectionAllowed = accessToUnderlyingConnectionAllowed;
95 this.lock = new ReentrantLock();
96 updateTransactionStatus();
97 }
98
99 @Override
100 protected void checkOpen() throws SQLException {
101 super.checkOpen();
102 updateTransactionStatus();
103 }
104
105 @Override
106 public void close() throws SQLException {
107 if (!isClosedInternal()) {
108 // Don't actually close the connection if in a transaction. The
109 // connection will be closed by the transactionComplete method.
110 //
111 // DBCP-484 we need to make sure setClosedInternal(true) being
112 // invoked if transactionContext is not null as this value will
113 // be modified by the transactionComplete method which could run
114 // in the different thread with the transaction calling back.
115 lock.lock();
116 try {
117 if (transactionContext == null || transactionContext.isTransactionComplete()) {
118 super.close();
119 }
120 } finally {
121 try {
122 setClosedInternal(true);
123 } finally {
124 lock.unlock();
125 }
126 }
127 }
128 }
129
130 @Override
131 public void commit() throws SQLException {
132 if (transactionContext != null) {
133 throw new SQLException("Commit cannot be set while enrolled in a transaction");
134 }
135 super.commit();
136 }
137
138 @Override
139 public C getDelegate() {
140 if (isAccessToUnderlyingConnectionAllowed()) {
141 return getDelegateInternal();
142 }
143 return null;
144 }
145
146 //
147 // The following methods can't be used while enlisted in a transaction
148 //
149
150 @Override
151 public Connection getInnermostDelegate() {
152 if (isAccessToUnderlyingConnectionAllowed()) {
153 return super.getInnermostDelegateInternal();
154 }
155 return null;
156 }
157
158 /**
159 * Gets the transaction context.
160 *
161 * @return The transaction context.
162 * @since 2.6.0
163 */
164 public TransactionContext getTransactionContext() {
165 return transactionContext;
166 }
167
168 /**
169 * Gets the transaction registry.
170 *
171 * @return The transaction registry.
172 * @since 2.6.0
173 */
174 public TransactionRegistry getTransactionRegistry() {
175 return transactionRegistry;
176 }
177
178 /**
179 * If false, getDelegate() and getInnermostDelegate() will return null.
180 *
181 * @return if false, getDelegate() and getInnermostDelegate() will return null
182 */
183 public boolean isAccessToUnderlyingConnectionAllowed() {
184 return accessToUnderlyingConnectionAllowed;
185 }
186
187 @Override
188 public void rollback() throws SQLException {
189 if (transactionContext != null) {
190 throw new SQLException("Commit cannot be set while enrolled in a transaction");
191 }
192 super.rollback();
193 }
194
195 @Override
196 public void setAutoCommit(final boolean autoCommit) throws SQLException {
197 if (transactionContext != null) {
198 throw new SQLException("Auto-commit cannot be set while enrolled in a transaction");
199 }
200 super.setAutoCommit(autoCommit);
201 }
202
203 @Override
204 public void setReadOnly(final boolean readOnly) throws SQLException {
205 if (transactionContext != null) {
206 throw new SQLException("Read-only cannot be set while enrolled in a transaction");
207 }
208 super.setReadOnly(readOnly);
209 }
210
211 /**
212 * Completes the transaction.
213 */
214 protected void transactionComplete() {
215 lock.lock();
216 try {
217 transactionContext.completeTransaction();
218 // If we were using a shared connection, clear the reference now that
219 // the transaction has completed
220 if (isSharedConnection) {
221 setDelegate(null);
222 isSharedConnection = false;
223 }
224 } finally {
225 lock.unlock();
226 }
227 // autoCommit may have been changed directly on the underlying connection
228 clearCachedState();
229 // If this connection was closed during the transaction and there is
230 // still a delegate present close it
231 final Connection delegate = getDelegateInternal();
232 if (isClosedInternal() && delegate != null) {
233 try {
234 setDelegate(null);
235 if (!delegate.isClosed()) {
236 delegate.close();
237 }
238 } catch (final SQLException ignored) {
239 // Not a whole lot we can do here as connection is closed
240 // and this is a transaction callback so there is no
241 // way to report the error.
242 }
243 }
244 }
245
246 private void updateTransactionStatus() throws SQLException {
247 // if there is an active transaction context, assure the transaction context hasn't changed
248 if (transactionContext != null && !transactionContext.isTransactionComplete()) {
249 if (transactionContext.isActive()) {
250 if (transactionContext != transactionRegistry.getActiveTransactionContext()) {
251 throw new SQLException("Connection cannot be used while enlisted in another transaction");
252 }
253 return;
254 }
255 // transaction should have been cleared up by TransactionContextListener, but in
256 // rare cases another lister could have registered which uses the connection before
257 // our listener is called. In that rare case, trigger the transaction complete call now
258 transactionComplete();
259 }
260 // the existing transaction context ended (or we didn't have one), get the active transaction context
261 transactionContext = transactionRegistry.getActiveTransactionContext();
262 // if there is an active transaction context, and it already has a shared connection, use it
263 if (transactionContext != null && transactionContext.getSharedConnection() != null) {
264 // A connection for the connection factory has already been enrolled
265 // in the transaction, replace our delegate with the enrolled connection
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 e) {
274 // whatever... try to invalidate the connection
275 try {
276 pool.invalidateObject(connection);
277 } catch (final Exception ignored) {
278 // no big deal
279 }
280 }
281 }
282 // add a listener to the transaction context
283 transactionContext.addTransactionContextListener(new CompletionListener());
284 // Set our delegate to the shared connection. Note that this will
285 // always be of type C since it has been shared by another
286 // connection from the same pool.
287 @SuppressWarnings("unchecked")
288 final C shared = (C) transactionContext.getSharedConnection();
289 setDelegate(shared);
290 // remember that we are using a shared connection, so it can be cleared after the
291 // transaction completes
292 isSharedConnection = true;
293 } else {
294 C connection = getDelegateInternal();
295 // if our delegate is null, create one
296 if (connection == null) {
297 try {
298 // borrow a new connection from the pool
299 connection = pool.borrowObject();
300 setDelegate(connection);
301 } catch (final Exception e) {
302 throw new SQLException("Unable to acquire a new connection from the pool", e);
303 }
304 }
305 // if we have a transaction, out delegate becomes the shared delegate
306 if (transactionContext != null) {
307 // add a listener to the transaction context
308 transactionContext.addTransactionContextListener(new CompletionListener());
309 // register our connection as the shared connection
310 try {
311 transactionContext.setSharedConnection(connection);
312 } catch (final SQLException e) {
313 // transaction is hosed
314 transactionContext = null;
315 try {
316 pool.invalidateObject(connection);
317 } catch (final Exception ignored) {
318 // we are try but no luck
319 }
320 throw e;
321 }
322 }
323 }
324 // autoCommit may have been changed directly on the underlying
325 // connection
326 clearCachedState();
327 }
328 }