001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.activemq.store.jdbc;
018
019import java.io.IOException;
020import java.sql.*;
021import java.util.LinkedList;
022import java.util.Map;
023import java.util.Properties;
024import java.util.concurrent.Executor;
025import java.util.concurrent.Executors;
026import java.util.concurrent.locks.Lock;
027import java.util.concurrent.locks.ReentrantReadWriteLock;
028
029import javax.sql.DataSource;
030
031import org.apache.activemq.util.IOExceptionSupport;
032import org.slf4j.Logger;
033import org.slf4j.LoggerFactory;
034
035/**
036 * Helps keep track of the current transaction/JDBC connection.
037 */
038public class TransactionContext {
039
040    private static final Logger LOG = LoggerFactory.getLogger(TransactionContext.class);
041
042    private final DataSource dataSource;
043    private final JDBCPersistenceAdapter persistenceAdapter;
044    private Connection connection;
045    private boolean inTx;
046    private PreparedStatement addMessageStatement;
047    private PreparedStatement removedMessageStatement;
048    private PreparedStatement updateLastAckStatement;
049    // a cheap dirty level that we can live with    
050    private int transactionIsolation = Connection.TRANSACTION_READ_UNCOMMITTED;
051    private LinkedList<Runnable> completions = new LinkedList<Runnable>();
052    private ReentrantReadWriteLock exclusiveConnectionLock = new ReentrantReadWriteLock();
053    private int networkTimeout;
054    private int queryTimeout;
055
056    public TransactionContext(JDBCPersistenceAdapter persistenceAdapter, int networkTimeout, int queryTimeout) throws IOException {
057        this.persistenceAdapter = persistenceAdapter;
058        this.dataSource = persistenceAdapter.getDataSource();
059        this.networkTimeout = networkTimeout;
060        this.queryTimeout = queryTimeout;
061    }
062
063    public Connection getExclusiveConnection() throws IOException {
064        return lockAndWrapped(exclusiveConnectionLock.writeLock());
065    }
066
067    public Connection getConnection() throws IOException {
068        return lockAndWrapped(exclusiveConnectionLock.readLock());
069    }
070
071    private Connection lockAndWrapped(Lock toLock) throws IOException {
072        if (connection == null) {
073            toLock.lock();
074            try {
075                connection = dataSource.getConnection();
076                if (networkTimeout > 0) {
077                    connection.setNetworkTimeout(Executors.newSingleThreadExecutor(), networkTimeout);
078                }
079                if (persistenceAdapter.isChangeAutoCommitAllowed()) {
080                    boolean autoCommit = !inTx;
081                    if (connection.getAutoCommit() != autoCommit) {
082                        LOG.trace("Setting auto commit to {} on connection {}", autoCommit, connection);
083                        connection.setAutoCommit(autoCommit);
084                    }
085                }
086                connection = new UnlockOnCloseConnection(connection, toLock);
087            } catch (SQLException e) {
088                JDBCPersistenceAdapter.log("Could not get JDBC connection: ", e);
089                inTx = false;
090                try {
091                    toLock.unlock();
092                } catch (IllegalMonitorStateException oops) {
093                    LOG.error("Thread does not hold the context lock on close of:"  + connection, oops);
094                }
095                close();
096                IOException ioe = IOExceptionSupport.create(e);
097                if (persistenceAdapter.getBrokerService() != null) {
098                    persistenceAdapter.getBrokerService().handleIOException(ioe);
099                }
100                throw ioe;
101            }
102
103            try {
104                connection.setTransactionIsolation(transactionIsolation);
105            } catch (Throwable e) {
106                // ignore
107                LOG.trace("Cannot set transaction isolation to " + transactionIsolation + " due " + e.getMessage()
108                        + ". This exception is ignored.", e);
109            }
110        }
111        return connection;
112    }
113
114    public void executeBatch() throws SQLException {
115        try {
116            executeBatch(addMessageStatement, "Failed add a message");
117        } finally {
118            addMessageStatement = null;
119            try {
120                executeBatch(removedMessageStatement, "Failed to remove a message");
121            } finally {
122                removedMessageStatement = null;
123                try {
124                    executeBatch(updateLastAckStatement, "Failed to ack a message");
125                } finally {
126                    updateLastAckStatement = null;
127                }
128            }
129        }
130    }
131
132    private void executeBatch(PreparedStatement p, String message) throws SQLException {
133        if (p == null) {
134            return;
135        }
136
137        try {
138            int[] rc = p.executeBatch();
139            for (int i = 0; i < rc.length; i++) {
140                int code = rc[i];
141                if (code < 0 && code != Statement.SUCCESS_NO_INFO) {
142                    throw new SQLException(message + ". Response code: " + code);
143                }
144            }
145        } finally {
146            try {
147                p.close();
148            } catch (Throwable e) {
149            }
150        }
151    }
152
153    public void close() throws IOException {
154        if (!inTx) {
155            try {
156
157                /**
158                 * we are not in a transaction so should not be committing ??
159                 * This was previously commented out - but had adverse affects
160                 * on testing - so it's back!
161                 * 
162                 */
163                try {
164                    executeBatch();
165                } finally {
166                    if (connection != null && !connection.getAutoCommit()) {
167                        connection.commit();
168                    }
169                }
170
171            } catch (SQLException e) {
172                JDBCPersistenceAdapter.log("Error while closing connection: ", e);
173                IOException ioe = IOExceptionSupport.create(e);
174                persistenceAdapter.getBrokerService().handleIOException(ioe);
175                throw ioe;
176            } finally {
177                try {
178                    if (connection != null) {
179                        connection.close();
180                    }
181                } catch (Throwable e) {
182                    // ignore
183                    LOG.trace("Closing connection failed due: " + e.getMessage() + ". This exception is ignored.", e);
184                } finally {
185                    connection = null;
186                }
187                for (Runnable completion: completions) {
188                    completion.run();
189                }
190                completions.clear();
191            }
192        }
193    }
194
195    public void begin() throws IOException {
196        if (inTx) {
197            throw new IOException("Already started.");
198        }
199        inTx = true;
200        connection = getConnection();
201    }
202
203    public void commit() throws IOException {
204        if (!inTx) {
205            throw new IOException("Not started.");
206        }
207        try {
208            executeBatch();
209            if (!connection.getAutoCommit()) {
210                connection.commit();
211            }
212        } catch (SQLException e) {
213            JDBCPersistenceAdapter.log("Commit failed: ", e);
214            try {
215                doRollback();
216            } catch (Exception ignored) {}
217            IOException ioe = IOExceptionSupport.create(e);
218            persistenceAdapter.getBrokerService().handleIOException(ioe);
219            throw ioe;
220        } finally {
221            inTx = false;
222            close();
223        }
224    }
225
226    public void rollback() throws IOException {
227        if (!inTx) {
228            throw new IOException("Not started.");
229        }
230        try {
231            doRollback();
232        } catch (SQLException e) {
233            JDBCPersistenceAdapter.log("Rollback failed: ", e);
234            throw IOExceptionSupport.create(e);
235        } finally {
236            inTx = false;
237            close();
238        }
239    }
240
241    private void doRollback() throws SQLException {
242        if (addMessageStatement != null) {
243            addMessageStatement.close();
244            addMessageStatement = null;
245        }
246        if (removedMessageStatement != null) {
247            removedMessageStatement.close();
248            removedMessageStatement = null;
249        }
250        if (updateLastAckStatement != null) {
251            updateLastAckStatement.close();
252            updateLastAckStatement = null;
253        }
254        connection.rollback();
255    }
256
257    public PreparedStatement getAddMessageStatement() {
258        return addMessageStatement;
259    }
260
261    public void setAddMessageStatement(PreparedStatement addMessageStatement) {
262        this.addMessageStatement = addMessageStatement;
263    }
264
265    public PreparedStatement getUpdateLastAckStatement() {
266        return updateLastAckStatement;
267    }
268
269    public void setUpdateLastAckStatement(PreparedStatement ackMessageStatement) {
270        this.updateLastAckStatement = ackMessageStatement;
271    }
272
273    public PreparedStatement getRemovedMessageStatement() {
274        return removedMessageStatement;
275    }
276
277    public void setRemovedMessageStatement(PreparedStatement removedMessageStatement) {
278        this.removedMessageStatement = removedMessageStatement;
279    }
280    
281    public void setTransactionIsolation(int transactionIsolation) {
282        this.transactionIsolation = transactionIsolation;
283    }
284
285    public void onCompletion(Runnable runnable) {
286        completions.add(runnable);
287    }
288
289    final private class UnlockOnCloseConnection implements Connection {
290
291        private final Connection delegate;
292        private final Lock lock;
293
294        UnlockOnCloseConnection(Connection delegate, Lock toUnlockOnClose) {
295            this.delegate = delegate;
296            this.lock = toUnlockOnClose;
297        }
298
299        @Override
300        public void close() throws SQLException {
301            try {
302                delegate.close();
303            } finally {
304                lock.unlock();
305            }
306        }
307
308        // simple delegate for the  rest of the impl..
309        @Override
310        public Statement createStatement() throws SQLException {
311            Statement statement = delegate.createStatement();
312            if (queryTimeout > 0) {
313                statement.setQueryTimeout(queryTimeout);
314            }
315            return statement;
316        }
317
318        @Override
319        public PreparedStatement prepareStatement(String sql) throws SQLException {
320            PreparedStatement statement = delegate.prepareStatement(sql);
321            if (queryTimeout > 0) {
322                statement.setQueryTimeout(queryTimeout);
323            }
324            return statement;
325        }
326
327        @Override
328        public CallableStatement prepareCall(String sql) throws SQLException {
329            CallableStatement statement = delegate.prepareCall(sql);
330            if (queryTimeout > 0) {
331                statement.setQueryTimeout(queryTimeout);
332            }
333            return statement;
334        }
335
336        @Override
337        public String nativeSQL(String sql) throws SQLException {
338            return delegate.nativeSQL(sql);
339        }
340
341        @Override
342        public void setAutoCommit(boolean autoCommit) throws SQLException {
343            delegate.setAutoCommit(autoCommit);
344        }
345
346        @Override
347        public boolean getAutoCommit() throws SQLException {
348            return delegate.getAutoCommit();
349        }
350
351        @Override
352        public void commit() throws SQLException {
353            delegate.commit();
354        }
355
356        @Override
357        public void rollback() throws SQLException {
358            delegate.rollback();
359        }
360
361        @Override
362        public boolean isClosed() throws SQLException {
363            return delegate.isClosed();
364        }
365
366        @Override
367        public DatabaseMetaData getMetaData() throws SQLException {
368            return delegate.getMetaData();
369        }
370
371        @Override
372        public void setReadOnly(boolean readOnly) throws SQLException {
373            delegate.setReadOnly(readOnly);
374        }
375
376        @Override
377        public boolean isReadOnly() throws SQLException {
378            return delegate.isReadOnly();
379        }
380
381        @Override
382        public void setCatalog(String catalog) throws SQLException {
383            delegate.setCatalog(catalog);
384        }
385
386        @Override
387        public String getCatalog() throws SQLException {
388            return delegate.getCatalog();
389        }
390
391        @Override
392        public void setTransactionIsolation(int level) throws SQLException {
393            delegate.setTransactionIsolation(level);
394        }
395
396        @Override
397        public int getTransactionIsolation() throws SQLException {
398            return delegate.getTransactionIsolation();
399        }
400
401        @Override
402        public SQLWarning getWarnings() throws SQLException {
403            return delegate.getWarnings();
404        }
405
406        @Override
407        public void clearWarnings() throws SQLException {
408            delegate.clearWarnings();
409        }
410
411        @Override
412        public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
413            Statement statement = delegate.createStatement(resultSetType, resultSetConcurrency);
414            if (queryTimeout > 0) {
415                statement.setQueryTimeout(queryTimeout);
416            }
417            return statement;
418        }
419
420        @Override
421        public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
422            PreparedStatement statement = delegate.prepareStatement(sql, resultSetType, resultSetConcurrency);
423            if (queryTimeout > 0) {
424                statement.setQueryTimeout(queryTimeout);
425            }
426            return statement;
427        }
428
429        @Override
430        public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
431            CallableStatement statement = delegate.prepareCall(sql, resultSetType, resultSetConcurrency);
432            if (queryTimeout > 0) {
433                statement.setQueryTimeout(queryTimeout);
434            }
435            return statement;
436        }
437
438        @Override
439        public Map<String, Class<?>> getTypeMap() throws SQLException {
440            return delegate.getTypeMap();
441        }
442
443        @Override
444        public void setTypeMap(Map<String, Class<?>> map) throws SQLException {
445            delegate.setTypeMap(map);
446        }
447
448        @Override
449        public void setHoldability(int holdability) throws SQLException {
450            delegate.setHoldability(holdability);
451        }
452
453        @Override
454        public int getHoldability() throws SQLException {
455            return delegate.getHoldability();
456        }
457
458        @Override
459        public Savepoint setSavepoint() throws SQLException {
460            return delegate.setSavepoint();
461        }
462
463        @Override
464        public Savepoint setSavepoint(String name) throws SQLException {
465            return delegate.setSavepoint(name);
466        }
467
468        @Override
469        public void rollback(Savepoint savepoint) throws SQLException {
470            delegate.rollback(savepoint);
471        }
472
473        @Override
474        public void releaseSavepoint(Savepoint savepoint) throws SQLException {
475            delegate.releaseSavepoint(savepoint);
476        }
477
478        @Override
479        public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
480            Statement statement = delegate.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability);
481            if (queryTimeout > 0) {
482                statement.setQueryTimeout(queryTimeout);
483            }
484            return statement;
485        }
486
487        @Override
488        public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
489            PreparedStatement statement = delegate.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability);
490            if (queryTimeout > 0) {
491                statement.setQueryTimeout(queryTimeout);
492            }
493            return statement;
494        }
495
496        @Override
497        public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
498            CallableStatement statement = delegate.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability);
499            if (queryTimeout > 0) {
500                statement.setQueryTimeout(queryTimeout);
501            }
502            return statement;
503        }
504
505        @Override
506        public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException {
507            PreparedStatement statement = delegate.prepareStatement(sql, autoGeneratedKeys);
508            if (queryTimeout > 0) {
509                statement.setQueryTimeout(queryTimeout);
510            }
511            return statement;
512        }
513
514        @Override
515        public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException {
516            PreparedStatement statement = delegate.prepareStatement(sql, columnIndexes);
517            if (queryTimeout > 0) {
518                statement.setQueryTimeout(queryTimeout);
519            }
520            return statement;
521        }
522
523        @Override
524        public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
525            PreparedStatement statement = delegate.prepareStatement(sql, columnNames);
526            if (queryTimeout > 0) {
527                statement.setQueryTimeout(queryTimeout);
528            }
529            return statement;
530        }
531
532        @Override
533        public Clob createClob() throws SQLException {
534            return delegate.createClob();
535        }
536
537        @Override
538        public Blob createBlob() throws SQLException {
539            return delegate.createBlob();
540        }
541
542        @Override
543        public NClob createNClob() throws SQLException {
544            return delegate.createNClob();
545        }
546
547        @Override
548        public SQLXML createSQLXML() throws SQLException {
549            return delegate.createSQLXML();
550        }
551
552        @Override
553        public boolean isValid(int timeout) throws SQLException {
554            return delegate.isValid(timeout);
555        }
556
557        @Override
558        public void setClientInfo(String name, String value) throws SQLClientInfoException {
559            delegate.setClientInfo(name, value);
560        }
561
562        @Override
563        public void setClientInfo(Properties properties) throws SQLClientInfoException {
564            delegate.setClientInfo(properties);
565        }
566
567        @Override
568        public String getClientInfo(String name) throws SQLException {
569            return delegate.getClientInfo(name);
570        }
571
572        @Override
573        public Properties getClientInfo() throws SQLException {
574            return delegate.getClientInfo();
575        }
576
577        @Override
578        public Array createArrayOf(String typeName, Object[] elements) throws SQLException {
579            return delegate.createArrayOf(typeName, elements);
580        }
581
582        @Override
583        public Struct createStruct(String typeName, Object[] attributes) throws SQLException {
584            return delegate.createStruct(typeName, attributes);
585        }
586
587        @Override
588        public void setSchema(String schema) throws SQLException {
589            delegate.setSchema(schema);
590        }
591
592        @Override
593        public String getSchema() throws SQLException {
594            return delegate.getSchema();
595        }
596
597        @Override
598        public void abort(Executor executor) throws SQLException {
599            delegate.abort(executor);
600        }
601
602        @Override
603        public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException {
604            delegate.setNetworkTimeout(executor, milliseconds);
605        }
606
607        @Override
608        public int getNetworkTimeout() throws SQLException {
609            return delegate.getNetworkTimeout();
610        }
611
612        @Override
613        public <T> T unwrap(Class<T> iface) throws SQLException {
614            return delegate.unwrap(iface);
615        }
616
617        @Override
618        public boolean isWrapperFor(Class<?> iface) throws SQLException {
619            return delegate.isWrapperFor(iface);
620        }
621    }
622}