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.kahadb;
018
019import java.io.DataInputStream;
020import java.io.IOException;
021import java.io.InterruptedIOException;
022import java.util.ArrayList;
023import java.util.HashMap;
024import java.util.HashSet;
025import java.util.Iterator;
026import java.util.LinkedList;
027import java.util.List;
028import java.util.Map;
029import java.util.Map.Entry;
030import java.util.Set;
031import java.util.concurrent.BlockingQueue;
032import java.util.concurrent.Callable;
033import java.util.concurrent.ExecutorService;
034import java.util.concurrent.FutureTask;
035import java.util.concurrent.LinkedBlockingQueue;
036import java.util.concurrent.Semaphore;
037import java.util.concurrent.ThreadFactory;
038import java.util.concurrent.ThreadPoolExecutor;
039import java.util.concurrent.TimeUnit;
040import java.util.concurrent.TimeoutException;
041import java.util.concurrent.atomic.AtomicBoolean;
042import java.util.concurrent.atomic.AtomicInteger;
043
044import org.apache.activemq.broker.ConnectionContext;
045import org.apache.activemq.broker.region.BaseDestination;
046import org.apache.activemq.broker.scheduler.JobSchedulerStore;
047import org.apache.activemq.command.ActiveMQDestination;
048import org.apache.activemq.command.ActiveMQQueue;
049import org.apache.activemq.command.ActiveMQTempQueue;
050import org.apache.activemq.command.ActiveMQTempTopic;
051import org.apache.activemq.command.ActiveMQTopic;
052import org.apache.activemq.command.Message;
053import org.apache.activemq.command.MessageAck;
054import org.apache.activemq.command.MessageId;
055import org.apache.activemq.command.ProducerId;
056import org.apache.activemq.command.SubscriptionInfo;
057import org.apache.activemq.command.TransactionId;
058import org.apache.activemq.openwire.OpenWireFormat;
059import org.apache.activemq.protobuf.Buffer;
060import org.apache.activemq.store.AbstractMessageStore;
061import org.apache.activemq.store.IndexListener;
062import org.apache.activemq.store.ListenableFuture;
063import org.apache.activemq.store.MessageRecoveryListener;
064import org.apache.activemq.store.MessageStore;
065import org.apache.activemq.store.PersistenceAdapter;
066import org.apache.activemq.store.TopicMessageStore;
067import org.apache.activemq.store.TransactionIdTransformer;
068import org.apache.activemq.store.TransactionStore;
069import org.apache.activemq.store.kahadb.data.KahaAddMessageCommand;
070import org.apache.activemq.store.kahadb.data.KahaDestination;
071import org.apache.activemq.store.kahadb.data.KahaDestination.DestinationType;
072import org.apache.activemq.store.kahadb.data.KahaLocation;
073import org.apache.activemq.store.kahadb.data.KahaRemoveDestinationCommand;
074import org.apache.activemq.store.kahadb.data.KahaRemoveMessageCommand;
075import org.apache.activemq.store.kahadb.data.KahaSubscriptionCommand;
076import org.apache.activemq.store.kahadb.data.KahaUpdateMessageCommand;
077import org.apache.activemq.store.kahadb.disk.journal.Location;
078import org.apache.activemq.store.kahadb.disk.page.Transaction;
079import org.apache.activemq.store.kahadb.scheduler.JobSchedulerStoreImpl;
080import org.apache.activemq.usage.MemoryUsage;
081import org.apache.activemq.usage.SystemUsage;
082import org.apache.activemq.util.ServiceStopper;
083import org.apache.activemq.util.ThreadPoolUtils;
084import org.apache.activemq.wireformat.WireFormat;
085import org.slf4j.Logger;
086import org.slf4j.LoggerFactory;
087
088public class KahaDBStore extends MessageDatabase implements PersistenceAdapter {
089    static final Logger LOG = LoggerFactory.getLogger(KahaDBStore.class);
090    private static final int MAX_ASYNC_JOBS = BaseDestination.MAX_AUDIT_DEPTH;
091
092    public static final String PROPERTY_CANCELED_TASK_MOD_METRIC = "org.apache.activemq.store.kahadb.CANCELED_TASK_MOD_METRIC";
093    public static final int cancelledTaskModMetric = Integer.parseInt(System.getProperty(
094            PROPERTY_CANCELED_TASK_MOD_METRIC, "0"), 10);
095    public static final String PROPERTY_ASYNC_EXECUTOR_MAX_THREADS = "org.apache.activemq.store.kahadb.ASYNC_EXECUTOR_MAX_THREADS";
096    private static final int asyncExecutorMaxThreads = Integer.parseInt(System.getProperty(
097            PROPERTY_ASYNC_EXECUTOR_MAX_THREADS, "1"), 10);;
098
099    protected ExecutorService queueExecutor;
100    protected ExecutorService topicExecutor;
101    protected final List<Map<AsyncJobKey, StoreTask>> asyncQueueMaps = new LinkedList<Map<AsyncJobKey, StoreTask>>();
102    protected final List<Map<AsyncJobKey, StoreTask>> asyncTopicMaps = new LinkedList<Map<AsyncJobKey, StoreTask>>();
103    final WireFormat wireFormat = new OpenWireFormat();
104    private SystemUsage usageManager;
105    private LinkedBlockingQueue<Runnable> asyncQueueJobQueue;
106    private LinkedBlockingQueue<Runnable> asyncTopicJobQueue;
107    Semaphore globalQueueSemaphore;
108    Semaphore globalTopicSemaphore;
109    private boolean concurrentStoreAndDispatchQueues = true;
110    // when true, message order may be compromised when cache is exhausted if store is out
111    // or order w.r.t cache
112    private boolean concurrentStoreAndDispatchTopics = false;
113    private final boolean concurrentStoreAndDispatchTransactions = false;
114    private int maxAsyncJobs = MAX_ASYNC_JOBS;
115    private final KahaDBTransactionStore transactionStore;
116    private TransactionIdTransformer transactionIdTransformer;
117
118    public KahaDBStore() {
119        this.transactionStore = new KahaDBTransactionStore(this);
120        this.transactionIdTransformer = new TransactionIdTransformer() {
121            @Override
122            public TransactionId transform(TransactionId txid) {
123                return txid;
124            }
125        };
126    }
127
128    @Override
129    public String toString() {
130        return "KahaDB:[" + directory.getAbsolutePath() + "]";
131    }
132
133    @Override
134    public void setBrokerName(String brokerName) {
135    }
136
137    @Override
138    public void setUsageManager(SystemUsage usageManager) {
139        this.usageManager = usageManager;
140    }
141
142    public SystemUsage getUsageManager() {
143        return this.usageManager;
144    }
145
146    /**
147     * @return the concurrentStoreAndDispatch
148     */
149    public boolean isConcurrentStoreAndDispatchQueues() {
150        return this.concurrentStoreAndDispatchQueues;
151    }
152
153    /**
154     * @param concurrentStoreAndDispatch
155     *            the concurrentStoreAndDispatch to set
156     */
157    public void setConcurrentStoreAndDispatchQueues(boolean concurrentStoreAndDispatch) {
158        this.concurrentStoreAndDispatchQueues = concurrentStoreAndDispatch;
159    }
160
161    /**
162     * @return the concurrentStoreAndDispatch
163     */
164    public boolean isConcurrentStoreAndDispatchTopics() {
165        return this.concurrentStoreAndDispatchTopics;
166    }
167
168    /**
169     * @param concurrentStoreAndDispatch
170     *            the concurrentStoreAndDispatch to set
171     */
172    public void setConcurrentStoreAndDispatchTopics(boolean concurrentStoreAndDispatch) {
173        this.concurrentStoreAndDispatchTopics = concurrentStoreAndDispatch;
174    }
175
176    public boolean isConcurrentStoreAndDispatchTransactions() {
177        return this.concurrentStoreAndDispatchTransactions;
178    }
179
180    /**
181     * @return the maxAsyncJobs
182     */
183    public int getMaxAsyncJobs() {
184        return this.maxAsyncJobs;
185    }
186
187    /**
188     * @param maxAsyncJobs
189     *            the maxAsyncJobs to set
190     */
191    public void setMaxAsyncJobs(int maxAsyncJobs) {
192        this.maxAsyncJobs = maxAsyncJobs;
193    }
194
195    @Override
196    public void doStart() throws Exception {
197        if (brokerService != null) {
198            metadata.openwireVersion = brokerService.getStoreOpenWireVersion();
199            wireFormat.setVersion(metadata.openwireVersion);
200
201            if (LOG.isDebugEnabled()) {
202                LOG.debug("Store OpenWire version configured as: {}", metadata.openwireVersion);
203            }
204
205        }
206        super.doStart();
207
208        if (brokerService != null) {
209            // In case the recovered store used a different OpenWire version log a warning
210            // to assist in determining why journal reads fail.
211            if (metadata.openwireVersion != brokerService.getStoreOpenWireVersion()) {
212                LOG.warn("Recovered Store uses a different OpenWire version[{}] " +
213                         "than the version configured[{}].",
214                         metadata.openwireVersion, brokerService.getStoreOpenWireVersion());
215            }
216        }
217
218        this.globalQueueSemaphore = new Semaphore(getMaxAsyncJobs());
219        this.globalTopicSemaphore = new Semaphore(getMaxAsyncJobs());
220        this.asyncQueueJobQueue = new LinkedBlockingQueue<Runnable>(getMaxAsyncJobs());
221        this.asyncTopicJobQueue = new LinkedBlockingQueue<Runnable>(getMaxAsyncJobs());
222        this.queueExecutor = new StoreTaskExecutor(1, asyncExecutorMaxThreads, 0L, TimeUnit.MILLISECONDS,
223            asyncQueueJobQueue, new ThreadFactory() {
224                @Override
225                public Thread newThread(Runnable runnable) {
226                    Thread thread = new Thread(runnable, "ConcurrentQueueStoreAndDispatch");
227                    thread.setDaemon(true);
228                    return thread;
229                }
230            });
231        this.topicExecutor = new StoreTaskExecutor(1, asyncExecutorMaxThreads, 0L, TimeUnit.MILLISECONDS,
232            asyncTopicJobQueue, new ThreadFactory() {
233                @Override
234                public Thread newThread(Runnable runnable) {
235                    Thread thread = new Thread(runnable, "ConcurrentTopicStoreAndDispatch");
236                    thread.setDaemon(true);
237                    return thread;
238                }
239            });
240    }
241
242    @Override
243    public void doStop(ServiceStopper stopper) throws Exception {
244        // drain down async jobs
245        LOG.info("Stopping async queue tasks");
246        if (this.globalQueueSemaphore != null) {
247            this.globalQueueSemaphore.tryAcquire(this.maxAsyncJobs, 60, TimeUnit.SECONDS);
248        }
249        synchronized (this.asyncQueueMaps) {
250            for (Map<AsyncJobKey, StoreTask> m : asyncQueueMaps) {
251                synchronized (m) {
252                    for (StoreTask task : m.values()) {
253                        task.cancel();
254                    }
255                }
256            }
257            this.asyncQueueMaps.clear();
258        }
259        LOG.info("Stopping async topic tasks");
260        if (this.globalTopicSemaphore != null) {
261            this.globalTopicSemaphore.tryAcquire(this.maxAsyncJobs, 60, TimeUnit.SECONDS);
262        }
263        synchronized (this.asyncTopicMaps) {
264            for (Map<AsyncJobKey, StoreTask> m : asyncTopicMaps) {
265                synchronized (m) {
266                    for (StoreTask task : m.values()) {
267                        task.cancel();
268                    }
269                }
270            }
271            this.asyncTopicMaps.clear();
272        }
273        if (this.globalQueueSemaphore != null) {
274            this.globalQueueSemaphore.drainPermits();
275        }
276        if (this.globalTopicSemaphore != null) {
277            this.globalTopicSemaphore.drainPermits();
278        }
279        if (this.queueExecutor != null) {
280            ThreadPoolUtils.shutdownNow(queueExecutor);
281            queueExecutor = null;
282        }
283        if (this.topicExecutor != null) {
284            ThreadPoolUtils.shutdownNow(topicExecutor);
285            topicExecutor = null;
286        }
287        LOG.info("Stopped KahaDB");
288        super.doStop(stopper);
289    }
290
291    private Location findMessageLocation(final String key, final KahaDestination destination) throws IOException {
292        return pageFile.tx().execute(new Transaction.CallableClosure<Location, IOException>() {
293            @Override
294            public Location execute(Transaction tx) throws IOException {
295                StoredDestination sd = getStoredDestination(destination, tx);
296                Long sequence = sd.messageIdIndex.get(tx, key);
297                if (sequence == null) {
298                    return null;
299                }
300                return sd.orderIndex.get(tx, sequence).location;
301            }
302        });
303    }
304
305    protected StoreQueueTask removeQueueTask(KahaDBMessageStore store, MessageId id) {
306        StoreQueueTask task = null;
307        synchronized (store.asyncTaskMap) {
308            task = (StoreQueueTask) store.asyncTaskMap.remove(new AsyncJobKey(id, store.getDestination()));
309        }
310        return task;
311    }
312
313    protected void addQueueTask(KahaDBMessageStore store, StoreQueueTask task) throws IOException {
314        synchronized (store.asyncTaskMap) {
315            store.asyncTaskMap.put(new AsyncJobKey(task.getMessage().getMessageId(), store.getDestination()), task);
316        }
317        this.queueExecutor.execute(task);
318    }
319
320    protected StoreTopicTask removeTopicTask(KahaDBTopicMessageStore store, MessageId id) {
321        StoreTopicTask task = null;
322        synchronized (store.asyncTaskMap) {
323            task = (StoreTopicTask) store.asyncTaskMap.remove(new AsyncJobKey(id, store.getDestination()));
324        }
325        return task;
326    }
327
328    protected void addTopicTask(KahaDBTopicMessageStore store, StoreTopicTask task) throws IOException {
329        synchronized (store.asyncTaskMap) {
330            store.asyncTaskMap.put(new AsyncJobKey(task.getMessage().getMessageId(), store.getDestination()), task);
331        }
332        this.topicExecutor.execute(task);
333    }
334
335    @Override
336    public TransactionStore createTransactionStore() throws IOException {
337        return this.transactionStore;
338    }
339
340    public boolean getForceRecoverIndex() {
341        return this.forceRecoverIndex;
342    }
343
344    public void setForceRecoverIndex(boolean forceRecoverIndex) {
345        this.forceRecoverIndex = forceRecoverIndex;
346    }
347
348    public class KahaDBMessageStore extends AbstractMessageStore {
349        protected final Map<AsyncJobKey, StoreTask> asyncTaskMap = new HashMap<AsyncJobKey, StoreTask>();
350        protected KahaDestination dest;
351        private final int maxAsyncJobs;
352        private final Semaphore localDestinationSemaphore;
353
354        double doneTasks, canceledTasks = 0;
355
356        public KahaDBMessageStore(ActiveMQDestination destination) {
357            super(destination);
358            this.dest = convert(destination);
359            this.maxAsyncJobs = getMaxAsyncJobs();
360            this.localDestinationSemaphore = new Semaphore(this.maxAsyncJobs);
361        }
362
363        @Override
364        public ActiveMQDestination getDestination() {
365            return destination;
366        }
367
368        @Override
369        public ListenableFuture<Object> asyncAddQueueMessage(final ConnectionContext context, final Message message)
370                throws IOException {
371            if (isConcurrentStoreAndDispatchQueues()) {
372                StoreQueueTask result = new StoreQueueTask(this, context, message);
373                ListenableFuture<Object> future = result.getFuture();
374                message.getMessageId().setFutureOrSequenceLong(future);
375                message.setRecievedByDFBridge(true); // flag message as concurrentStoreAndDispatch
376                result.aquireLocks();
377                addQueueTask(this, result);
378                if (indexListener != null) {
379                    // allow concurrent dispatch by setting entry locator,
380                    indexListener.onAdd(new IndexListener.MessageContext(context, message, null));
381                }
382                return future;
383            } else {
384                return super.asyncAddQueueMessage(context, message);
385            }
386        }
387
388        @Override
389        public void removeAsyncMessage(ConnectionContext context, MessageAck ack) throws IOException {
390            if (isConcurrentStoreAndDispatchQueues()) {
391                AsyncJobKey key = new AsyncJobKey(ack.getLastMessageId(), getDestination());
392                StoreQueueTask task = null;
393                synchronized (asyncTaskMap) {
394                    task = (StoreQueueTask) asyncTaskMap.get(key);
395                }
396                if (task != null) {
397                    if (ack.isInTransaction() || !task.cancel()) {
398                        try {
399                            task.future.get();
400                        } catch (InterruptedException e) {
401                            throw new InterruptedIOException(e.toString());
402                        } catch (Exception ignored) {
403                            LOG.debug("removeAsync: cannot cancel, waiting for add resulted in ex", ignored);
404                        }
405                        removeMessage(context, ack);
406                    } else {
407                        synchronized (asyncTaskMap) {
408                            asyncTaskMap.remove(key);
409                        }
410                    }
411                } else {
412                    removeMessage(context, ack);
413                }
414            } else {
415                removeMessage(context, ack);
416            }
417        }
418
419        @Override
420        public void addMessage(final ConnectionContext context, final Message message) throws IOException {
421            final KahaAddMessageCommand command = new KahaAddMessageCommand();
422            command.setDestination(dest);
423            command.setMessageId(message.getMessageId().toProducerKey());
424            command.setTransactionInfo(TransactionIdConversion.convert(transactionIdTransformer.transform(message.getTransactionId())));
425            command.setPriority(message.getPriority());
426            command.setPrioritySupported(isPrioritizedMessages());
427            org.apache.activemq.util.ByteSequence packet = wireFormat.marshal(message);
428            command.setMessage(new Buffer(packet.getData(), packet.getOffset(), packet.getLength()));
429                store(command, isEnableJournalDiskSyncs() && message.isResponseRequired(), new IndexAware() {
430                    // sync add? (for async, future present from getFutureOrSequenceLong)
431                    Object possibleFuture = message.getMessageId().getFutureOrSequenceLong();
432
433                    public void sequenceAssignedWithIndexLocked(final long sequence) {
434                        message.getMessageId().setFutureOrSequenceLong(sequence);
435                        if (indexListener != null) {
436                            if (possibleFuture == null) {
437                                trackPendingAdd(dest, sequence);
438                                indexListener.onAdd(new IndexListener.MessageContext(context, message, new Runnable() {
439                                    @Override
440                                    public void run() {
441                                        trackPendingAddComplete(dest, sequence);
442                                    }
443                                }));
444                            }
445                        }
446                    }
447                }, null);
448        }
449
450        @Override
451        public void updateMessage(Message message) throws IOException {
452            if (LOG.isTraceEnabled()) {
453                LOG.trace("updating: " + message.getMessageId() + " with deliveryCount: " + message.getRedeliveryCounter());
454            }
455            KahaUpdateMessageCommand updateMessageCommand = new KahaUpdateMessageCommand();
456            KahaAddMessageCommand command = new KahaAddMessageCommand();
457            command.setDestination(dest);
458            command.setMessageId(message.getMessageId().toProducerKey());
459            command.setPriority(message.getPriority());
460            command.setPrioritySupported(prioritizedMessages);
461            org.apache.activemq.util.ByteSequence packet = wireFormat.marshal(message);
462            command.setMessage(new Buffer(packet.getData(), packet.getOffset(), packet.getLength()));
463            updateMessageCommand.setMessage(command);
464            store(updateMessageCommand, isEnableJournalDiskSyncs(), null, null);
465        }
466
467        @Override
468        public void removeMessage(ConnectionContext context, MessageAck ack) throws IOException {
469            KahaRemoveMessageCommand command = new KahaRemoveMessageCommand();
470            command.setDestination(dest);
471            command.setMessageId(ack.getLastMessageId().toProducerKey());
472            command.setTransactionInfo(TransactionIdConversion.convert(transactionIdTransformer.transform(ack.getTransactionId())));
473
474            org.apache.activemq.util.ByteSequence packet = wireFormat.marshal(ack);
475            command.setAck(new Buffer(packet.getData(), packet.getOffset(), packet.getLength()));
476            store(command, isEnableJournalDiskSyncs() && ack.isResponseRequired(), null, null);
477        }
478
479        @Override
480        public void removeAllMessages(ConnectionContext context) throws IOException {
481            KahaRemoveDestinationCommand command = new KahaRemoveDestinationCommand();
482            command.setDestination(dest);
483            store(command, true, null, null);
484        }
485
486        @Override
487        public Message getMessage(MessageId identity) throws IOException {
488            final String key = identity.toProducerKey();
489
490            // Hopefully one day the page file supports concurrent read
491            // operations... but for now we must
492            // externally synchronize...
493            Location location;
494            indexLock.writeLock().lock();
495            try {
496                location = findMessageLocation(key, dest);
497            } finally {
498                indexLock.writeLock().unlock();
499            }
500            if (location == null) {
501                return null;
502            }
503
504            return loadMessage(location);
505        }
506
507        @Override
508        public int getMessageCount() throws IOException {
509            try {
510                lockAsyncJobQueue();
511                indexLock.writeLock().lock();
512                try {
513                    return pageFile.tx().execute(new Transaction.CallableClosure<Integer, IOException>() {
514                        @Override
515                        public Integer execute(Transaction tx) throws IOException {
516                            // Iterate through all index entries to get a count
517                            // of messages in the destination.
518                            StoredDestination sd = getStoredDestination(dest, tx);
519                            int rc = 0;
520                            for (Iterator<Entry<Location, Long>> iterator = sd.locationIndex.iterator(tx); iterator.hasNext();) {
521                                iterator.next();
522                                rc++;
523                            }
524                            return rc;
525                        }
526                    });
527                } finally {
528                    indexLock.writeLock().unlock();
529                }
530            } finally {
531                unlockAsyncJobQueue();
532            }
533        }
534
535        @Override
536        public boolean isEmpty() throws IOException {
537            indexLock.writeLock().lock();
538            try {
539                return pageFile.tx().execute(new Transaction.CallableClosure<Boolean, IOException>() {
540                    @Override
541                    public Boolean execute(Transaction tx) throws IOException {
542                        // Iterate through all index entries to get a count of
543                        // messages in the destination.
544                        StoredDestination sd = getStoredDestination(dest, tx);
545                        return sd.locationIndex.isEmpty(tx);
546                    }
547                });
548            } finally {
549                indexLock.writeLock().unlock();
550            }
551        }
552
553        @Override
554        public void recover(final MessageRecoveryListener listener) throws Exception {
555            // recovery may involve expiry which will modify
556            indexLock.writeLock().lock();
557            try {
558                pageFile.tx().execute(new Transaction.Closure<Exception>() {
559                    @Override
560                    public void execute(Transaction tx) throws Exception {
561                        StoredDestination sd = getStoredDestination(dest, tx);
562                        recoverRolledBackAcks(sd, tx, Integer.MAX_VALUE, listener);
563                        sd.orderIndex.resetCursorPosition();
564                        for (Iterator<Entry<Long, MessageKeys>> iterator = sd.orderIndex.iterator(tx); listener.hasSpace() && iterator
565                                .hasNext(); ) {
566                            Entry<Long, MessageKeys> entry = iterator.next();
567                            if (ackedAndPrepared.contains(entry.getValue().messageId)) {
568                                continue;
569                            }
570                            Message msg = loadMessage(entry.getValue().location);
571                            listener.recoverMessage(msg);
572                        }
573                    }
574                });
575            } finally {
576                indexLock.writeLock().unlock();
577            }
578        }
579
580        @Override
581        public void recoverNextMessages(final int maxReturned, final MessageRecoveryListener listener) throws Exception {
582            indexLock.writeLock().lock();
583            try {
584                pageFile.tx().execute(new Transaction.Closure<Exception>() {
585                    @Override
586                    public void execute(Transaction tx) throws Exception {
587                        StoredDestination sd = getStoredDestination(dest, tx);
588                        Entry<Long, MessageKeys> entry = null;
589                        int counter = recoverRolledBackAcks(sd, tx, maxReturned, listener);
590                        for (Iterator<Entry<Long, MessageKeys>> iterator = sd.orderIndex.iterator(tx); iterator.hasNext(); ) {
591                            entry = iterator.next();
592                            if (ackedAndPrepared.contains(entry.getValue().messageId)) {
593                                continue;
594                            }
595                            Message msg = loadMessage(entry.getValue().location);
596                            msg.getMessageId().setFutureOrSequenceLong(entry.getKey());
597                            listener.recoverMessage(msg);
598                            counter++;
599                            if (counter >= maxReturned) {
600                                break;
601                            }
602                        }
603                        sd.orderIndex.stoppedIterating();
604                    }
605                });
606            } finally {
607                indexLock.writeLock().unlock();
608            }
609        }
610
611        protected int recoverRolledBackAcks(StoredDestination sd, Transaction tx, int maxReturned, MessageRecoveryListener listener) throws Exception {
612            int counter = 0;
613            String id;
614            for (Iterator<String> iterator = rolledBackAcks.iterator(); iterator.hasNext(); ) {
615                id = iterator.next();
616                iterator.remove();
617                Long sequence = sd.messageIdIndex.get(tx, id);
618                if (sequence != null) {
619                    if (sd.orderIndex.alreadyDispatched(sequence)) {
620                        listener.recoverMessage(loadMessage(sd.orderIndex.get(tx, sequence).location));
621                        counter++;
622                        if (counter >= maxReturned) {
623                            break;
624                        }
625                    } else {
626                        LOG.info("rolledback ack message {} with seq {} will be picked up in future batch {}", id, sequence, sd.orderIndex.cursor);
627                    }
628                } else {
629                    LOG.warn("Failed to locate rolled back ack message {} in {}", id, sd);
630                }
631            }
632            return counter;
633        }
634
635
636        @Override
637        public void resetBatching() {
638            if (pageFile.isLoaded()) {
639                indexLock.writeLock().lock();
640                try {
641                    pageFile.tx().execute(new Transaction.Closure<Exception>() {
642                        @Override
643                        public void execute(Transaction tx) throws Exception {
644                            StoredDestination sd = getExistingStoredDestination(dest, tx);
645                            if (sd != null) {
646                                sd.orderIndex.resetCursorPosition();}
647                            }
648                        });
649                } catch (Exception e) {
650                    LOG.error("Failed to reset batching",e);
651                } finally {
652                    indexLock.writeLock().unlock();
653                }
654            }
655        }
656
657        @Override
658        public void setBatch(final MessageId identity) throws IOException {
659            indexLock.writeLock().lock();
660            try {
661                pageFile.tx().execute(new Transaction.Closure<IOException>() {
662                    @Override
663                    public void execute(Transaction tx) throws IOException {
664                        StoredDestination sd = getStoredDestination(dest, tx);
665                        Long location = (Long) identity.getFutureOrSequenceLong();
666                        Long pending = sd.orderIndex.minPendingAdd();
667                        if (pending != null) {
668                            location = Math.min(location, pending-1);
669                        }
670                        sd.orderIndex.setBatch(tx, location);
671                    }
672                });
673            } finally {
674                indexLock.writeLock().unlock();
675            }
676        }
677
678        @Override
679        public void setMemoryUsage(MemoryUsage memoryUsage) {
680        }
681        @Override
682        public void start() throws Exception {
683            super.start();
684        }
685        @Override
686        public void stop() throws Exception {
687            super.stop();
688        }
689
690        protected void lockAsyncJobQueue() {
691            try {
692                if (!this.localDestinationSemaphore.tryAcquire(this.maxAsyncJobs, 60, TimeUnit.SECONDS)) {
693                    throw new TimeoutException(this +" timeout waiting for localDestSem:" + this.localDestinationSemaphore);
694                }
695            } catch (Exception e) {
696                LOG.error("Failed to lock async jobs for " + this.destination, e);
697            }
698        }
699
700        protected void unlockAsyncJobQueue() {
701            this.localDestinationSemaphore.release(this.maxAsyncJobs);
702        }
703
704        protected void acquireLocalAsyncLock() {
705            try {
706                this.localDestinationSemaphore.acquire();
707            } catch (InterruptedException e) {
708                LOG.error("Failed to aquire async lock for " + this.destination, e);
709            }
710        }
711
712        protected void releaseLocalAsyncLock() {
713            this.localDestinationSemaphore.release();
714        }
715
716        @Override
717        public String toString(){
718            return "permits:" + this.localDestinationSemaphore.availablePermits() + ",sd=" + storedDestinations.get(key(dest));
719        }
720    }
721
722    class KahaDBTopicMessageStore extends KahaDBMessageStore implements TopicMessageStore {
723        private final AtomicInteger subscriptionCount = new AtomicInteger();
724        public KahaDBTopicMessageStore(ActiveMQTopic destination) throws IOException {
725            super(destination);
726            this.subscriptionCount.set(getAllSubscriptions().length);
727            asyncTopicMaps.add(asyncTaskMap);
728        }
729
730        @Override
731        public ListenableFuture<Object> asyncAddTopicMessage(final ConnectionContext context, final Message message)
732                throws IOException {
733            if (isConcurrentStoreAndDispatchTopics()) {
734                StoreTopicTask result = new StoreTopicTask(this, context, message, subscriptionCount.get());
735                result.aquireLocks();
736                addTopicTask(this, result);
737                return result.getFuture();
738            } else {
739                return super.asyncAddTopicMessage(context, message);
740            }
741        }
742
743        @Override
744        public void acknowledge(ConnectionContext context, String clientId, String subscriptionName,
745                                MessageId messageId, MessageAck ack) throws IOException {
746            String subscriptionKey = subscriptionKey(clientId, subscriptionName).toString();
747            if (isConcurrentStoreAndDispatchTopics()) {
748                AsyncJobKey key = new AsyncJobKey(messageId, getDestination());
749                StoreTopicTask task = null;
750                synchronized (asyncTaskMap) {
751                    task = (StoreTopicTask) asyncTaskMap.get(key);
752                }
753                if (task != null) {
754                    if (task.addSubscriptionKey(subscriptionKey)) {
755                        removeTopicTask(this, messageId);
756                        if (task.cancel()) {
757                            synchronized (asyncTaskMap) {
758                                asyncTaskMap.remove(key);
759                            }
760                        }
761                    }
762                } else {
763                    doAcknowledge(context, subscriptionKey, messageId, ack);
764                }
765            } else {
766                doAcknowledge(context, subscriptionKey, messageId, ack);
767            }
768        }
769
770        protected void doAcknowledge(ConnectionContext context, String subscriptionKey, MessageId messageId, MessageAck ack)
771                throws IOException {
772            KahaRemoveMessageCommand command = new KahaRemoveMessageCommand();
773            command.setDestination(dest);
774            command.setSubscriptionKey(subscriptionKey);
775            command.setMessageId(messageId.toProducerKey());
776            command.setTransactionInfo(ack != null ? TransactionIdConversion.convert(transactionIdTransformer.transform(ack.getTransactionId())) : null);
777            if (ack != null && ack.isUnmatchedAck()) {
778                command.setAck(UNMATCHED);
779            } else {
780                org.apache.activemq.util.ByteSequence packet = wireFormat.marshal(ack);
781                command.setAck(new Buffer(packet.getData(), packet.getOffset(), packet.getLength()));
782            }
783            store(command, false, null, null);
784        }
785
786        @Override
787        public void addSubscription(SubscriptionInfo subscriptionInfo, boolean retroactive) throws IOException {
788            String subscriptionKey = subscriptionKey(subscriptionInfo.getClientId(), subscriptionInfo
789                    .getSubscriptionName());
790            KahaSubscriptionCommand command = new KahaSubscriptionCommand();
791            command.setDestination(dest);
792            command.setSubscriptionKey(subscriptionKey.toString());
793            command.setRetroactive(retroactive);
794            org.apache.activemq.util.ByteSequence packet = wireFormat.marshal(subscriptionInfo);
795            command.setSubscriptionInfo(new Buffer(packet.getData(), packet.getOffset(), packet.getLength()));
796            store(command, isEnableJournalDiskSyncs() && true, null, null);
797            this.subscriptionCount.incrementAndGet();
798        }
799
800        @Override
801        public void deleteSubscription(String clientId, String subscriptionName) throws IOException {
802            KahaSubscriptionCommand command = new KahaSubscriptionCommand();
803            command.setDestination(dest);
804            command.setSubscriptionKey(subscriptionKey(clientId, subscriptionName).toString());
805            store(command, isEnableJournalDiskSyncs() && true, null, null);
806            this.subscriptionCount.decrementAndGet();
807        }
808
809        @Override
810        public SubscriptionInfo[] getAllSubscriptions() throws IOException {
811
812            final ArrayList<SubscriptionInfo> subscriptions = new ArrayList<SubscriptionInfo>();
813            indexLock.writeLock().lock();
814            try {
815                pageFile.tx().execute(new Transaction.Closure<IOException>() {
816                    @Override
817                    public void execute(Transaction tx) throws IOException {
818                        StoredDestination sd = getStoredDestination(dest, tx);
819                        for (Iterator<Entry<String, KahaSubscriptionCommand>> iterator = sd.subscriptions.iterator(tx); iterator
820                                .hasNext();) {
821                            Entry<String, KahaSubscriptionCommand> entry = iterator.next();
822                            SubscriptionInfo info = (SubscriptionInfo) wireFormat.unmarshal(new DataInputStream(entry
823                                    .getValue().getSubscriptionInfo().newInput()));
824                            subscriptions.add(info);
825
826                        }
827                    }
828                });
829            } finally {
830                indexLock.writeLock().unlock();
831            }
832
833            SubscriptionInfo[] rc = new SubscriptionInfo[subscriptions.size()];
834            subscriptions.toArray(rc);
835            return rc;
836        }
837
838        @Override
839        public SubscriptionInfo lookupSubscription(String clientId, String subscriptionName) throws IOException {
840            final String subscriptionKey = subscriptionKey(clientId, subscriptionName);
841            indexLock.writeLock().lock();
842            try {
843                return pageFile.tx().execute(new Transaction.CallableClosure<SubscriptionInfo, IOException>() {
844                    @Override
845                    public SubscriptionInfo execute(Transaction tx) throws IOException {
846                        StoredDestination sd = getStoredDestination(dest, tx);
847                        KahaSubscriptionCommand command = sd.subscriptions.get(tx, subscriptionKey);
848                        if (command == null) {
849                            return null;
850                        }
851                        return (SubscriptionInfo) wireFormat.unmarshal(new DataInputStream(command
852                                .getSubscriptionInfo().newInput()));
853                    }
854                });
855            } finally {
856                indexLock.writeLock().unlock();
857            }
858        }
859
860        @Override
861        public int getMessageCount(String clientId, String subscriptionName) throws IOException {
862            final String subscriptionKey = subscriptionKey(clientId, subscriptionName);
863            indexLock.writeLock().lock();
864            try {
865                return pageFile.tx().execute(new Transaction.CallableClosure<Integer, IOException>() {
866                    @Override
867                    public Integer execute(Transaction tx) throws IOException {
868                        StoredDestination sd = getStoredDestination(dest, tx);
869                        LastAck cursorPos = getLastAck(tx, sd, subscriptionKey);
870                        if (cursorPos == null) {
871                            // The subscription might not exist.
872                            return 0;
873                        }
874
875                        return (int) getStoredMessageCount(tx, sd, subscriptionKey);
876                    }
877                });
878            } finally {
879                indexLock.writeLock().unlock();
880            }
881        }
882
883        @Override
884        public void recoverSubscription(String clientId, String subscriptionName, final MessageRecoveryListener listener)
885                throws Exception {
886            final String subscriptionKey = subscriptionKey(clientId, subscriptionName);
887            @SuppressWarnings("unused")
888            final SubscriptionInfo info = lookupSubscription(clientId, subscriptionName);
889            indexLock.writeLock().lock();
890            try {
891                pageFile.tx().execute(new Transaction.Closure<Exception>() {
892                    @Override
893                    public void execute(Transaction tx) throws Exception {
894                        StoredDestination sd = getStoredDestination(dest, tx);
895                        LastAck cursorPos = getLastAck(tx, sd, subscriptionKey);
896                        sd.orderIndex.setBatch(tx, cursorPos);
897                        recoverRolledBackAcks(sd, tx, Integer.MAX_VALUE, listener);
898                        for (Iterator<Entry<Long, MessageKeys>> iterator = sd.orderIndex.iterator(tx); iterator
899                                .hasNext();) {
900                            Entry<Long, MessageKeys> entry = iterator.next();
901                            if (ackedAndPrepared.contains(entry.getValue().messageId)) {
902                                continue;
903                            }
904                            listener.recoverMessage(loadMessage(entry.getValue().location));
905                        }
906                        sd.orderIndex.resetCursorPosition();
907                    }
908                });
909            } finally {
910                indexLock.writeLock().unlock();
911            }
912        }
913
914        @Override
915        public void recoverNextMessages(String clientId, String subscriptionName, final int maxReturned,
916                final MessageRecoveryListener listener) throws Exception {
917            final String subscriptionKey = subscriptionKey(clientId, subscriptionName);
918            @SuppressWarnings("unused")
919            final SubscriptionInfo info = lookupSubscription(clientId, subscriptionName);
920            indexLock.writeLock().lock();
921            try {
922                pageFile.tx().execute(new Transaction.Closure<Exception>() {
923                    @Override
924                    public void execute(Transaction tx) throws Exception {
925                        StoredDestination sd = getStoredDestination(dest, tx);
926                        sd.orderIndex.resetCursorPosition();
927                        MessageOrderCursor moc = sd.subscriptionCursors.get(subscriptionKey);
928                        if (moc == null) {
929                            LastAck pos = getLastAck(tx, sd, subscriptionKey);
930                            if (pos == null) {
931                                // sub deleted
932                                return;
933                            }
934                            sd.orderIndex.setBatch(tx, pos);
935                            moc = sd.orderIndex.cursor;
936                        } else {
937                            sd.orderIndex.cursor.sync(moc);
938                        }
939
940                        Entry<Long, MessageKeys> entry = null;
941                        int counter = recoverRolledBackAcks(sd, tx, maxReturned, listener);
942                        for (Iterator<Entry<Long, MessageKeys>> iterator = sd.orderIndex.iterator(tx, moc); iterator
943                                .hasNext();) {
944                            entry = iterator.next();
945                            if (ackedAndPrepared.contains(entry.getValue().messageId)) {
946                                continue;
947                            }
948                            if (listener.recoverMessage(loadMessage(entry.getValue().location))) {
949                                counter++;
950                            }
951                            if (counter >= maxReturned || listener.hasSpace() == false) {
952                                break;
953                            }
954                        }
955                        sd.orderIndex.stoppedIterating();
956                        if (entry != null) {
957                            MessageOrderCursor copy = sd.orderIndex.cursor.copy();
958                            sd.subscriptionCursors.put(subscriptionKey, copy);
959                        }
960                    }
961                });
962            } finally {
963                indexLock.writeLock().unlock();
964            }
965        }
966
967        @Override
968        public void resetBatching(String clientId, String subscriptionName) {
969            try {
970                final String subscriptionKey = subscriptionKey(clientId, subscriptionName);
971                indexLock.writeLock().lock();
972                try {
973                    pageFile.tx().execute(new Transaction.Closure<IOException>() {
974                        @Override
975                        public void execute(Transaction tx) throws IOException {
976                            StoredDestination sd = getStoredDestination(dest, tx);
977                            sd.subscriptionCursors.remove(subscriptionKey);
978                        }
979                    });
980                }finally {
981                    indexLock.writeLock().unlock();
982                }
983            } catch (IOException e) {
984                throw new RuntimeException(e);
985            }
986        }
987    }
988
989    String subscriptionKey(String clientId, String subscriptionName) {
990        return clientId + ":" + subscriptionName;
991    }
992
993    @Override
994    public MessageStore createQueueMessageStore(ActiveMQQueue destination) throws IOException {
995        return this.transactionStore.proxy(new KahaDBMessageStore(destination));
996    }
997
998    @Override
999    public TopicMessageStore createTopicMessageStore(ActiveMQTopic destination) throws IOException {
1000        return this.transactionStore.proxy(new KahaDBTopicMessageStore(destination));
1001    }
1002
1003    /**
1004     * Cleanup method to remove any state associated with the given destination.
1005     * This method does not stop the message store (it might not be cached).
1006     *
1007     * @param destination
1008     *            Destination to forget
1009     */
1010    @Override
1011    public void removeQueueMessageStore(ActiveMQQueue destination) {
1012    }
1013
1014    /**
1015     * Cleanup method to remove any state associated with the given destination
1016     * This method does not stop the message store (it might not be cached).
1017     *
1018     * @param destination
1019     *            Destination to forget
1020     */
1021    @Override
1022    public void removeTopicMessageStore(ActiveMQTopic destination) {
1023    }
1024
1025    @Override
1026    public void deleteAllMessages() throws IOException {
1027        deleteAllMessages = true;
1028    }
1029
1030    @Override
1031    public Set<ActiveMQDestination> getDestinations() {
1032        try {
1033            final HashSet<ActiveMQDestination> rc = new HashSet<ActiveMQDestination>();
1034            indexLock.writeLock().lock();
1035            try {
1036                pageFile.tx().execute(new Transaction.Closure<IOException>() {
1037                    @Override
1038                    public void execute(Transaction tx) throws IOException {
1039                        for (Iterator<Entry<String, StoredDestination>> iterator = metadata.destinations.iterator(tx); iterator
1040                                .hasNext();) {
1041                            Entry<String, StoredDestination> entry = iterator.next();
1042                            if (!isEmptyTopic(entry, tx)) {
1043                                rc.add(convert(entry.getKey()));
1044                            }
1045                        }
1046                    }
1047
1048                    private boolean isEmptyTopic(Entry<String, StoredDestination> entry, Transaction tx)
1049                            throws IOException {
1050                        boolean isEmptyTopic = false;
1051                        ActiveMQDestination dest = convert(entry.getKey());
1052                        if (dest.isTopic()) {
1053                            StoredDestination loadedStore = getStoredDestination(convert(dest), tx);
1054                            if (loadedStore.subscriptionAcks.isEmpty(tx)) {
1055                                isEmptyTopic = true;
1056                            }
1057                        }
1058                        return isEmptyTopic;
1059                    }
1060                });
1061            }finally {
1062                indexLock.writeLock().unlock();
1063            }
1064            return rc;
1065        } catch (IOException e) {
1066            throw new RuntimeException(e);
1067        }
1068    }
1069
1070    @Override
1071    public long getLastMessageBrokerSequenceId() throws IOException {
1072        return 0;
1073    }
1074
1075    @Override
1076    public long getLastProducerSequenceId(ProducerId id) {
1077        indexLock.readLock().lock();
1078        try {
1079            return metadata.producerSequenceIdTracker.getLastSeqId(id);
1080        } finally {
1081            indexLock.readLock().unlock();
1082        }
1083    }
1084
1085    @Override
1086    public long size() {
1087        try {
1088            return journalSize.get() + getPageFile().getDiskSize();
1089        } catch (IOException e) {
1090            throw new RuntimeException(e);
1091        }
1092    }
1093
1094    @Override
1095    public void beginTransaction(ConnectionContext context) throws IOException {
1096        throw new IOException("Not yet implemented.");
1097    }
1098    @Override
1099    public void commitTransaction(ConnectionContext context) throws IOException {
1100        throw new IOException("Not yet implemented.");
1101    }
1102    @Override
1103    public void rollbackTransaction(ConnectionContext context) throws IOException {
1104        throw new IOException("Not yet implemented.");
1105    }
1106
1107    @Override
1108    public void checkpoint(boolean sync) throws IOException {
1109        super.checkpointCleanup(sync);
1110    }
1111
1112    // /////////////////////////////////////////////////////////////////
1113    // Internal helper methods.
1114    // /////////////////////////////////////////////////////////////////
1115
1116    /**
1117     * @param location
1118     * @return
1119     * @throws IOException
1120     */
1121    Message loadMessage(Location location) throws IOException {
1122        JournalCommand<?> command = load(location);
1123        KahaAddMessageCommand addMessage = null;
1124        switch (command.type()) {
1125            case KAHA_UPDATE_MESSAGE_COMMAND:
1126                addMessage = ((KahaUpdateMessageCommand)command).getMessage();
1127                break;
1128            default:
1129                addMessage = (KahaAddMessageCommand) command;
1130        }
1131        Message msg = (Message) wireFormat.unmarshal(new DataInputStream(addMessage.getMessage().newInput()));
1132        return msg;
1133    }
1134
1135    // /////////////////////////////////////////////////////////////////
1136    // Internal conversion methods.
1137    // /////////////////////////////////////////////////////////////////
1138
1139    KahaLocation convert(Location location) {
1140        KahaLocation rc = new KahaLocation();
1141        rc.setLogId(location.getDataFileId());
1142        rc.setOffset(location.getOffset());
1143        return rc;
1144    }
1145
1146    KahaDestination convert(ActiveMQDestination dest) {
1147        KahaDestination rc = new KahaDestination();
1148        rc.setName(dest.getPhysicalName());
1149        switch (dest.getDestinationType()) {
1150        case ActiveMQDestination.QUEUE_TYPE:
1151            rc.setType(DestinationType.QUEUE);
1152            return rc;
1153        case ActiveMQDestination.TOPIC_TYPE:
1154            rc.setType(DestinationType.TOPIC);
1155            return rc;
1156        case ActiveMQDestination.TEMP_QUEUE_TYPE:
1157            rc.setType(DestinationType.TEMP_QUEUE);
1158            return rc;
1159        case ActiveMQDestination.TEMP_TOPIC_TYPE:
1160            rc.setType(DestinationType.TEMP_TOPIC);
1161            return rc;
1162        default:
1163            return null;
1164        }
1165    }
1166
1167    ActiveMQDestination convert(String dest) {
1168        int p = dest.indexOf(":");
1169        if (p < 0) {
1170            throw new IllegalArgumentException("Not in the valid destination format");
1171        }
1172        int type = Integer.parseInt(dest.substring(0, p));
1173        String name = dest.substring(p + 1);
1174        return convert(type, name);
1175    }
1176
1177    private ActiveMQDestination convert(KahaDestination commandDestination) {
1178        return convert(commandDestination.getType().getNumber(), commandDestination.getName());
1179    }
1180
1181    private ActiveMQDestination convert(int type, String name) {
1182        switch (KahaDestination.DestinationType.valueOf(type)) {
1183        case QUEUE:
1184            return new ActiveMQQueue(name);
1185        case TOPIC:
1186            return new ActiveMQTopic(name);
1187        case TEMP_QUEUE:
1188            return new ActiveMQTempQueue(name);
1189        case TEMP_TOPIC:
1190            return new ActiveMQTempTopic(name);
1191        default:
1192            throw new IllegalArgumentException("Not in the valid destination format");
1193        }
1194    }
1195
1196    public TransactionIdTransformer getTransactionIdTransformer() {
1197        return transactionIdTransformer;
1198    }
1199
1200    public void setTransactionIdTransformer(TransactionIdTransformer transactionIdTransformer) {
1201        this.transactionIdTransformer = transactionIdTransformer;
1202    }
1203
1204    static class AsyncJobKey {
1205        MessageId id;
1206        ActiveMQDestination destination;
1207
1208        AsyncJobKey(MessageId id, ActiveMQDestination destination) {
1209            this.id = id;
1210            this.destination = destination;
1211        }
1212
1213        @Override
1214        public boolean equals(Object obj) {
1215            if (obj == this) {
1216                return true;
1217            }
1218            return obj instanceof AsyncJobKey && id.equals(((AsyncJobKey) obj).id)
1219                    && destination.equals(((AsyncJobKey) obj).destination);
1220        }
1221
1222        @Override
1223        public int hashCode() {
1224            return id.hashCode() + destination.hashCode();
1225        }
1226
1227        @Override
1228        public String toString() {
1229            return destination.getPhysicalName() + "-" + id;
1230        }
1231    }
1232
1233    public interface StoreTask {
1234        public boolean cancel();
1235
1236        public void aquireLocks();
1237
1238        public void releaseLocks();
1239    }
1240
1241    class StoreQueueTask implements Runnable, StoreTask {
1242        protected final Message message;
1243        protected final ConnectionContext context;
1244        protected final KahaDBMessageStore store;
1245        protected final InnerFutureTask future;
1246        protected final AtomicBoolean done = new AtomicBoolean();
1247        protected final AtomicBoolean locked = new AtomicBoolean();
1248
1249        public StoreQueueTask(KahaDBMessageStore store, ConnectionContext context, Message message) {
1250            this.store = store;
1251            this.context = context;
1252            this.message = message;
1253            this.future = new InnerFutureTask(this);
1254        }
1255
1256        public ListenableFuture<Object> getFuture() {
1257            return this.future;
1258        }
1259
1260        @Override
1261        public boolean cancel() {
1262            if (this.done.compareAndSet(false, true)) {
1263                return this.future.cancel(false);
1264            }
1265            return false;
1266        }
1267
1268        @Override
1269        public void aquireLocks() {
1270            if (this.locked.compareAndSet(false, true)) {
1271                try {
1272                    globalQueueSemaphore.acquire();
1273                    store.acquireLocalAsyncLock();
1274                    message.incrementReferenceCount();
1275                } catch (InterruptedException e) {
1276                    LOG.warn("Failed to aquire lock", e);
1277                }
1278            }
1279
1280        }
1281
1282        @Override
1283        public void releaseLocks() {
1284            if (this.locked.compareAndSet(true, false)) {
1285                store.releaseLocalAsyncLock();
1286                globalQueueSemaphore.release();
1287                message.decrementReferenceCount();
1288            }
1289        }
1290
1291        @Override
1292        public void run() {
1293            this.store.doneTasks++;
1294            try {
1295                if (this.done.compareAndSet(false, true)) {
1296                    this.store.addMessage(context, message);
1297                    removeQueueTask(this.store, this.message.getMessageId());
1298                    this.future.complete();
1299                } else if (cancelledTaskModMetric > 0 && this.store.canceledTasks++ % cancelledTaskModMetric == 0) {
1300                    System.err.println(this.store.dest.getName() + " cancelled: "
1301                            + (this.store.canceledTasks / this.store.doneTasks) * 100);
1302                    this.store.canceledTasks = this.store.doneTasks = 0;
1303                }
1304            } catch (Exception e) {
1305                this.future.setException(e);
1306            }
1307        }
1308
1309        protected Message getMessage() {
1310            return this.message;
1311        }
1312
1313        private class InnerFutureTask extends FutureTask<Object> implements ListenableFuture<Object>  {
1314
1315            private Runnable listener;
1316            public InnerFutureTask(Runnable runnable) {
1317                super(runnable, null);
1318
1319            }
1320
1321            public void setException(final Exception e) {
1322                super.setException(e);
1323            }
1324
1325            public void complete() {
1326                super.set(null);
1327            }
1328
1329            @Override
1330            public void done() {
1331                fireListener();
1332            }
1333
1334            @Override
1335            public void addListener(Runnable listener) {
1336                this.listener = listener;
1337                if (isDone()) {
1338                    fireListener();
1339                }
1340            }
1341
1342            private void fireListener() {
1343                if (listener != null) {
1344                    try {
1345                        listener.run();
1346                    } catch (Exception ignored) {
1347                        LOG.warn("Unexpected exception from future {} listener callback {}", this, listener, ignored);
1348                    }
1349                }
1350            }
1351        }
1352    }
1353
1354    class StoreTopicTask extends StoreQueueTask {
1355        private final int subscriptionCount;
1356        private final List<String> subscriptionKeys = new ArrayList<String>(1);
1357        private final KahaDBTopicMessageStore topicStore;
1358        public StoreTopicTask(KahaDBTopicMessageStore store, ConnectionContext context, Message message,
1359                int subscriptionCount) {
1360            super(store, context, message);
1361            this.topicStore = store;
1362            this.subscriptionCount = subscriptionCount;
1363
1364        }
1365
1366        @Override
1367        public void aquireLocks() {
1368            if (this.locked.compareAndSet(false, true)) {
1369                try {
1370                    globalTopicSemaphore.acquire();
1371                    store.acquireLocalAsyncLock();
1372                    message.incrementReferenceCount();
1373                } catch (InterruptedException e) {
1374                    LOG.warn("Failed to aquire lock", e);
1375                }
1376            }
1377        }
1378
1379        @Override
1380        public void releaseLocks() {
1381            if (this.locked.compareAndSet(true, false)) {
1382                message.decrementReferenceCount();
1383                store.releaseLocalAsyncLock();
1384                globalTopicSemaphore.release();
1385            }
1386        }
1387
1388        /**
1389         * add a key
1390         *
1391         * @param key
1392         * @return true if all acknowledgements received
1393         */
1394        public boolean addSubscriptionKey(String key) {
1395            synchronized (this.subscriptionKeys) {
1396                this.subscriptionKeys.add(key);
1397            }
1398            return this.subscriptionKeys.size() >= this.subscriptionCount;
1399        }
1400
1401        @Override
1402        public void run() {
1403            this.store.doneTasks++;
1404            try {
1405                if (this.done.compareAndSet(false, true)) {
1406                    this.topicStore.addMessage(context, message);
1407                    // apply any acks we have
1408                    synchronized (this.subscriptionKeys) {
1409                        for (String key : this.subscriptionKeys) {
1410                            this.topicStore.doAcknowledge(context, key, this.message.getMessageId(), null);
1411
1412                        }
1413                    }
1414                    removeTopicTask(this.topicStore, this.message.getMessageId());
1415                    this.future.complete();
1416                } else if (cancelledTaskModMetric > 0 && this.store.canceledTasks++ % cancelledTaskModMetric == 0) {
1417                    System.err.println(this.store.dest.getName() + " cancelled: "
1418                            + (this.store.canceledTasks / this.store.doneTasks) * 100);
1419                    this.store.canceledTasks = this.store.doneTasks = 0;
1420                }
1421            } catch (Exception e) {
1422                this.future.setException(e);
1423            }
1424        }
1425    }
1426
1427    public class StoreTaskExecutor extends ThreadPoolExecutor {
1428
1429        public StoreTaskExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit timeUnit, BlockingQueue<Runnable> queue, ThreadFactory threadFactory) {
1430            super(corePoolSize, maximumPoolSize, keepAliveTime, timeUnit, queue, threadFactory);
1431        }
1432
1433        @Override
1434        protected void afterExecute(Runnable runnable, Throwable throwable) {
1435            super.afterExecute(runnable, throwable);
1436
1437            if (runnable instanceof StoreTask) {
1438               ((StoreTask)runnable).releaseLocks();
1439            }
1440        }
1441    }
1442
1443    @Override
1444    public JobSchedulerStore createJobSchedulerStore() throws IOException, UnsupportedOperationException {
1445        return new JobSchedulerStoreImpl();
1446    }
1447}