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