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.File;
020import java.io.IOException;
021import java.sql.Connection;
022import java.sql.SQLException;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.Locale;
026import java.util.Set;
027import java.util.concurrent.ScheduledFuture;
028import java.util.concurrent.ScheduledThreadPoolExecutor;
029import java.util.concurrent.ThreadFactory;
030import java.util.concurrent.TimeUnit;
031
032import javax.sql.DataSource;
033
034import org.apache.activemq.ActiveMQMessageAudit;
035import org.apache.activemq.broker.BrokerService;
036import org.apache.activemq.broker.ConnectionContext;
037import org.apache.activemq.broker.Locker;
038import org.apache.activemq.broker.scheduler.JobSchedulerStore;
039import org.apache.activemq.command.ActiveMQDestination;
040import org.apache.activemq.command.ActiveMQQueue;
041import org.apache.activemq.command.ActiveMQTopic;
042import org.apache.activemq.command.Message;
043import org.apache.activemq.command.MessageAck;
044import org.apache.activemq.command.MessageId;
045import org.apache.activemq.command.ProducerId;
046import org.apache.activemq.openwire.OpenWireFormat;
047import org.apache.activemq.store.MessageStore;
048import org.apache.activemq.store.PersistenceAdapter;
049import org.apache.activemq.store.TopicMessageStore;
050import org.apache.activemq.store.TransactionStore;
051import org.apache.activemq.store.jdbc.adapter.DefaultJDBCAdapter;
052import org.apache.activemq.store.memory.MemoryTransactionStore;
053import org.apache.activemq.usage.SystemUsage;
054import org.apache.activemq.util.ByteSequence;
055import org.apache.activemq.util.FactoryFinder;
056import org.apache.activemq.util.IOExceptionSupport;
057import org.apache.activemq.util.LongSequenceGenerator;
058import org.apache.activemq.util.ServiceStopper;
059import org.apache.activemq.util.ThreadPoolUtils;
060import org.apache.activemq.wireformat.WireFormat;
061import org.slf4j.Logger;
062import org.slf4j.LoggerFactory;
063
064/**
065 * A {@link PersistenceAdapter} implementation using JDBC for persistence
066 * storage.
067 *
068 * This persistence adapter will correctly remember prepared XA transactions,
069 * but it will not keep track of local transaction commits so that operations
070 * performed against the Message store are done as a single uow.
071 *
072 * @org.apache.xbean.XBean element="jdbcPersistenceAdapter"
073 *
074 */
075public class JDBCPersistenceAdapter extends DataSourceServiceSupport implements PersistenceAdapter {
076
077    private static final Logger LOG = LoggerFactory.getLogger(JDBCPersistenceAdapter.class);
078    private static FactoryFinder adapterFactoryFinder = new FactoryFinder(
079        "META-INF/services/org/apache/activemq/store/jdbc/");
080    private static FactoryFinder lockFactoryFinder = new FactoryFinder(
081        "META-INF/services/org/apache/activemq/store/jdbc/lock/");
082
083    public static final long DEFAULT_LOCK_KEEP_ALIVE_PERIOD = 30 * 1000;
084
085    private WireFormat wireFormat = new OpenWireFormat();
086    private Statements statements;
087    private JDBCAdapter adapter;
088    private final JdbcMemoryTransactionStore transactionStore = new JdbcMemoryTransactionStore(this);
089    private ScheduledFuture<?> cleanupTicket;
090    private int cleanupPeriod = 1000 * 60 * 5;
091    private boolean useExternalMessageReferences;
092    private boolean createTablesOnStartup = true;
093    private DataSource lockDataSource;
094    private int transactionIsolation;
095    private File directory;
096    private boolean changeAutoCommitAllowed = true;
097    private int queryTimeout = -1;
098    private int networkTimeout = -1;
099
100    protected int maxProducersToAudit=1024;
101    protected int maxAuditDepth=1000;
102    protected boolean enableAudit=false;
103    protected int auditRecoveryDepth = 1024;
104    protected ActiveMQMessageAudit audit;
105
106    protected LongSequenceGenerator sequenceGenerator = new LongSequenceGenerator();
107    protected int maxRows = DefaultJDBCAdapter.MAX_ROWS;
108    protected final HashMap<ActiveMQDestination, MessageStore> storeCache = new HashMap<>();
109
110    {
111        setLockKeepAlivePeriod(DEFAULT_LOCK_KEEP_ALIVE_PERIOD);
112    }
113
114    public JDBCPersistenceAdapter() {
115    }
116
117    public JDBCPersistenceAdapter(DataSource ds, WireFormat wireFormat) {
118        super(ds);
119        this.wireFormat = wireFormat;
120    }
121
122    @Override
123    public Set<ActiveMQDestination> getDestinations() {
124        TransactionContext c = null;
125        try {
126            c = getTransactionContext();
127            return getAdapter().doGetDestinations(c);
128        } catch (IOException e) {
129            return emptyDestinationSet();
130        } catch (SQLException e) {
131            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
132            return emptyDestinationSet();
133        } finally {
134            if (c != null) {
135                try {
136                    c.close();
137                } catch (Throwable e) {
138                }
139            }
140        }
141    }
142
143    @SuppressWarnings("unchecked")
144    private Set<ActiveMQDestination> emptyDestinationSet() {
145        return Collections.EMPTY_SET;
146    }
147
148    protected void createMessageAudit() {
149        if (enableAudit && audit == null) {
150            audit = new ActiveMQMessageAudit(maxAuditDepth,maxProducersToAudit);
151            TransactionContext c = null;
152
153            try {
154                c = getTransactionContext();
155                getAdapter().doMessageIdScan(c, auditRecoveryDepth, new JDBCMessageIdScanListener() {
156                    @Override
157                    public void messageId(MessageId id) {
158                        audit.isDuplicate(id);
159                    }
160                });
161            } catch (Exception e) {
162                LOG.error("Failed to reload store message audit for JDBC persistence adapter", e);
163            } finally {
164                if (c != null) {
165                    try {
166                        c.close();
167                    } catch (Throwable e) {
168                    }
169                }
170            }
171        }
172    }
173
174    public void initSequenceIdGenerator() {
175        TransactionContext c = null;
176        try {
177            c = getTransactionContext();
178            getAdapter().doMessageIdScan(c, auditRecoveryDepth, new JDBCMessageIdScanListener() {
179                @Override
180                public void messageId(MessageId id) {
181                    audit.isDuplicate(id);
182                }
183            });
184        } catch (Exception e) {
185            LOG.error("Failed to reload store message audit for JDBC persistence adapter", e);
186        } finally {
187            if (c != null) {
188                try {
189                    c.close();
190                } catch (Throwable e) {
191                }
192            }
193        }
194    }
195
196    @Override
197    public MessageStore createQueueMessageStore(ActiveMQQueue destination) throws IOException {
198        MessageStore rc = storeCache.get(destination);
199        if (rc == null) {
200            MessageStore store = transactionStore.proxy(new JDBCMessageStore(this, getAdapter(), wireFormat, destination, audit));
201            rc = storeCache.putIfAbsent(destination, store);
202            if (rc == null) {
203                rc = store;
204            }
205        }
206        return rc;
207    }
208
209    @Override
210    public TopicMessageStore createTopicMessageStore(ActiveMQTopic destination) throws IOException {
211        TopicMessageStore rc = (TopicMessageStore) storeCache.get(destination);
212        if (rc == null) {
213            TopicMessageStore store = transactionStore.proxy(new JDBCTopicMessageStore(this, getAdapter(), wireFormat, destination, audit));
214            rc = (TopicMessageStore) storeCache.putIfAbsent(destination, store);
215            if (rc == null) {
216                rc = store;
217            }
218        }
219        return rc;
220    }
221
222    /**
223     * Cleanup method to remove any state associated with the given destination
224     * @param destination Destination to forget
225     */
226    @Override
227    public void removeQueueMessageStore(ActiveMQQueue destination) {
228        if (destination.isQueue() && getBrokerService().shouldRecordVirtualDestination(destination)) {
229            try {
230                removeConsumerDestination(destination);
231            } catch (IOException ioe) {
232                LOG.error("Failed to remove consumer destination: " + destination, ioe);
233            }
234        }
235        storeCache.remove(destination);
236    }
237
238    private void removeConsumerDestination(ActiveMQQueue destination) throws IOException {
239        TransactionContext c = getTransactionContext();
240        try {
241            String id = destination.getQualifiedName();
242            getAdapter().doDeleteSubscription(c, destination, id, id);
243        } catch (SQLException e) {
244            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
245            throw IOExceptionSupport.create("Failed to remove consumer destination: " + destination, e);
246        } finally {
247            c.close();
248        }
249    }
250
251    /**
252     * Cleanup method to remove any state associated with the given destination
253     * No state retained.... nothing to do
254     *
255     * @param destination Destination to forget
256     */
257    @Override
258    public void removeTopicMessageStore(ActiveMQTopic destination) {
259        storeCache.remove(destination);
260    }
261
262    @Override
263    public TransactionStore createTransactionStore() throws IOException {
264        return this.transactionStore;
265    }
266
267    @Override
268    public long getLastMessageBrokerSequenceId() throws IOException {
269        TransactionContext c = getTransactionContext();
270        try {
271            long seq =  getAdapter().doGetLastMessageStoreSequenceId(c);
272            sequenceGenerator.setLastSequenceId(seq);
273            long brokerSeq = 0;
274            if (seq != 0) {
275                byte[] msg = getAdapter().doGetMessageById(c, seq);
276                if (msg != null) {
277                    Message last = (Message)wireFormat.unmarshal(new ByteSequence(msg));
278                    brokerSeq = last.getMessageId().getBrokerSequenceId();
279                } else {
280                   LOG.warn("Broker sequence id wasn't recovered properly, possible duplicates!");
281                }
282            }
283            return brokerSeq;
284        } catch (SQLException e) {
285            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
286            throw IOExceptionSupport.create("Failed to get last broker message id: " + e, e);
287        } finally {
288            c.close();
289        }
290    }
291
292    @Override
293    public long getLastProducerSequenceId(ProducerId id) throws IOException {
294        TransactionContext c = getTransactionContext();
295        try {
296            return getAdapter().doGetLastProducerSequenceId(c, id);
297        } catch (SQLException e) {
298            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
299            throw IOExceptionSupport.create("Failed to get last broker message id: " + e, e);
300        } finally {
301            c.close();
302        }
303    }
304
305    @Override
306    public void allowIOResumption() {}
307
308    @Override
309    public void init() throws Exception {
310        getAdapter().setUseExternalMessageReferences(isUseExternalMessageReferences());
311
312        if (isCreateTablesOnStartup()) {
313            TransactionContext transactionContext = getTransactionContext();
314            transactionContext.getExclusiveConnection();
315            transactionContext.begin();
316            try {
317                try {
318                    getAdapter().doCreateTables(transactionContext);
319                } catch (SQLException e) {
320                    LOG.warn("Cannot create tables due to: " + e);
321                    JDBCPersistenceAdapter.log("Failure Details: ", e);
322                }
323            } finally {
324                transactionContext.commit();
325            }
326        }
327    }
328
329    @Override
330    public void doStart() throws Exception {
331
332        if( brokerService!=null ) {
333          wireFormat.setVersion(brokerService.getStoreOpenWireVersion());
334        }
335
336        // Cleanup the db periodically.
337        if (cleanupPeriod > 0) {
338            cleanupTicket = getScheduledThreadPoolExecutor().scheduleWithFixedDelay(new Runnable() {
339                @Override
340                public void run() {
341                    cleanup();
342                }
343            }, 0, cleanupPeriod, TimeUnit.MILLISECONDS);
344        }
345        createMessageAudit();
346    }
347
348    @Override
349    public synchronized void doStop(ServiceStopper stopper) throws Exception {
350        if (cleanupTicket != null) {
351            cleanupTicket.cancel(true);
352            cleanupTicket = null;
353        }
354        closeDataSource(getDataSource());
355    }
356
357    public void cleanup() {
358        TransactionContext c = null;
359        try {
360            LOG.debug("Cleaning up old messages.");
361            c = getTransactionContext();
362            c.getExclusiveConnection();
363            getAdapter().doDeleteOldMessages(c);
364        } catch (IOException e) {
365            LOG.warn("Old message cleanup failed due to: " + e, e);
366        } catch (SQLException e) {
367            LOG.warn("Old message cleanup failed due to: " + e);
368            JDBCPersistenceAdapter.log("Failure Details: ", e);
369        } finally {
370            if (c != null) {
371                try {
372                    c.close();
373                } catch (Throwable e) {
374                }
375            }
376            LOG.debug("Cleanup done.");
377        }
378    }
379
380    @Override
381    public ScheduledThreadPoolExecutor getScheduledThreadPoolExecutor() {
382        if (clockDaemon == null) {
383            clockDaemon = new ScheduledThreadPoolExecutor(5, new ThreadFactory() {
384                @Override
385                public Thread newThread(Runnable runnable) {
386                    Thread thread = new Thread(runnable, "ActiveMQ JDBC PA Scheduled Task");
387                    thread.setDaemon(true);
388                    return thread;
389                }
390            });
391        }
392        return clockDaemon;
393    }
394
395    public JDBCAdapter getAdapter() throws IOException {
396        if (adapter == null) {
397            setAdapter(createAdapter());
398        }
399        return adapter;
400    }
401
402    /**
403     * @deprecated as of 5.7.0, replaced by {@link #getLocker()}
404     */
405    @Deprecated
406    public Locker getDatabaseLocker() throws IOException {
407        return getLocker();
408    }
409
410    /**
411     * Sets the database locker strategy to use to lock the database on startup
412     * @throws IOException
413     *
414     * @deprecated as of 5.7.0, replaced by {@link #setLocker(org.apache.activemq.broker.Locker)}
415     */
416    @Deprecated
417    public void setDatabaseLocker(Locker locker) throws IOException {
418        setLocker(locker);
419    }
420
421    public DataSource getLockDataSource() throws IOException {
422        if (lockDataSource == null) {
423            lockDataSource = getDataSource();
424            if (lockDataSource == null) {
425                throw new IllegalArgumentException(
426                        "No dataSource property has been configured");
427            }
428        }
429        return lockDataSource;
430    }
431
432    public void setLockDataSource(DataSource dataSource) {
433        this.lockDataSource = dataSource;
434        LOG.info("Using a separate dataSource for locking: "
435                            + lockDataSource);
436    }
437
438    @Override
439    public BrokerService getBrokerService() {
440        return brokerService;
441    }
442
443    /**
444     * @throws IOException
445     */
446    protected JDBCAdapter createAdapter() throws IOException {
447
448        adapter = (JDBCAdapter) loadAdapter(adapterFactoryFinder, "adapter");
449
450        // Use the default JDBC adapter if the
451        // Database type is not recognized.
452        if (adapter == null) {
453            adapter = new DefaultJDBCAdapter();
454            LOG.debug("Using default JDBC Adapter: " + adapter);
455        }
456        return adapter;
457    }
458
459    private Object loadAdapter(FactoryFinder finder, String kind) throws IOException {
460        Object adapter = null;
461        TransactionContext c = getTransactionContext();
462        try {
463            try {
464                // Make the filename file system safe.
465                String dirverName = c.getConnection().getMetaData().getDriverName();
466                dirverName = dirverName.replaceAll("[^a-zA-Z0-9\\-]", "_").toLowerCase(Locale.ENGLISH);
467
468                try {
469                    adapter = finder.newInstance(dirverName);
470                    LOG.info("Database " + kind + " driver override recognized for : [" + dirverName + "] - adapter: " + adapter.getClass());
471                } catch (Throwable e) {
472                    LOG.info("Database " + kind + " driver override not found for : [" + dirverName
473                             + "].  Will use default implementation.");
474                }
475            } catch (SQLException e) {
476                LOG.warn("JDBC error occurred while trying to detect database type for overrides. Will use default implementations: "
477                          + e.getMessage());
478                JDBCPersistenceAdapter.log("Failure Details: ", e);
479            }
480        } finally {
481            c.close();
482        }
483        return adapter;
484    }
485
486    public void setAdapter(JDBCAdapter adapter) {
487        this.adapter = adapter;
488        this.adapter.setStatements(getStatements());
489        this.adapter.setMaxRows(getMaxRows());
490    }
491
492    public WireFormat getWireFormat() {
493        return wireFormat;
494    }
495
496    public void setWireFormat(WireFormat wireFormat) {
497        this.wireFormat = wireFormat;
498    }
499
500    public TransactionContext getTransactionContext(ConnectionContext context) throws IOException {
501        if (context == null || isBrokerContext(context)) {
502            return getTransactionContext();
503        } else {
504            TransactionContext answer = (TransactionContext)context.getLongTermStoreContext();
505            if (answer == null) {
506                answer = getTransactionContext();
507                context.setLongTermStoreContext(answer);
508            }
509            return answer;
510        }
511    }
512
513    private boolean isBrokerContext(ConnectionContext context) {
514        return context.getSecurityContext() != null && context.getSecurityContext().isBrokerContext();
515    }
516
517    public TransactionContext getTransactionContext() throws IOException {
518        TransactionContext answer = new TransactionContext(this, networkTimeout, queryTimeout);
519        if (transactionIsolation > 0) {
520            answer.setTransactionIsolation(transactionIsolation);
521        }
522        return answer;
523    }
524
525    @Override
526    public void beginTransaction(ConnectionContext context) throws IOException {
527        TransactionContext transactionContext = getTransactionContext(context);
528        transactionContext.begin();
529    }
530
531    @Override
532    public void commitTransaction(ConnectionContext context) throws IOException {
533        TransactionContext transactionContext = getTransactionContext(context);
534        transactionContext.commit();
535    }
536
537    @Override
538    public void rollbackTransaction(ConnectionContext context) throws IOException {
539        TransactionContext transactionContext = getTransactionContext(context);
540        transactionContext.rollback();
541    }
542
543    public int getCleanupPeriod() {
544        return cleanupPeriod;
545    }
546
547    /**
548     * Sets the number of milliseconds until the database is attempted to be
549     * cleaned up for durable topics
550     */
551    public void setCleanupPeriod(int cleanupPeriod) {
552        this.cleanupPeriod = cleanupPeriod;
553    }
554
555    public boolean isChangeAutoCommitAllowed() {
556        return changeAutoCommitAllowed;
557    }
558
559    /**
560     * Whether the JDBC driver allows to set the auto commit.
561     * Some drivers does not allow changing the auto commit. The default value is true.
562     *
563     * @param changeAutoCommitAllowed true to change, false to not change.
564     */
565    public void setChangeAutoCommitAllowed(boolean changeAutoCommitAllowed) {
566        this.changeAutoCommitAllowed = changeAutoCommitAllowed;
567    }
568
569    public int getNetworkTimeout() {
570        return networkTimeout;
571    }
572
573    /**
574     * Define the JDBC connection network timeout.
575     *
576     * @param networkTimeout the connection network timeout (in milliseconds).
577     */
578    public void setNetworkTimeout(int networkTimeout) {
579        this.networkTimeout = networkTimeout;
580    }
581
582    public int getQueryTimeout() {
583        return queryTimeout;
584    }
585
586    /**
587     * Define the JDBC statement query timeout.
588     *
589     * @param queryTimeout the statement query timeout (in seconds).
590     */
591    public void setQueryTimeout(int queryTimeout) {
592        this.queryTimeout = queryTimeout;
593    }
594
595    @Override
596    public void deleteAllMessages() throws IOException {
597        TransactionContext c = getTransactionContext();
598        c.getExclusiveConnection();
599        try {
600            getAdapter().doDropTables(c);
601            getAdapter().setUseExternalMessageReferences(isUseExternalMessageReferences());
602            getAdapter().doCreateTables(c);
603            LOG.info("Persistence store purged.");
604        } catch (SQLException e) {
605            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
606            throw IOExceptionSupport.create(e);
607        } finally {
608            c.close();
609        }
610    }
611
612    public boolean isUseExternalMessageReferences() {
613        return useExternalMessageReferences;
614    }
615
616    public void setUseExternalMessageReferences(boolean useExternalMessageReferences) {
617        this.useExternalMessageReferences = useExternalMessageReferences;
618    }
619
620    public boolean isCreateTablesOnStartup() {
621        return createTablesOnStartup;
622    }
623
624    /**
625     * Sets whether or not tables are created on startup
626     */
627    public void setCreateTablesOnStartup(boolean createTablesOnStartup) {
628        this.createTablesOnStartup = createTablesOnStartup;
629    }
630
631    /**
632     * @deprecated use {@link #setUseLock(boolean)} instead
633     *
634     * Sets whether or not an exclusive database lock should be used to enable
635     * JDBC Master/Slave. Enabled by default.
636     */
637    @Deprecated
638    public void setUseDatabaseLock(boolean useDatabaseLock) {
639        setUseLock(useDatabaseLock);
640    }
641
642    public static void log(String msg, SQLException e) {
643        String s = msg + e.getMessage();
644        while (e.getNextException() != null) {
645            e = e.getNextException();
646            s += ", due to: " + e.getMessage();
647        }
648        LOG.warn(s, e);
649    }
650
651    public Statements getStatements() {
652        if (statements == null) {
653            statements = new Statements();
654        }
655        return statements;
656    }
657
658    public void setStatements(Statements statements) {
659        this.statements = statements;
660        if (adapter != null) {
661            this.adapter.setStatements(getStatements());
662        }
663    }
664
665    /**
666     * @param usageManager The UsageManager that is controlling the
667     *                destination's memory usage.
668     */
669    @Override
670    public void setUsageManager(SystemUsage usageManager) {
671    }
672
673    @Override
674    public Locker createDefaultLocker() throws IOException {
675        Locker locker = (Locker) loadAdapter(lockFactoryFinder, "lock");
676        if (locker == null) {
677            locker = new DefaultDatabaseLocker();
678            LOG.debug("Using default JDBC Locker: " + locker);
679        }
680        locker.configure(this);
681        return locker;
682    }
683
684    @Override
685    public void setBrokerName(String brokerName) {
686    }
687
688    @Override
689    public String toString() {
690        return "JDBCPersistenceAdapter(" + super.toString() + ")";
691    }
692
693    @Override
694    public void setDirectory(File dir) {
695        this.directory=dir;
696    }
697
698    @Override
699    public File getDirectory(){
700        if (this.directory==null && brokerService != null){
701            this.directory=brokerService.getBrokerDataDirectory();
702        }
703        return this.directory;
704    }
705
706    // interesting bit here is proof that DB is ok
707    @Override
708    public void checkpoint(boolean sync) throws IOException {
709        // by pass TransactionContext to avoid IO Exception handler
710        Connection connection = null;
711        try {
712            connection = getDataSource().getConnection();
713            if (!connection.isValid(10)) {
714                throw new IOException("isValid(10) failed for: " + connection);
715            }
716        } catch (SQLException e) {
717            LOG.debug("Could not get JDBC connection for checkpoint: " + e, e);
718            throw IOExceptionSupport.create(e);
719        } finally {
720            if (connection != null) {
721                try {
722                    connection.close();
723                } catch (Throwable ignored) {
724                }
725            }
726        }
727    }
728
729    @Override
730    public long size(){
731        return 0;
732    }
733
734    /**
735     * @deprecated use {@link Locker#setLockAcquireSleepInterval(long)} instead
736     *
737     * millisecond interval between lock acquire attempts, applied to newly created DefaultDatabaseLocker
738     * not applied if DataBaseLocker is injected.
739     *
740     */
741    @Deprecated
742    public void setLockAcquireSleepInterval(long lockAcquireSleepInterval) throws IOException {
743        getLocker().setLockAcquireSleepInterval(lockAcquireSleepInterval);
744    }
745
746    /**
747     * set the Transaction isolation level to something other that TRANSACTION_READ_UNCOMMITTED
748     * This allowable dirty isolation level may not be achievable in clustered DB environments
749     * so a more restrictive and expensive option may be needed like TRANSACTION_REPEATABLE_READ
750     * see isolation level constants in {@link java.sql.Connection}
751     * @param transactionIsolation the isolation level to use
752     */
753    public void setTransactionIsolation(int transactionIsolation) {
754        this.transactionIsolation = transactionIsolation;
755    }
756
757    public int getMaxProducersToAudit() {
758        return maxProducersToAudit;
759    }
760
761    public void setMaxProducersToAudit(int maxProducersToAudit) {
762        this.maxProducersToAudit = maxProducersToAudit;
763    }
764
765    public int getMaxAuditDepth() {
766        return maxAuditDepth;
767    }
768
769    public void setMaxAuditDepth(int maxAuditDepth) {
770        this.maxAuditDepth = maxAuditDepth;
771    }
772
773    public boolean isEnableAudit() {
774        return enableAudit;
775    }
776
777    public void setEnableAudit(boolean enableAudit) {
778        this.enableAudit = enableAudit;
779    }
780
781    public int getAuditRecoveryDepth() {
782        return auditRecoveryDepth;
783    }
784
785    public void setAuditRecoveryDepth(int auditRecoveryDepth) {
786        this.auditRecoveryDepth = auditRecoveryDepth;
787    }
788
789    public long getNextSequenceId() {
790        return sequenceGenerator.getNextSequenceId();
791    }
792
793    public int getMaxRows() {
794        return maxRows;
795    }
796
797    /*
798     * the max rows return from queries, with sparse selectors this may need to be increased
799     */
800    public void setMaxRows(int maxRows) {
801        this.maxRows = maxRows;
802    }
803
804    public void recover(JdbcMemoryTransactionStore jdbcMemoryTransactionStore) throws IOException {
805        TransactionContext c = getTransactionContext();
806        try {
807            getAdapter().doRecoverPreparedOps(c, jdbcMemoryTransactionStore);
808        } catch (SQLException e) {
809            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
810            throw IOExceptionSupport.create("Failed to recover from: " + jdbcMemoryTransactionStore + ". Reason: " + e,e);
811        } finally {
812            c.close();
813        }
814    }
815
816    public void commitAdd(ConnectionContext context, final MessageId messageId, final long preparedSequenceId, final long newSequence) throws IOException {
817        TransactionContext c = getTransactionContext(context);
818        try {
819            getAdapter().doCommitAddOp(c, preparedSequenceId, newSequence);
820        } catch (SQLException e) {
821            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
822            throw IOExceptionSupport.create("Failed to commit add: " + messageId + ". Reason: " + e, e);
823        } finally {
824            c.close();
825        }
826    }
827
828    public void commitRemove(ConnectionContext context, MessageAck ack) throws IOException {
829        TransactionContext c = getTransactionContext(context);
830        try {
831            if (c != null && ack != null && ack.getLastMessageId() != null && ack.getLastMessageId().getEntryLocator() != null) {
832                getAdapter().doRemoveMessage(c, (Long) ack.getLastMessageId().getEntryLocator(), null);
833            }
834        } catch (SQLException e) {
835            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
836            throw IOExceptionSupport.create("Failed to commit last ack: " + ack + ". Reason: " + e,e);
837        } finally {
838            c.close();
839        }
840    }
841
842    public void commitLastAck(ConnectionContext context, long xidLastAck, long priority, ActiveMQDestination destination, String subName, String clientId) throws IOException {
843        TransactionContext c = getTransactionContext(context);
844        try {
845            getAdapter().doSetLastAck(c, destination, null, clientId, subName, xidLastAck, priority);
846        } catch (SQLException e) {
847            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
848            throw IOExceptionSupport.create("Failed to commit last ack with priority: " + priority + " on " + destination + " for " + subName + ":" + clientId + ". Reason: " + e,e);
849        } finally {
850            c.close();
851        }
852    }
853
854    public void rollbackLastAck(ConnectionContext context, JDBCTopicMessageStore store, MessageAck ack, String subName, String clientId) throws IOException {
855        TransactionContext c = getTransactionContext(context);
856        try {
857            byte priority = (byte) store.getCachedStoreSequenceId(c, store.getDestination(), ack.getLastMessageId())[1];
858            getAdapter().doClearLastAck(c, store.getDestination(), priority, clientId, subName);
859        } catch (SQLException e) {
860            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
861            throw IOExceptionSupport.create("Failed to rollback last ack: " + ack + " on " +  store.getDestination() + " for " + subName + ":" + clientId + ". Reason: " + e,e);
862        } finally {
863            c.close();
864        }
865    }
866
867    // after recovery there is no record of the original messageId for the ack
868    public void rollbackLastAck(ConnectionContext context, byte priority, ActiveMQDestination destination, String subName, String clientId) throws IOException {
869        TransactionContext c = getTransactionContext(context);
870        try {
871            getAdapter().doClearLastAck(c, destination, priority, clientId, subName);
872        } catch (SQLException e) {
873            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
874            throw IOExceptionSupport.create("Failed to rollback last ack with priority: " + priority + " on " + destination + " for " + subName + ":" + clientId + ". Reason: " + e, e);
875        } finally {
876            c.close();
877        }
878    }
879
880    long[] getStoreSequenceIdForMessageId(ConnectionContext context, MessageId messageId, ActiveMQDestination destination) throws IOException {
881        long[] result = new long[]{-1, Byte.MAX_VALUE -1};
882        TransactionContext c = getTransactionContext(context);
883        try {
884            result = adapter.getStoreSequenceId(c, destination, messageId);
885        } catch (SQLException e) {
886            JDBCPersistenceAdapter.log("JDBC Failure: ", e);
887            throw IOExceptionSupport.create("Failed to get store sequenceId for messageId: " + messageId +", on: " + destination + ". Reason: " + e, e);
888        } finally {
889            c.close();
890        }
891        return result;
892    }
893
894    @Override
895    public JobSchedulerStore createJobSchedulerStore() throws IOException, UnsupportedOperationException {
896        throw new UnsupportedOperationException();
897    }
898}