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.ByteArrayInputStream;
020import java.io.ByteArrayOutputStream;
021import java.io.DataInput;
022import java.io.DataOutput;
023import java.io.EOFException;
024import java.io.File;
025import java.io.IOException;
026import java.io.InputStream;
027import java.io.InterruptedIOException;
028import java.io.ObjectInputStream;
029import java.io.ObjectOutputStream;
030import java.io.OutputStream;
031import java.util.ArrayList;
032import java.util.Arrays;
033import java.util.Collection;
034import java.util.Collections;
035import java.util.Date;
036import java.util.HashMap;
037import java.util.HashSet;
038import java.util.Iterator;
039import java.util.LinkedHashMap;
040import java.util.LinkedHashSet;
041import java.util.LinkedList;
042import java.util.List;
043import java.util.Map;
044import java.util.Map.Entry;
045import java.util.Set;
046import java.util.SortedSet;
047import java.util.TreeMap;
048import java.util.TreeSet;
049import java.util.concurrent.ConcurrentHashMap;
050import java.util.concurrent.ConcurrentMap;
051import java.util.concurrent.atomic.AtomicBoolean;
052import java.util.concurrent.atomic.AtomicLong;
053import java.util.concurrent.locks.ReentrantReadWriteLock;
054
055import org.apache.activemq.ActiveMQMessageAuditNoSync;
056import org.apache.activemq.broker.BrokerService;
057import org.apache.activemq.broker.BrokerServiceAware;
058import org.apache.activemq.broker.region.Destination;
059import org.apache.activemq.broker.region.Queue;
060import org.apache.activemq.broker.region.Topic;
061import org.apache.activemq.command.MessageAck;
062import org.apache.activemq.command.TransactionId;
063import org.apache.activemq.openwire.OpenWireFormat;
064import org.apache.activemq.protobuf.Buffer;
065import org.apache.activemq.store.MessageStore;
066import org.apache.activemq.store.MessageStoreStatistics;
067import org.apache.activemq.store.kahadb.data.KahaAckMessageFileMapCommand;
068import org.apache.activemq.store.kahadb.data.KahaAddMessageCommand;
069import org.apache.activemq.store.kahadb.data.KahaCommitCommand;
070import org.apache.activemq.store.kahadb.data.KahaDestination;
071import org.apache.activemq.store.kahadb.data.KahaEntryType;
072import org.apache.activemq.store.kahadb.data.KahaPrepareCommand;
073import org.apache.activemq.store.kahadb.data.KahaProducerAuditCommand;
074import org.apache.activemq.store.kahadb.data.KahaRemoveDestinationCommand;
075import org.apache.activemq.store.kahadb.data.KahaRemoveMessageCommand;
076import org.apache.activemq.store.kahadb.data.KahaRollbackCommand;
077import org.apache.activemq.store.kahadb.data.KahaSubscriptionCommand;
078import org.apache.activemq.store.kahadb.data.KahaTraceCommand;
079import org.apache.activemq.store.kahadb.data.KahaTransactionInfo;
080import org.apache.activemq.store.kahadb.data.KahaUpdateMessageCommand;
081import org.apache.activemq.store.kahadb.disk.index.BTreeIndex;
082import org.apache.activemq.store.kahadb.disk.index.BTreeVisitor;
083import org.apache.activemq.store.kahadb.disk.index.ListIndex;
084import org.apache.activemq.store.kahadb.disk.journal.DataFile;
085import org.apache.activemq.store.kahadb.disk.journal.Journal;
086import org.apache.activemq.store.kahadb.disk.journal.Location;
087import org.apache.activemq.store.kahadb.disk.page.Page;
088import org.apache.activemq.store.kahadb.disk.page.PageFile;
089import org.apache.activemq.store.kahadb.disk.page.Transaction;
090import org.apache.activemq.store.kahadb.disk.util.LocationMarshaller;
091import org.apache.activemq.store.kahadb.disk.util.LongMarshaller;
092import org.apache.activemq.store.kahadb.disk.util.Marshaller;
093import org.apache.activemq.store.kahadb.disk.util.Sequence;
094import org.apache.activemq.store.kahadb.disk.util.SequenceSet;
095import org.apache.activemq.store.kahadb.disk.util.StringMarshaller;
096import org.apache.activemq.store.kahadb.disk.util.VariableMarshaller;
097import org.apache.activemq.util.ByteSequence;
098import org.apache.activemq.util.DataByteArrayInputStream;
099import org.apache.activemq.util.DataByteArrayOutputStream;
100import org.apache.activemq.util.IOHelper;
101import org.apache.activemq.util.ServiceStopper;
102import org.apache.activemq.util.ServiceSupport;
103import org.slf4j.Logger;
104import org.slf4j.LoggerFactory;
105
106public abstract class MessageDatabase extends ServiceSupport implements BrokerServiceAware {
107
108    protected BrokerService brokerService;
109
110    public static final String PROPERTY_LOG_SLOW_ACCESS_TIME = "org.apache.activemq.store.kahadb.LOG_SLOW_ACCESS_TIME";
111    public static final int LOG_SLOW_ACCESS_TIME = Integer.getInteger(PROPERTY_LOG_SLOW_ACCESS_TIME, 0);
112    public static final File DEFAULT_DIRECTORY = new File("KahaDB");
113    protected static final Buffer UNMATCHED;
114    static {
115        UNMATCHED = new Buffer(new byte[]{});
116    }
117    private static final Logger LOG = LoggerFactory.getLogger(MessageDatabase.class);
118
119    static final int CLOSED_STATE = 1;
120    static final int OPEN_STATE = 2;
121    static final long NOT_ACKED = -1;
122
123    static final int VERSION = 6;
124
125    protected class Metadata {
126        protected Page<Metadata> page;
127        protected int state;
128        protected BTreeIndex<String, StoredDestination> destinations;
129        protected Location lastUpdate;
130        protected Location firstInProgressTransactionLocation;
131        protected Location producerSequenceIdTrackerLocation = null;
132        protected Location ackMessageFileMapLocation = null;
133        protected transient ActiveMQMessageAuditNoSync producerSequenceIdTracker = new ActiveMQMessageAuditNoSync();
134        protected transient Map<Integer, Set<Integer>> ackMessageFileMap = new HashMap<Integer, Set<Integer>>();
135        protected int version = VERSION;
136        protected int openwireVersion = OpenWireFormat.DEFAULT_STORE_VERSION;
137
138        public void read(DataInput is) throws IOException {
139            state = is.readInt();
140            destinations = new BTreeIndex<String, StoredDestination>(pageFile, is.readLong());
141            if (is.readBoolean()) {
142                lastUpdate = LocationMarshaller.INSTANCE.readPayload(is);
143            } else {
144                lastUpdate = null;
145            }
146            if (is.readBoolean()) {
147                firstInProgressTransactionLocation = LocationMarshaller.INSTANCE.readPayload(is);
148            } else {
149                firstInProgressTransactionLocation = null;
150            }
151            try {
152                if (is.readBoolean()) {
153                    producerSequenceIdTrackerLocation = LocationMarshaller.INSTANCE.readPayload(is);
154                } else {
155                    producerSequenceIdTrackerLocation = null;
156                }
157            } catch (EOFException expectedOnUpgrade) {
158            }
159            try {
160                version = is.readInt();
161            } catch (EOFException expectedOnUpgrade) {
162                version = 1;
163            }
164            if (version >= 5 && is.readBoolean()) {
165                ackMessageFileMapLocation = LocationMarshaller.INSTANCE.readPayload(is);
166            } else {
167                ackMessageFileMapLocation = null;
168            }
169            try {
170                openwireVersion = is.readInt();
171            } catch (EOFException expectedOnUpgrade) {
172                openwireVersion = OpenWireFormat.DEFAULT_LEGACY_VERSION;
173            }
174            LOG.info("KahaDB is version " + version);
175        }
176
177        public void write(DataOutput os) throws IOException {
178            os.writeInt(state);
179            os.writeLong(destinations.getPageId());
180
181            if (lastUpdate != null) {
182                os.writeBoolean(true);
183                LocationMarshaller.INSTANCE.writePayload(lastUpdate, os);
184            } else {
185                os.writeBoolean(false);
186            }
187
188            if (firstInProgressTransactionLocation != null) {
189                os.writeBoolean(true);
190                LocationMarshaller.INSTANCE.writePayload(firstInProgressTransactionLocation, os);
191            } else {
192                os.writeBoolean(false);
193            }
194
195            if (producerSequenceIdTrackerLocation != null) {
196                os.writeBoolean(true);
197                LocationMarshaller.INSTANCE.writePayload(producerSequenceIdTrackerLocation, os);
198            } else {
199                os.writeBoolean(false);
200            }
201            os.writeInt(VERSION);
202            if (ackMessageFileMapLocation != null) {
203                os.writeBoolean(true);
204                LocationMarshaller.INSTANCE.writePayload(ackMessageFileMapLocation, os);
205            } else {
206                os.writeBoolean(false);
207            }
208            os.writeInt(this.openwireVersion);
209        }
210    }
211
212    class MetadataMarshaller extends VariableMarshaller<Metadata> {
213        @Override
214        public Metadata readPayload(DataInput dataIn) throws IOException {
215            Metadata rc = createMetadata();
216            rc.read(dataIn);
217            return rc;
218        }
219
220        @Override
221        public void writePayload(Metadata object, DataOutput dataOut) throws IOException {
222            object.write(dataOut);
223        }
224    }
225
226    protected PageFile pageFile;
227    protected Journal journal;
228    protected Metadata metadata = new Metadata();
229
230    protected MetadataMarshaller metadataMarshaller = new MetadataMarshaller();
231
232    protected boolean failIfDatabaseIsLocked;
233
234    protected boolean deleteAllMessages;
235    protected File directory = DEFAULT_DIRECTORY;
236    protected File indexDirectory = null;
237    protected Thread checkpointThread;
238    protected boolean enableJournalDiskSyncs=true;
239    protected boolean archiveDataLogs;
240    protected File directoryArchive;
241    protected AtomicLong journalSize = new AtomicLong(0);
242    long checkpointInterval = 5*1000;
243    long cleanupInterval = 30*1000;
244    int journalMaxFileLength = Journal.DEFAULT_MAX_FILE_LENGTH;
245    int journalMaxWriteBatchSize = Journal.DEFAULT_MAX_WRITE_BATCH_SIZE;
246    boolean enableIndexWriteAsync = false;
247    int setIndexWriteBatchSize = PageFile.DEFAULT_WRITE_BATCH_SIZE;
248    private String preallocationScope = Journal.PreallocationScope.ENTIRE_JOURNAL.name();
249    private String preallocationStrategy = Journal.PreallocationStrategy.SPARSE_FILE.name();
250
251    protected AtomicBoolean opened = new AtomicBoolean();
252    private boolean ignoreMissingJournalfiles = false;
253    private int indexCacheSize = 10000;
254    private boolean checkForCorruptJournalFiles = false;
255    private boolean checksumJournalFiles = true;
256    protected boolean forceRecoverIndex = false;
257    private final Object checkpointThreadLock = new Object();
258    private boolean archiveCorruptedIndex = false;
259    private boolean useIndexLFRUEviction = false;
260    private float indexLFUEvictionFactor = 0.2f;
261    private boolean enableIndexDiskSyncs = true;
262    private boolean enableIndexRecoveryFile = true;
263    private boolean enableIndexPageCaching = true;
264    ReentrantReadWriteLock checkpointLock = new ReentrantReadWriteLock();
265
266    @Override
267    public void doStart() throws Exception {
268        load();
269    }
270
271    @Override
272    public void doStop(ServiceStopper stopper) throws Exception {
273        unload();
274    }
275
276    private void loadPageFile() throws IOException {
277        this.indexLock.writeLock().lock();
278        try {
279            final PageFile pageFile = getPageFile();
280            pageFile.load();
281            pageFile.tx().execute(new Transaction.Closure<IOException>() {
282                @Override
283                public void execute(Transaction tx) throws IOException {
284                    if (pageFile.getPageCount() == 0) {
285                        // First time this is created.. Initialize the metadata
286                        Page<Metadata> page = tx.allocate();
287                        assert page.getPageId() == 0;
288                        page.set(metadata);
289                        metadata.page = page;
290                        metadata.state = CLOSED_STATE;
291                        metadata.destinations = new BTreeIndex<String, StoredDestination>(pageFile, tx.allocate().getPageId());
292
293                        tx.store(metadata.page, metadataMarshaller, true);
294                    } else {
295                        Page<Metadata> page = tx.load(0, metadataMarshaller);
296                        metadata = page.get();
297                        metadata.page = page;
298                    }
299                    metadata.destinations.setKeyMarshaller(StringMarshaller.INSTANCE);
300                    metadata.destinations.setValueMarshaller(new StoredDestinationMarshaller());
301                    metadata.destinations.load(tx);
302                }
303            });
304            // Load up all the destinations since we need to scan all the indexes to figure out which journal files can be deleted.
305            // Perhaps we should just keep an index of file
306            storedDestinations.clear();
307            pageFile.tx().execute(new Transaction.Closure<IOException>() {
308                @Override
309                public void execute(Transaction tx) throws IOException {
310                    for (Iterator<Entry<String, StoredDestination>> iterator = metadata.destinations.iterator(tx); iterator.hasNext();) {
311                        Entry<String, StoredDestination> entry = iterator.next();
312                        StoredDestination sd = loadStoredDestination(tx, entry.getKey(), entry.getValue().subscriptions!=null);
313                        storedDestinations.put(entry.getKey(), sd);
314
315                        if (checkForCorruptJournalFiles) {
316                            // sanity check the index also
317                            if (!entry.getValue().locationIndex.isEmpty(tx)) {
318                                if (entry.getValue().orderIndex.nextMessageId <= 0) {
319                                    throw new IOException("Detected uninitialized orderIndex nextMessageId with pending messages for " + entry.getKey());
320                                }
321                            }
322                        }
323                    }
324                }
325            });
326            pageFile.flush();
327        } finally {
328            this.indexLock.writeLock().unlock();
329        }
330    }
331
332    private void startCheckpoint() {
333        if (checkpointInterval == 0 &&  cleanupInterval == 0) {
334            LOG.info("periodic checkpoint/cleanup disabled, will ocurr on clean shutdown/restart");
335            return;
336        }
337        synchronized (checkpointThreadLock) {
338            boolean start = false;
339            if (checkpointThread == null) {
340                start = true;
341            } else if (!checkpointThread.isAlive()) {
342                start = true;
343                LOG.info("KahaDB: Recovering checkpoint thread after death");
344            }
345            if (start) {
346                checkpointThread = new Thread("ActiveMQ Journal Checkpoint Worker") {
347                    @Override
348                    public void run() {
349                        try {
350                            long lastCleanup = System.currentTimeMillis();
351                            long lastCheckpoint = System.currentTimeMillis();
352                            // Sleep for a short time so we can periodically check
353                            // to see if we need to exit this thread.
354                            long sleepTime = Math.min(checkpointInterval > 0 ? checkpointInterval : cleanupInterval, 500);
355                            while (opened.get()) {
356                                Thread.sleep(sleepTime);
357                                long now = System.currentTimeMillis();
358                                if( cleanupInterval > 0 && (now - lastCleanup >= cleanupInterval) ) {
359                                    checkpointCleanup(true);
360                                    lastCleanup = now;
361                                    lastCheckpoint = now;
362                                } else if( checkpointInterval > 0 && (now - lastCheckpoint >= checkpointInterval )) {
363                                    checkpointCleanup(false);
364                                    lastCheckpoint = now;
365                                }
366                            }
367                        } catch (InterruptedException e) {
368                            // Looks like someone really wants us to exit this thread...
369                        } catch (IOException ioe) {
370                            LOG.error("Checkpoint failed", ioe);
371                            brokerService.handleIOException(ioe);
372                        }
373                    }
374                };
375
376                checkpointThread.setDaemon(true);
377                checkpointThread.start();
378            }
379        }
380    }
381
382    public void open() throws IOException {
383        if( opened.compareAndSet(false, true) ) {
384            getJournal().start();
385            try {
386                loadPageFile();
387            } catch (Throwable t) {
388                LOG.warn("Index corrupted. Recovering the index through journal replay. Cause:" + t);
389                if (LOG.isDebugEnabled()) {
390                    LOG.debug("Index load failure", t);
391                }
392                // try to recover index
393                try {
394                    pageFile.unload();
395                } catch (Exception ignore) {}
396                if (archiveCorruptedIndex) {
397                    pageFile.archive();
398                } else {
399                    pageFile.delete();
400                }
401                metadata = createMetadata();
402                pageFile = null;
403                loadPageFile();
404            }
405            startCheckpoint();
406            recover();
407        }
408    }
409
410    public void load() throws IOException {
411        this.indexLock.writeLock().lock();
412        IOHelper.mkdirs(directory);
413        try {
414            if (deleteAllMessages) {
415                getJournal().start();
416                getJournal().delete();
417                getJournal().close();
418                journal = null;
419                getPageFile().delete();
420                LOG.info("Persistence store purged.");
421                deleteAllMessages = false;
422            }
423
424            open();
425            store(new KahaTraceCommand().setMessage("LOADED " + new Date()));
426        } finally {
427            this.indexLock.writeLock().unlock();
428        }
429    }
430
431    public void close() throws IOException, InterruptedException {
432        if( opened.compareAndSet(true, false)) {
433            checkpointLock.writeLock().lock();
434            try {
435                if (metadata.page != null) {
436                    checkpointUpdate(true);
437                }
438                pageFile.unload();
439                metadata = createMetadata();
440            } finally {
441                checkpointLock.writeLock().unlock();
442            }
443            journal.close();
444            synchronized (checkpointThreadLock) {
445                if (checkpointThread != null) {
446                    checkpointThread.join();
447                }
448            }
449            //clear the cache on shutdown of the store
450            storeCache.clear();
451        }
452    }
453
454    public void unload() throws IOException, InterruptedException {
455        this.indexLock.writeLock().lock();
456        try {
457            if( pageFile != null && pageFile.isLoaded() ) {
458                metadata.state = CLOSED_STATE;
459                metadata.firstInProgressTransactionLocation = getInProgressTxLocationRange()[0];
460
461                if (metadata.page != null) {
462                    pageFile.tx().execute(new Transaction.Closure<IOException>() {
463                        @Override
464                        public void execute(Transaction tx) throws IOException {
465                            tx.store(metadata.page, metadataMarshaller, true);
466                        }
467                    });
468                }
469            }
470        } finally {
471            this.indexLock.writeLock().unlock();
472        }
473        close();
474    }
475
476    // public for testing
477    @SuppressWarnings("rawtypes")
478    public Location[] getInProgressTxLocationRange() {
479        Location[] range = new Location[]{null, null};
480        synchronized (inflightTransactions) {
481            if (!inflightTransactions.isEmpty()) {
482                for (List<Operation> ops : inflightTransactions.values()) {
483                    if (!ops.isEmpty()) {
484                        trackMaxAndMin(range, ops);
485                    }
486                }
487            }
488            if (!preparedTransactions.isEmpty()) {
489                for (List<Operation> ops : preparedTransactions.values()) {
490                    if (!ops.isEmpty()) {
491                        trackMaxAndMin(range, ops);
492                    }
493                }
494            }
495        }
496        return range;
497    }
498
499    @SuppressWarnings("rawtypes")
500    private void trackMaxAndMin(Location[] range, List<Operation> ops) {
501        Location t = ops.get(0).getLocation();
502        if (range[0]==null || t.compareTo(range[0]) <= 0) {
503            range[0] = t;
504        }
505        t = ops.get(ops.size() -1).getLocation();
506        if (range[1]==null || t.compareTo(range[1]) >= 0) {
507            range[1] = t;
508        }
509    }
510
511    class TranInfo {
512        TransactionId id;
513        Location location;
514
515        class opCount {
516            int add;
517            int remove;
518        }
519        HashMap<KahaDestination, opCount> destinationOpCount = new HashMap<KahaDestination, opCount>();
520
521        @SuppressWarnings("rawtypes")
522        public void track(Operation operation) {
523            if (location == null ) {
524                location = operation.getLocation();
525            }
526            KahaDestination destination;
527            boolean isAdd = false;
528            if (operation instanceof AddOperation) {
529                AddOperation add = (AddOperation) operation;
530                destination = add.getCommand().getDestination();
531                isAdd = true;
532            } else {
533                RemoveOperation removeOpperation = (RemoveOperation) operation;
534                destination = removeOpperation.getCommand().getDestination();
535            }
536            opCount opCount = destinationOpCount.get(destination);
537            if (opCount == null) {
538                opCount = new opCount();
539                destinationOpCount.put(destination, opCount);
540            }
541            if (isAdd) {
542                opCount.add++;
543            } else {
544                opCount.remove++;
545            }
546        }
547
548        @Override
549        public String toString() {
550           StringBuffer buffer = new StringBuffer();
551           buffer.append(location).append(";").append(id).append(";\n");
552           for (Entry<KahaDestination, opCount> op : destinationOpCount.entrySet()) {
553               buffer.append(op.getKey()).append('+').append(op.getValue().add).append(',').append('-').append(op.getValue().remove).append(';');
554           }
555           return buffer.toString();
556        }
557    }
558
559    @SuppressWarnings("rawtypes")
560    public String getTransactions() {
561
562        ArrayList<TranInfo> infos = new ArrayList<TranInfo>();
563        synchronized (inflightTransactions) {
564            if (!inflightTransactions.isEmpty()) {
565                for (Entry<TransactionId, List<Operation>> entry : inflightTransactions.entrySet()) {
566                    TranInfo info = new TranInfo();
567                    info.id = entry.getKey();
568                    for (Operation operation : entry.getValue()) {
569                        info.track(operation);
570                    }
571                    infos.add(info);
572                }
573            }
574        }
575        synchronized (preparedTransactions) {
576            if (!preparedTransactions.isEmpty()) {
577                for (Entry<TransactionId, List<Operation>> entry : preparedTransactions.entrySet()) {
578                    TranInfo info = new TranInfo();
579                    info.id = entry.getKey();
580                    for (Operation operation : entry.getValue()) {
581                        info.track(operation);
582                    }
583                    infos.add(info);
584                }
585            }
586        }
587        return infos.toString();
588    }
589
590    /**
591     * Move all the messages that were in the journal into long term storage. We
592     * just replay and do a checkpoint.
593     *
594     * @throws IOException
595     * @throws IOException
596     * @throws IllegalStateException
597     */
598    private void recover() throws IllegalStateException, IOException {
599        this.indexLock.writeLock().lock();
600        try {
601
602            long start = System.currentTimeMillis();
603            Location producerAuditPosition = recoverProducerAudit();
604            Location ackMessageFileLocation = recoverAckMessageFileMap();
605            Location lastIndoubtPosition = getRecoveryPosition();
606
607            Location recoveryPosition = minimum(producerAuditPosition, ackMessageFileLocation);
608            recoveryPosition = minimum(recoveryPosition, lastIndoubtPosition);
609
610            if (recoveryPosition != null) {
611                int redoCounter = 0;
612                LOG.info("Recovering from the journal @" + recoveryPosition);
613                while (recoveryPosition != null) {
614                    try {
615                        JournalCommand<?> message = load(recoveryPosition);
616                        metadata.lastUpdate = recoveryPosition;
617                        process(message, recoveryPosition, lastIndoubtPosition);
618                        redoCounter++;
619                    } catch (IOException failedRecovery) {
620                        if (isIgnoreMissingJournalfiles()) {
621                            LOG.debug("Failed to recover data at position:" + recoveryPosition, failedRecovery);
622                            // track this dud location
623                            journal.corruptRecoveryLocation(recoveryPosition);
624                        } else {
625                            throw new IOException("Failed to recover data at position:" + recoveryPosition, failedRecovery);
626                        }
627                    }
628                    recoveryPosition = journal.getNextLocation(recoveryPosition);
629                     if (LOG.isInfoEnabled() && redoCounter % 100000 == 0) {
630                         LOG.info("@" + recoveryPosition +  ", "  + redoCounter + " entries recovered ..");
631                     }
632                }
633                if (LOG.isInfoEnabled()) {
634                    long end = System.currentTimeMillis();
635                    LOG.info("Recovery replayed " + redoCounter + " operations from the journal in " + ((end - start) / 1000.0f) + " seconds.");
636                }
637            }
638
639            // We may have to undo some index updates.
640            pageFile.tx().execute(new Transaction.Closure<IOException>() {
641                @Override
642                public void execute(Transaction tx) throws IOException {
643                    recoverIndex(tx);
644                }
645            });
646
647            // rollback any recovered inflight local transactions, and discard any inflight XA transactions.
648            Set<TransactionId> toRollback = new HashSet<TransactionId>();
649            Set<TransactionId> toDiscard = new HashSet<TransactionId>();
650            synchronized (inflightTransactions) {
651                for (Iterator<TransactionId> it = inflightTransactions.keySet().iterator(); it.hasNext(); ) {
652                    TransactionId id = it.next();
653                    if (id.isLocalTransaction()) {
654                        toRollback.add(id);
655                    } else {
656                        toDiscard.add(id);
657                    }
658                }
659                for (TransactionId tx: toRollback) {
660                    if (LOG.isDebugEnabled()) {
661                        LOG.debug("rolling back recovered indoubt local transaction " + tx);
662                    }
663                    store(new KahaRollbackCommand().setTransactionInfo(TransactionIdConversion.convertToLocal(tx)), false, null, null);
664                }
665                for (TransactionId tx: toDiscard) {
666                    if (LOG.isDebugEnabled()) {
667                        LOG.debug("discarding recovered in-flight XA transaction " + tx);
668                    }
669                    inflightTransactions.remove(tx);
670                }
671            }
672
673            synchronized (preparedTransactions) {
674                for (TransactionId txId : preparedTransactions.keySet()) {
675                    LOG.warn("Recovered prepared XA TX: [{}]", txId);
676                }
677            }
678
679        } finally {
680            this.indexLock.writeLock().unlock();
681        }
682    }
683
684    @SuppressWarnings("unused")
685    private KahaTransactionInfo createLocalTransactionInfo(TransactionId tx) {
686        return TransactionIdConversion.convertToLocal(tx);
687    }
688
689    private Location minimum(Location producerAuditPosition,
690            Location lastIndoubtPosition) {
691        Location min = null;
692        if (producerAuditPosition != null) {
693            min = producerAuditPosition;
694            if (lastIndoubtPosition != null && lastIndoubtPosition.compareTo(producerAuditPosition) < 0) {
695                min = lastIndoubtPosition;
696            }
697        } else {
698            min = lastIndoubtPosition;
699        }
700        return min;
701    }
702
703    private Location recoverProducerAudit() throws IOException {
704        if (metadata.producerSequenceIdTrackerLocation != null) {
705            KahaProducerAuditCommand audit = (KahaProducerAuditCommand) load(metadata.producerSequenceIdTrackerLocation);
706            try {
707                ObjectInputStream objectIn = new ObjectInputStream(audit.getAudit().newInput());
708                int maxNumProducers = getMaxFailoverProducersToTrack();
709                int maxAuditDepth = getFailoverProducersAuditDepth();
710                metadata.producerSequenceIdTracker = (ActiveMQMessageAuditNoSync) objectIn.readObject();
711                metadata.producerSequenceIdTracker.setAuditDepth(maxAuditDepth);
712                metadata.producerSequenceIdTracker.setMaximumNumberOfProducersToTrack(maxNumProducers);
713                return journal.getNextLocation(metadata.producerSequenceIdTrackerLocation);
714            } catch (Exception e) {
715                LOG.warn("Cannot recover message audit", e);
716                return journal.getNextLocation(null);
717            }
718        } else {
719            // got no audit stored so got to recreate via replay from start of the journal
720            return journal.getNextLocation(null);
721        }
722    }
723
724    @SuppressWarnings("unchecked")
725    private Location recoverAckMessageFileMap() throws IOException {
726        if (metadata.ackMessageFileMapLocation != null) {
727            KahaAckMessageFileMapCommand audit = (KahaAckMessageFileMapCommand) load(metadata.ackMessageFileMapLocation);
728            try {
729                ObjectInputStream objectIn = new ObjectInputStream(audit.getAckMessageFileMap().newInput());
730                metadata.ackMessageFileMap = (Map<Integer, Set<Integer>>) objectIn.readObject();
731                return journal.getNextLocation(metadata.ackMessageFileMapLocation);
732            } catch (Exception e) {
733                LOG.warn("Cannot recover ackMessageFileMap", e);
734                return journal.getNextLocation(null);
735            }
736        } else {
737            // got no ackMessageFileMap stored so got to recreate via replay from start of the journal
738            return journal.getNextLocation(null);
739        }
740    }
741
742    protected void recoverIndex(Transaction tx) throws IOException {
743        long start = System.currentTimeMillis();
744        // It is possible index updates got applied before the journal updates..
745        // in that case we need to removed references to messages that are not in the journal
746        final Location lastAppendLocation = journal.getLastAppendLocation();
747        long undoCounter=0;
748
749        // Go through all the destinations to see if they have messages past the lastAppendLocation
750        for (String key : storedDestinations.keySet()) {
751            StoredDestination sd = storedDestinations.get(key);
752
753            final ArrayList<Long> matches = new ArrayList<Long>();
754            // Find all the Locations that are >= than the last Append Location.
755            sd.locationIndex.visit(tx, new BTreeVisitor.GTEVisitor<Location, Long>(lastAppendLocation) {
756                @Override
757                protected void matched(Location key, Long value) {
758                    matches.add(value);
759                }
760            });
761
762            for (Long sequenceId : matches) {
763                MessageKeys keys = sd.orderIndex.remove(tx, sequenceId);
764                sd.locationIndex.remove(tx, keys.location);
765                sd.messageIdIndex.remove(tx, keys.messageId);
766                metadata.producerSequenceIdTracker.rollback(keys.messageId);
767                undoCounter++;
768                decrementAndSubSizeToStoreStat(key, keys.location.getSize());
769                // TODO: do we need to modify the ack positions for the pub sub case?
770            }
771        }
772
773        if( undoCounter > 0 ) {
774            // The rolledback operations are basically in flight journal writes.  To avoid getting
775            // these the end user should do sync writes to the journal.
776            if (LOG.isInfoEnabled()) {
777                long end = System.currentTimeMillis();
778                LOG.info("Rolled back " + undoCounter + " messages from the index in " + ((end - start) / 1000.0f) + " seconds.");
779            }
780        }
781
782        undoCounter = 0;
783        start = System.currentTimeMillis();
784
785        // Lets be extra paranoid here and verify that all the datafiles being referenced
786        // by the indexes still exists.
787
788        final SequenceSet ss = new SequenceSet();
789        for (StoredDestination sd : storedDestinations.values()) {
790            // Use a visitor to cut down the number of pages that we load
791            sd.locationIndex.visit(tx, new BTreeVisitor<Location, Long>() {
792                int last=-1;
793
794                @Override
795                public boolean isInterestedInKeysBetween(Location first, Location second) {
796                    if( first==null ) {
797                        return !ss.contains(0, second.getDataFileId());
798                    } else if( second==null ) {
799                        return true;
800                    } else {
801                        return !ss.contains(first.getDataFileId(), second.getDataFileId());
802                    }
803                }
804
805                @Override
806                public void visit(List<Location> keys, List<Long> values) {
807                    for (Location l : keys) {
808                        int fileId = l.getDataFileId();
809                        if( last != fileId ) {
810                            ss.add(fileId);
811                            last = fileId;
812                        }
813                    }
814                }
815
816            });
817        }
818        HashSet<Integer> missingJournalFiles = new HashSet<Integer>();
819        while (!ss.isEmpty()) {
820            missingJournalFiles.add((int) ss.removeFirst());
821        }
822        missingJournalFiles.removeAll(journal.getFileMap().keySet());
823
824        if (!missingJournalFiles.isEmpty()) {
825            if (LOG.isInfoEnabled()) {
826                LOG.info("Some journal files are missing: " + missingJournalFiles);
827            }
828        }
829
830        ArrayList<BTreeVisitor.Predicate<Location>> missingPredicates = new ArrayList<BTreeVisitor.Predicate<Location>>();
831        for (Integer missing : missingJournalFiles) {
832            missingPredicates.add(new BTreeVisitor.BetweenVisitor<Location, Long>(new Location(missing, 0), new Location(missing + 1, 0)));
833        }
834
835        if (checkForCorruptJournalFiles) {
836            Collection<DataFile> dataFiles = journal.getFileMap().values();
837            for (DataFile dataFile : dataFiles) {
838                int id = dataFile.getDataFileId();
839                missingPredicates.add(new BTreeVisitor.BetweenVisitor<Location, Long>(new Location(id, dataFile.getLength()), new Location(id + 1, 0)));
840                Sequence seq = dataFile.getCorruptedBlocks().getHead();
841                while (seq != null) {
842                    missingPredicates.add(new BTreeVisitor.BetweenVisitor<Location, Long>(new Location(id, (int) seq.getFirst()), new Location(id, (int) seq.getLast() + 1)));
843                    seq = seq.getNext();
844                }
845            }
846        }
847
848        if (!missingPredicates.isEmpty()) {
849            for (Entry<String, StoredDestination> sdEntry : storedDestinations.entrySet()) {
850                final StoredDestination sd = sdEntry.getValue();
851                final ArrayList<Long> matches = new ArrayList<Long>();
852                sd.locationIndex.visit(tx, new BTreeVisitor.OrVisitor<Location, Long>(missingPredicates) {
853                    @Override
854                    protected void matched(Location key, Long value) {
855                        matches.add(value);
856                    }
857                });
858
859                // If somes message references are affected by the missing data files...
860                if (!matches.isEmpty()) {
861
862                    // We either 'gracefully' recover dropping the missing messages or
863                    // we error out.
864                    if( ignoreMissingJournalfiles ) {
865                        // Update the index to remove the references to the missing data
866                        for (Long sequenceId : matches) {
867                            MessageKeys keys = sd.orderIndex.remove(tx, sequenceId);
868                            sd.locationIndex.remove(tx, keys.location);
869                            sd.messageIdIndex.remove(tx, keys.messageId);
870                            LOG.info("[" + sdEntry.getKey() + "] dropped: " + keys.messageId + " at corrupt location: " + keys.location);
871                            undoCounter++;
872                            decrementAndSubSizeToStoreStat(sdEntry.getKey(), keys.location.getSize());
873                            // TODO: do we need to modify the ack positions for the pub sub case?
874                        }
875                    } else {
876                        throw new IOException("Detected missing/corrupt journal files. "+matches.size()+" messages affected.");
877                    }
878                }
879            }
880        }
881
882        if( undoCounter > 0 ) {
883            // The rolledback operations are basically in flight journal writes.  To avoid getting these the end user
884            // should do sync writes to the journal.
885            if (LOG.isInfoEnabled()) {
886                long end = System.currentTimeMillis();
887                LOG.info("Detected missing/corrupt journal files.  Dropped " + undoCounter + " messages from the index in " + ((end - start) / 1000.0f) + " seconds.");
888            }
889        }
890    }
891
892    private Location nextRecoveryPosition;
893    private Location lastRecoveryPosition;
894
895    public void incrementalRecover() throws IOException {
896        this.indexLock.writeLock().lock();
897        try {
898            if( nextRecoveryPosition == null ) {
899                if( lastRecoveryPosition==null ) {
900                    nextRecoveryPosition = getRecoveryPosition();
901                } else {
902                    nextRecoveryPosition = journal.getNextLocation(lastRecoveryPosition);
903                }
904            }
905            while (nextRecoveryPosition != null) {
906                lastRecoveryPosition = nextRecoveryPosition;
907                metadata.lastUpdate = lastRecoveryPosition;
908                JournalCommand<?> message = load(lastRecoveryPosition);
909                process(message, lastRecoveryPosition, (IndexAware) null);
910                nextRecoveryPosition = journal.getNextLocation(lastRecoveryPosition);
911            }
912        } finally {
913            this.indexLock.writeLock().unlock();
914        }
915    }
916
917    public Location getLastUpdatePosition() throws IOException {
918        return metadata.lastUpdate;
919    }
920
921    private Location getRecoveryPosition() throws IOException {
922
923        if (!this.forceRecoverIndex) {
924
925            // If we need to recover the transactions..
926            if (metadata.firstInProgressTransactionLocation != null) {
927                return metadata.firstInProgressTransactionLocation;
928            }
929
930            // Perhaps there were no transactions...
931            if( metadata.lastUpdate!=null) {
932                // Start replay at the record after the last one recorded in the index file.
933                return journal.getNextLocation(metadata.lastUpdate);
934            }
935        }
936        // This loads the first position.
937        return journal.getNextLocation(null);
938    }
939
940    protected void checkpointCleanup(final boolean cleanup) throws IOException {
941        long start;
942        this.indexLock.writeLock().lock();
943        try {
944            start = System.currentTimeMillis();
945            if( !opened.get() ) {
946                return;
947            }
948        } finally {
949            this.indexLock.writeLock().unlock();
950        }
951        checkpointUpdate(cleanup);
952        long end = System.currentTimeMillis();
953        if (LOG_SLOW_ACCESS_TIME > 0 && end - start > LOG_SLOW_ACCESS_TIME) {
954            if (LOG.isInfoEnabled()) {
955                LOG.info("Slow KahaDB access: cleanup took " + (end - start));
956            }
957        }
958    }
959
960    public ByteSequence toByteSequence(JournalCommand<?> data) throws IOException {
961        int size = data.serializedSizeFramed();
962        DataByteArrayOutputStream os = new DataByteArrayOutputStream(size + 1);
963        os.writeByte(data.type().getNumber());
964        data.writeFramed(os);
965        return os.toByteSequence();
966    }
967
968    // /////////////////////////////////////////////////////////////////
969    // Methods call by the broker to update and query the store.
970    // /////////////////////////////////////////////////////////////////
971    public Location store(JournalCommand<?> data) throws IOException {
972        return store(data, false, null,null);
973    }
974
975    public Location store(JournalCommand<?> data, Runnable onJournalStoreComplete) throws IOException {
976        return store(data, false, null, null, onJournalStoreComplete);
977    }
978
979    public Location store(JournalCommand<?> data, boolean sync, IndexAware before,Runnable after) throws IOException {
980        return store(data, sync, before, after, null);
981    }
982
983    /**
984     * All updated are are funneled through this method. The updates are converted
985     * to a JournalMessage which is logged to the journal and then the data from
986     * the JournalMessage is used to update the index just like it would be done
987     * during a recovery process.
988     */
989    public Location store(JournalCommand<?> data, boolean sync, IndexAware before, Runnable after, Runnable onJournalStoreComplete) throws IOException {
990        try {
991            ByteSequence sequence = toByteSequence(data);
992
993            Location location;
994            checkpointLock.readLock().lock();
995            try {
996
997                long start = System.currentTimeMillis();
998                location = onJournalStoreComplete == null ? journal.write(sequence, sync) :  journal.write(sequence, onJournalStoreComplete) ;
999                long start2 = System.currentTimeMillis();
1000                process(data, location, before);
1001
1002                long end = System.currentTimeMillis();
1003                if( LOG_SLOW_ACCESS_TIME>0 && end-start > LOG_SLOW_ACCESS_TIME) {
1004                    if (LOG.isInfoEnabled()) {
1005                        LOG.info("Slow KahaDB access: Journal append took: "+(start2-start)+" ms, Index update took "+(end-start2)+" ms");
1006                    }
1007                }
1008
1009            } finally{
1010                checkpointLock.readLock().unlock();
1011            }
1012            if (after != null) {
1013                after.run();
1014            }
1015
1016            if (checkpointThread != null && !checkpointThread.isAlive() && opened.get()) {
1017                startCheckpoint();
1018            }
1019            return location;
1020        } catch (IOException ioe) {
1021            LOG.error("KahaDB failed to store to Journal", ioe);
1022            brokerService.handleIOException(ioe);
1023            throw ioe;
1024        }
1025    }
1026
1027    /**
1028     * Loads a previously stored JournalMessage
1029     *
1030     * @param location
1031     * @return
1032     * @throws IOException
1033     */
1034    public JournalCommand<?> load(Location location) throws IOException {
1035        long start = System.currentTimeMillis();
1036        ByteSequence data = journal.read(location);
1037        long end = System.currentTimeMillis();
1038        if( LOG_SLOW_ACCESS_TIME>0 && end-start > LOG_SLOW_ACCESS_TIME) {
1039            if (LOG.isInfoEnabled()) {
1040                LOG.info("Slow KahaDB access: Journal read took: "+(end-start)+" ms");
1041            }
1042        }
1043        DataByteArrayInputStream is = new DataByteArrayInputStream(data);
1044        byte readByte = is.readByte();
1045        KahaEntryType type = KahaEntryType.valueOf(readByte);
1046        if( type == null ) {
1047            try {
1048                is.close();
1049            } catch (IOException e) {}
1050            throw new IOException("Could not load journal record. Invalid location: "+location);
1051        }
1052        JournalCommand<?> message = (JournalCommand<?>)type.createMessage();
1053        message.mergeFramed(is);
1054        return message;
1055    }
1056
1057    /**
1058     * do minimal recovery till we reach the last inDoubtLocation
1059     * @param data
1060     * @param location
1061     * @param inDoubtlocation
1062     * @throws IOException
1063     */
1064    void process(JournalCommand<?> data, final Location location, final Location inDoubtlocation) throws IOException {
1065        if (inDoubtlocation != null && location.compareTo(inDoubtlocation) >= 0) {
1066            process(data, location, (IndexAware) null);
1067        } else {
1068            // just recover producer audit
1069            data.visit(new Visitor() {
1070                @Override
1071                public void visit(KahaAddMessageCommand command) throws IOException {
1072                    metadata.producerSequenceIdTracker.isDuplicate(command.getMessageId());
1073                }
1074            });
1075        }
1076    }
1077
1078    // /////////////////////////////////////////////////////////////////
1079    // Journaled record processing methods. Once the record is journaled,
1080    // these methods handle applying the index updates. These may be called
1081    // from the recovery method too so they need to be idempotent
1082    // /////////////////////////////////////////////////////////////////
1083
1084    void process(JournalCommand<?> data, final Location location, final IndexAware onSequenceAssignedCallback) throws IOException {
1085        data.visit(new Visitor() {
1086            @Override
1087            public void visit(KahaAddMessageCommand command) throws IOException {
1088                process(command, location, onSequenceAssignedCallback);
1089            }
1090
1091            @Override
1092            public void visit(KahaRemoveMessageCommand command) throws IOException {
1093                process(command, location);
1094            }
1095
1096            @Override
1097            public void visit(KahaPrepareCommand command) throws IOException {
1098                process(command, location);
1099            }
1100
1101            @Override
1102            public void visit(KahaCommitCommand command) throws IOException {
1103                process(command, location, onSequenceAssignedCallback);
1104            }
1105
1106            @Override
1107            public void visit(KahaRollbackCommand command) throws IOException {
1108                process(command, location);
1109            }
1110
1111            @Override
1112            public void visit(KahaRemoveDestinationCommand command) throws IOException {
1113                process(command, location);
1114            }
1115
1116            @Override
1117            public void visit(KahaSubscriptionCommand command) throws IOException {
1118                process(command, location);
1119            }
1120
1121            @Override
1122            public void visit(KahaProducerAuditCommand command) throws IOException {
1123                processLocation(location);
1124            }
1125
1126            @Override
1127            public void visit(KahaAckMessageFileMapCommand command) throws IOException {
1128                processLocation(location);
1129            }
1130
1131            @Override
1132            public void visit(KahaTraceCommand command) {
1133                processLocation(location);
1134            }
1135
1136            @Override
1137            public void visit(KahaUpdateMessageCommand command) throws IOException {
1138                process(command, location);
1139            }
1140        });
1141    }
1142
1143    @SuppressWarnings("rawtypes")
1144    protected void process(final KahaAddMessageCommand command, final Location location, final IndexAware runWithIndexLock) throws IOException {
1145        if (command.hasTransactionInfo()) {
1146            List<Operation> inflightTx = getInflightTx(command.getTransactionInfo());
1147            inflightTx.add(new AddOperation(command, location, runWithIndexLock));
1148        } else {
1149            this.indexLock.writeLock().lock();
1150            try {
1151                pageFile.tx().execute(new Transaction.Closure<IOException>() {
1152                    @Override
1153                    public void execute(Transaction tx) throws IOException {
1154                        long assignedIndex = updateIndex(tx, command, location);
1155                        if (runWithIndexLock != null) {
1156                            runWithIndexLock.sequenceAssignedWithIndexLocked(assignedIndex);
1157                        }
1158                    }
1159                });
1160
1161            } finally {
1162                this.indexLock.writeLock().unlock();
1163            }
1164        }
1165    }
1166
1167    protected void process(final KahaUpdateMessageCommand command, final Location location) throws IOException {
1168        this.indexLock.writeLock().lock();
1169        try {
1170            pageFile.tx().execute(new Transaction.Closure<IOException>() {
1171                @Override
1172                public void execute(Transaction tx) throws IOException {
1173                    updateIndex(tx, command, location);
1174                }
1175            });
1176        } finally {
1177            this.indexLock.writeLock().unlock();
1178        }
1179    }
1180
1181    @SuppressWarnings("rawtypes")
1182    protected void process(final KahaRemoveMessageCommand command, final Location location) throws IOException {
1183        if (command.hasTransactionInfo()) {
1184           List<Operation> inflightTx = getInflightTx(command.getTransactionInfo());
1185           inflightTx.add(new RemoveOperation(command, location));
1186        } else {
1187            this.indexLock.writeLock().lock();
1188            try {
1189                pageFile.tx().execute(new Transaction.Closure<IOException>() {
1190                    @Override
1191                    public void execute(Transaction tx) throws IOException {
1192                        updateIndex(tx, command, location);
1193                    }
1194                });
1195            } finally {
1196                this.indexLock.writeLock().unlock();
1197            }
1198        }
1199    }
1200
1201    protected void process(final KahaRemoveDestinationCommand command, final Location location) throws IOException {
1202        this.indexLock.writeLock().lock();
1203        try {
1204            pageFile.tx().execute(new Transaction.Closure<IOException>() {
1205                @Override
1206                public void execute(Transaction tx) throws IOException {
1207                    updateIndex(tx, command, location);
1208                }
1209            });
1210        } finally {
1211            this.indexLock.writeLock().unlock();
1212        }
1213    }
1214
1215    protected void process(final KahaSubscriptionCommand command, final Location location) throws IOException {
1216        this.indexLock.writeLock().lock();
1217        try {
1218            pageFile.tx().execute(new Transaction.Closure<IOException>() {
1219                @Override
1220                public void execute(Transaction tx) throws IOException {
1221                    updateIndex(tx, command, location);
1222                }
1223            });
1224        } finally {
1225            this.indexLock.writeLock().unlock();
1226        }
1227    }
1228
1229    protected void processLocation(final Location location) {
1230        this.indexLock.writeLock().lock();
1231        try {
1232            metadata.lastUpdate = location;
1233        } finally {
1234            this.indexLock.writeLock().unlock();
1235        }
1236    }
1237
1238    @SuppressWarnings("rawtypes")
1239    protected void process(KahaCommitCommand command, final Location location, final IndexAware before) throws IOException {
1240        TransactionId key = TransactionIdConversion.convert(command.getTransactionInfo());
1241        List<Operation> inflightTx;
1242        synchronized (inflightTransactions) {
1243            inflightTx = inflightTransactions.remove(key);
1244            if (inflightTx == null) {
1245                inflightTx = preparedTransactions.remove(key);
1246            }
1247        }
1248        if (inflightTx == null) {
1249            // only non persistent messages in this tx
1250            if (before != null) {
1251                before.sequenceAssignedWithIndexLocked(-1);
1252            }
1253            return;
1254        }
1255
1256        final List<Operation> messagingTx = inflightTx;
1257        indexLock.writeLock().lock();
1258        try {
1259            pageFile.tx().execute(new Transaction.Closure<IOException>() {
1260                @Override
1261                public void execute(Transaction tx) throws IOException {
1262                    for (Operation op : messagingTx) {
1263                        op.execute(tx);
1264                    }
1265                }
1266            });
1267            metadata.lastUpdate = location;
1268        } finally {
1269            indexLock.writeLock().unlock();
1270        }
1271    }
1272
1273    @SuppressWarnings("rawtypes")
1274    protected void process(KahaPrepareCommand command, Location location) {
1275        TransactionId key = TransactionIdConversion.convert(command.getTransactionInfo());
1276        synchronized (inflightTransactions) {
1277            List<Operation> tx = inflightTransactions.remove(key);
1278            if (tx != null) {
1279                preparedTransactions.put(key, tx);
1280            }
1281        }
1282    }
1283
1284    @SuppressWarnings("rawtypes")
1285    protected void process(KahaRollbackCommand command, Location location)  throws IOException {
1286        TransactionId key = TransactionIdConversion.convert(command.getTransactionInfo());
1287        List<Operation> updates = null;
1288        synchronized (inflightTransactions) {
1289            updates = inflightTransactions.remove(key);
1290            if (updates == null) {
1291                updates = preparedTransactions.remove(key);
1292            }
1293        }
1294    }
1295
1296    // /////////////////////////////////////////////////////////////////
1297    // These methods do the actual index updates.
1298    // /////////////////////////////////////////////////////////////////
1299
1300    protected final ReentrantReadWriteLock indexLock = new ReentrantReadWriteLock();
1301    private final HashSet<Integer> journalFilesBeingReplicated = new HashSet<Integer>();
1302
1303    long updateIndex(Transaction tx, KahaAddMessageCommand command, Location location) throws IOException {
1304        StoredDestination sd = getStoredDestination(command.getDestination(), tx);
1305
1306        // Skip adding the message to the index if this is a topic and there are
1307        // no subscriptions.
1308        if (sd.subscriptions != null && sd.subscriptions.isEmpty(tx)) {
1309            return -1;
1310        }
1311
1312        // Add the message.
1313        int priority = command.getPrioritySupported() ? command.getPriority() : javax.jms.Message.DEFAULT_PRIORITY;
1314        long id = sd.orderIndex.getNextMessageId(priority);
1315        Long previous = sd.locationIndex.put(tx, location, id);
1316        if (previous == null) {
1317            previous = sd.messageIdIndex.put(tx, command.getMessageId(), id);
1318            if (previous == null) {
1319                incrementAndAddSizeToStoreStat(command.getDestination(), location.getSize());
1320                sd.orderIndex.put(tx, priority, id, new MessageKeys(command.getMessageId(), location));
1321                if (sd.subscriptions != null && !sd.subscriptions.isEmpty(tx)) {
1322                    addAckLocationForNewMessage(tx, sd, id);
1323                }
1324                metadata.lastUpdate = location;
1325            } else {
1326
1327                MessageKeys messageKeys = sd.orderIndex.get(tx, previous);
1328                if (messageKeys != null && messageKeys.location.compareTo(location) < 0) {
1329                    // If the message ID is indexed, then the broker asked us to store a duplicate before the message was dispatched and acked, we ignore this add attempt
1330                    LOG.warn("Duplicate message add attempt rejected. Destination: {}://{}, Message id: {}", command.getDestination().getType(), command.getDestination().getName(), command.getMessageId());
1331                }
1332                sd.messageIdIndex.put(tx, command.getMessageId(), previous);
1333                sd.locationIndex.remove(tx, location);
1334                id = -1;
1335            }
1336        } else {
1337            // restore the previous value.. Looks like this was a redo of a previously
1338            // added message. We don't want to assign it a new id as the other indexes would
1339            // be wrong..
1340            sd.locationIndex.put(tx, location, previous);
1341            metadata.lastUpdate = location;
1342        }
1343        // record this id in any event, initial send or recovery
1344        metadata.producerSequenceIdTracker.isDuplicate(command.getMessageId());
1345
1346       return id;
1347    }
1348
1349    void trackPendingAdd(KahaDestination destination, Long seq) {
1350        StoredDestination sd = storedDestinations.get(key(destination));
1351        if (sd != null) {
1352            sd.trackPendingAdd(seq);
1353        }
1354    }
1355
1356    void trackPendingAddComplete(KahaDestination destination, Long seq) {
1357        StoredDestination sd = storedDestinations.get(key(destination));
1358        if (sd != null) {
1359            sd.trackPendingAddComplete(seq);
1360        }
1361    }
1362
1363    void updateIndex(Transaction tx, KahaUpdateMessageCommand updateMessageCommand, Location location) throws IOException {
1364        KahaAddMessageCommand command = updateMessageCommand.getMessage();
1365        StoredDestination sd = getStoredDestination(command.getDestination(), tx);
1366
1367        Long id = sd.messageIdIndex.get(tx, command.getMessageId());
1368        if (id != null) {
1369            MessageKeys previousKeys = sd.orderIndex.put(
1370                    tx,
1371                    command.getPrioritySupported() ? command.getPriority() : javax.jms.Message.DEFAULT_PRIORITY,
1372                    id,
1373                    new MessageKeys(command.getMessageId(), location)
1374            );
1375            sd.locationIndex.put(tx, location, id);
1376            incrementAndAddSizeToStoreStat(command.getDestination(), location.getSize());
1377            // on first update previous is original location, on recovery/replay it may be the updated location
1378            if(previousKeys != null && !previousKeys.location.equals(location)) {
1379                sd.locationIndex.remove(tx, previousKeys.location);
1380                decrementAndSubSizeToStoreStat(command.getDestination(), previousKeys.location.getSize());
1381            }
1382            metadata.lastUpdate = location;
1383        } else {
1384            LOG.warn("Non existent message update attempt rejected. Destination: {}://{}, Message id: {}", command.getDestination().getType(), command.getDestination().getName(), command.getMessageId());
1385        }
1386    }
1387
1388    void updateIndex(Transaction tx, KahaRemoveMessageCommand command, Location ackLocation) throws IOException {
1389        StoredDestination sd = getStoredDestination(command.getDestination(), tx);
1390        if (!command.hasSubscriptionKey()) {
1391
1392            // In the queue case we just remove the message from the index..
1393            Long sequenceId = sd.messageIdIndex.remove(tx, command.getMessageId());
1394            if (sequenceId != null) {
1395                MessageKeys keys = sd.orderIndex.remove(tx, sequenceId);
1396                if (keys != null) {
1397                    sd.locationIndex.remove(tx, keys.location);
1398                    decrementAndSubSizeToStoreStat(command.getDestination(), keys.location.getSize());
1399                    recordAckMessageReferenceLocation(ackLocation, keys.location);
1400                    metadata.lastUpdate = ackLocation;
1401                }  else if (LOG.isDebugEnabled()) {
1402                    LOG.debug("message not found in order index: " + sequenceId  + " for: " + command.getMessageId());
1403                }
1404            } else if (LOG.isDebugEnabled()) {
1405                LOG.debug("message not found in sequence id index: " + command.getMessageId());
1406            }
1407        } else {
1408            // In the topic case we need remove the message once it's been acked
1409            // by all the subs
1410            Long sequence = sd.messageIdIndex.get(tx, command.getMessageId());
1411
1412            // Make sure it's a valid message id...
1413            if (sequence != null) {
1414                String subscriptionKey = command.getSubscriptionKey();
1415                if (command.getAck() != UNMATCHED) {
1416                    sd.orderIndex.get(tx, sequence);
1417                    byte priority = sd.orderIndex.lastGetPriority();
1418                    sd.subscriptionAcks.put(tx, subscriptionKey, new LastAck(sequence, priority));
1419                }
1420
1421                MessageKeys keys = sd.orderIndex.get(tx, sequence);
1422                if (keys != null) {
1423                    recordAckMessageReferenceLocation(ackLocation, keys.location);
1424                }
1425                // The following method handles deleting un-referenced messages.
1426                removeAckLocation(command, tx, sd, subscriptionKey, sequence);
1427                metadata.lastUpdate = ackLocation;
1428            } else if (LOG.isDebugEnabled()) {
1429                LOG.debug("no message sequence exists for id: " + command.getMessageId() + " and sub: " + command.getSubscriptionKey());
1430            }
1431
1432        }
1433    }
1434
1435    private void recordAckMessageReferenceLocation(Location ackLocation, Location messageLocation) {
1436        Set<Integer> referenceFileIds = metadata.ackMessageFileMap.get(Integer.valueOf(ackLocation.getDataFileId()));
1437        if (referenceFileIds == null) {
1438            referenceFileIds = new HashSet<Integer>();
1439            referenceFileIds.add(messageLocation.getDataFileId());
1440            metadata.ackMessageFileMap.put(ackLocation.getDataFileId(), referenceFileIds);
1441        } else {
1442            Integer id = Integer.valueOf(messageLocation.getDataFileId());
1443            if (!referenceFileIds.contains(id)) {
1444                referenceFileIds.add(id);
1445            }
1446        }
1447    }
1448
1449    void updateIndex(Transaction tx, KahaRemoveDestinationCommand command, Location location) throws IOException {
1450        StoredDestination sd = getStoredDestination(command.getDestination(), tx);
1451        sd.orderIndex.remove(tx);
1452
1453        sd.locationIndex.clear(tx);
1454        sd.locationIndex.unload(tx);
1455        tx.free(sd.locationIndex.getPageId());
1456
1457        sd.messageIdIndex.clear(tx);
1458        sd.messageIdIndex.unload(tx);
1459        tx.free(sd.messageIdIndex.getPageId());
1460
1461        if (sd.subscriptions != null) {
1462            sd.subscriptions.clear(tx);
1463            sd.subscriptions.unload(tx);
1464            tx.free(sd.subscriptions.getPageId());
1465
1466            sd.subscriptionAcks.clear(tx);
1467            sd.subscriptionAcks.unload(tx);
1468            tx.free(sd.subscriptionAcks.getPageId());
1469
1470            sd.ackPositions.clear(tx);
1471            sd.ackPositions.unload(tx);
1472            tx.free(sd.ackPositions.getHeadPageId());
1473
1474            sd.subLocations.clear(tx);
1475            sd.subLocations.unload(tx);
1476            tx.free(sd.subLocations.getHeadPageId());
1477        }
1478
1479        String key = key(command.getDestination());
1480        storedDestinations.remove(key);
1481        metadata.destinations.remove(tx, key);
1482        clearStoreStats(command.getDestination());
1483        storeCache.remove(key(command.getDestination()));
1484    }
1485
1486    void updateIndex(Transaction tx, KahaSubscriptionCommand command, Location location) throws IOException {
1487        StoredDestination sd = getStoredDestination(command.getDestination(), tx);
1488        final String subscriptionKey = command.getSubscriptionKey();
1489
1490        // If set then we are creating it.. otherwise we are destroying the sub
1491        if (command.hasSubscriptionInfo()) {
1492            Location existing = sd.subLocations.get(tx, subscriptionKey);
1493            if (existing != null && existing.compareTo(location) == 0) {
1494                // replay on recovery, ignore
1495                LOG.trace("ignoring journal replay of replay of sub from: " + location);
1496                return;
1497            }
1498
1499            sd.subscriptions.put(tx, subscriptionKey, command);
1500            sd.subLocations.put(tx, subscriptionKey, location);
1501            long ackLocation=NOT_ACKED;
1502            if (!command.getRetroactive()) {
1503                ackLocation = sd.orderIndex.nextMessageId-1;
1504            } else {
1505                addAckLocationForRetroactiveSub(tx, sd, subscriptionKey);
1506            }
1507            sd.subscriptionAcks.put(tx, subscriptionKey, new LastAck(ackLocation));
1508            sd.subscriptionCache.add(subscriptionKey);
1509        } else {
1510            // delete the sub...
1511            sd.subscriptions.remove(tx, subscriptionKey);
1512            sd.subLocations.remove(tx, subscriptionKey);
1513            sd.subscriptionAcks.remove(tx, subscriptionKey);
1514            sd.subscriptionCache.remove(subscriptionKey);
1515            removeAckLocationsForSub(command, tx, sd, subscriptionKey);
1516
1517            if (sd.subscriptions.isEmpty(tx)) {
1518                // remove the stored destination
1519                KahaRemoveDestinationCommand removeDestinationCommand = new KahaRemoveDestinationCommand();
1520                removeDestinationCommand.setDestination(command.getDestination());
1521                updateIndex(tx, removeDestinationCommand, null);
1522                clearStoreStats(command.getDestination());
1523            }
1524        }
1525    }
1526
1527    private void checkpointUpdate(final boolean cleanup) throws IOException {
1528        checkpointLock.writeLock().lock();
1529        try {
1530            this.indexLock.writeLock().lock();
1531            try {
1532                pageFile.tx().execute(new Transaction.Closure<IOException>() {
1533                    @Override
1534                    public void execute(Transaction tx) throws IOException {
1535                        checkpointUpdate(tx, cleanup);
1536                    }
1537                });
1538            } finally {
1539                this.indexLock.writeLock().unlock();
1540            }
1541
1542        } finally {
1543            checkpointLock.writeLock().unlock();
1544        }
1545    }
1546
1547    /**
1548     * @param tx
1549     * @throws IOException
1550     */
1551    void checkpointUpdate(Transaction tx, boolean cleanup) throws IOException {
1552        LOG.debug("Checkpoint started.");
1553
1554        // reflect last update exclusive of current checkpoint
1555        Location lastUpdate = metadata.lastUpdate;
1556
1557        metadata.state = OPEN_STATE;
1558        metadata.producerSequenceIdTrackerLocation = checkpointProducerAudit();
1559        metadata.ackMessageFileMapLocation = checkpointAckMessageFileMap();
1560        Location[] inProgressTxRange = getInProgressTxLocationRange();
1561        metadata.firstInProgressTransactionLocation = inProgressTxRange[0];
1562        tx.store(metadata.page, metadataMarshaller, true);
1563        pageFile.flush();
1564
1565        if( cleanup ) {
1566
1567            final TreeSet<Integer> completeFileSet = new TreeSet<Integer>(journal.getFileMap().keySet());
1568            final TreeSet<Integer> gcCandidateSet = new TreeSet<Integer>(completeFileSet);
1569
1570            if (LOG.isTraceEnabled()) {
1571                LOG.trace("Last update: " + lastUpdate + ", full gc candidates set: " + gcCandidateSet);
1572            }
1573
1574            if (lastUpdate != null) {
1575                gcCandidateSet.remove(lastUpdate.getDataFileId());
1576            }
1577
1578            // Don't GC files under replication
1579            if( journalFilesBeingReplicated!=null ) {
1580                gcCandidateSet.removeAll(journalFilesBeingReplicated);
1581            }
1582
1583            if (metadata.producerSequenceIdTrackerLocation != null) {
1584                int dataFileId = metadata.producerSequenceIdTrackerLocation.getDataFileId();
1585                if (gcCandidateSet.contains(dataFileId) && gcCandidateSet.first() == dataFileId) {
1586                    // rewrite so we don't prevent gc
1587                    metadata.producerSequenceIdTracker.setModified(true);
1588                    if (LOG.isTraceEnabled()) {
1589                        LOG.trace("rewriting producerSequenceIdTracker:" + metadata.producerSequenceIdTrackerLocation);
1590                    }
1591                }
1592                gcCandidateSet.remove(dataFileId);
1593                if (LOG.isTraceEnabled()) {
1594                    LOG.trace("gc candidates after producerSequenceIdTrackerLocation:" + dataFileId + ", " + gcCandidateSet);
1595                }
1596            }
1597
1598            if (metadata.ackMessageFileMapLocation != null) {
1599                int dataFileId = metadata.ackMessageFileMapLocation.getDataFileId();
1600                gcCandidateSet.remove(dataFileId);
1601                if (LOG.isTraceEnabled()) {
1602                    LOG.trace("gc candidates after ackMessageFileMapLocation:" + dataFileId + ", " + gcCandidateSet);
1603                }
1604            }
1605
1606            // Don't GC files referenced by in-progress tx
1607            if (inProgressTxRange[0] != null) {
1608                for (int pendingTx=inProgressTxRange[0].getDataFileId(); pendingTx <= inProgressTxRange[1].getDataFileId(); pendingTx++) {
1609                    gcCandidateSet.remove(pendingTx);
1610                }
1611            }
1612            if (LOG.isTraceEnabled()) {
1613                LOG.trace("gc candidates after tx range:" + Arrays.asList(inProgressTxRange) + ", " + gcCandidateSet);
1614            }
1615
1616            // Go through all the destinations to see if any of them can remove GC candidates.
1617            for (Entry<String, StoredDestination> entry : storedDestinations.entrySet()) {
1618                if( gcCandidateSet.isEmpty() ) {
1619                    break;
1620                }
1621
1622                // Use a visitor to cut down the number of pages that we load
1623                entry.getValue().locationIndex.visit(tx, new BTreeVisitor<Location, Long>() {
1624                    int last=-1;
1625                    @Override
1626                    public boolean isInterestedInKeysBetween(Location first, Location second) {
1627                        if( first==null ) {
1628                            SortedSet<Integer> subset = gcCandidateSet.headSet(second.getDataFileId()+1);
1629                            if( !subset.isEmpty() && subset.last() == second.getDataFileId() ) {
1630                                subset.remove(second.getDataFileId());
1631                            }
1632                            return !subset.isEmpty();
1633                        } else if( second==null ) {
1634                            SortedSet<Integer> subset = gcCandidateSet.tailSet(first.getDataFileId());
1635                            if( !subset.isEmpty() && subset.first() == first.getDataFileId() ) {
1636                                subset.remove(first.getDataFileId());
1637                            }
1638                            return !subset.isEmpty();
1639                        } else {
1640                            SortedSet<Integer> subset = gcCandidateSet.subSet(first.getDataFileId(), second.getDataFileId()+1);
1641                            if( !subset.isEmpty() && subset.first() == first.getDataFileId() ) {
1642                                subset.remove(first.getDataFileId());
1643                            }
1644                            if( !subset.isEmpty() && subset.last() == second.getDataFileId() ) {
1645                                subset.remove(second.getDataFileId());
1646                            }
1647                            return !subset.isEmpty();
1648                        }
1649                    }
1650
1651                    @Override
1652                    public void visit(List<Location> keys, List<Long> values) {
1653                        for (Location l : keys) {
1654                            int fileId = l.getDataFileId();
1655                            if( last != fileId ) {
1656                                gcCandidateSet.remove(fileId);
1657                                last = fileId;
1658                            }
1659                        }
1660                    }
1661                });
1662
1663                // Durable Subscription
1664                if (entry.getValue().subLocations != null) {
1665                    Iterator<Entry<String, Location>> iter = entry.getValue().subLocations.iterator(tx);
1666                    while (iter.hasNext()) {
1667                        Entry<String, Location> subscription = iter.next();
1668                        int dataFileId = subscription.getValue().getDataFileId();
1669
1670                        // Move subscription along if it has no outstanding messages that need ack'd
1671                        // and its in the last log file in the journal.
1672                        if (!gcCandidateSet.isEmpty() && gcCandidateSet.first() == dataFileId) {
1673                            final StoredDestination destination = entry.getValue();
1674                            final String subscriptionKey = subscription.getKey();
1675                            SequenceSet pendingAcks = destination.ackPositions.get(tx, subscriptionKey);
1676
1677                            // When pending is size one that is the next message Id meaning there
1678                            // are no pending messages currently.
1679                            if (pendingAcks == null || pendingAcks.size() <= 1) {
1680                                if (LOG.isTraceEnabled()) {
1681                                    LOG.trace("Found candidate for rewrite: {} from file {}", entry.getKey(), dataFileId);
1682                                }
1683
1684                                final KahaSubscriptionCommand kahaSub =
1685                                    destination.subscriptions.get(tx, subscriptionKey);
1686                                destination.subLocations.put(
1687                                    tx, subscriptionKey, checkpointSubscriptionCommand(kahaSub));
1688
1689                                // Skips the remove from candidates if we rewrote the subscription
1690                                // in order to prevent duplicate subscription commands on recover.
1691                                // If another subscription is on the same file and isn't rewritten
1692                                // than it will remove the file from the set.
1693                                continue;
1694                            }
1695                        }
1696
1697                        gcCandidateSet.remove(dataFileId);
1698                    }
1699                }
1700
1701                if (LOG.isTraceEnabled()) {
1702                    LOG.trace("gc candidates after dest:" + entry.getKey() + ", " + gcCandidateSet);
1703                }
1704            }
1705
1706            // check we are not deleting file with ack for in-use journal files
1707            if (LOG.isTraceEnabled()) {
1708                LOG.trace("gc candidates: " + gcCandidateSet);
1709            }
1710            Iterator<Integer> candidates = gcCandidateSet.iterator();
1711            while (candidates.hasNext()) {
1712                Integer candidate = candidates.next();
1713                Set<Integer> referencedFileIds = metadata.ackMessageFileMap.get(candidate);
1714                if (referencedFileIds != null) {
1715                    for (Integer referencedFileId : referencedFileIds) {
1716                        if (completeFileSet.contains(referencedFileId) && !gcCandidateSet.contains(referencedFileId)) {
1717                            // active file that is not targeted for deletion is referenced so don't delete
1718                            candidates.remove();
1719                            break;
1720                        }
1721                    }
1722                    if (gcCandidateSet.contains(candidate)) {
1723                        metadata.ackMessageFileMap.remove(candidate);
1724                    } else {
1725                        if (LOG.isTraceEnabled()) {
1726                            LOG.trace("not removing data file: " + candidate
1727                                    + " as contained ack(s) refer to referenced file: " + referencedFileIds);
1728                        }
1729                    }
1730                }
1731            }
1732
1733            if (!gcCandidateSet.isEmpty()) {
1734                if (LOG.isDebugEnabled()) {
1735                    LOG.debug("Cleanup removing the data files: " + gcCandidateSet);
1736                }
1737                journal.removeDataFiles(gcCandidateSet);
1738            }
1739        }
1740
1741        LOG.debug("Checkpoint done.");
1742    }
1743
1744    final Runnable nullCompletionCallback = new Runnable() {
1745        @Override
1746        public void run() {
1747        }
1748    };
1749
1750    private Location checkpointProducerAudit() throws IOException {
1751        if (metadata.producerSequenceIdTracker == null || metadata.producerSequenceIdTracker.modified()) {
1752            ByteArrayOutputStream baos = new ByteArrayOutputStream();
1753            ObjectOutputStream oout = new ObjectOutputStream(baos);
1754            oout.writeObject(metadata.producerSequenceIdTracker);
1755            oout.flush();
1756            oout.close();
1757            // using completion callback allows a disk sync to be avoided when enableJournalDiskSyncs = false
1758            Location location = store(new KahaProducerAuditCommand().setAudit(new Buffer(baos.toByteArray())), nullCompletionCallback);
1759            try {
1760                location.getLatch().await();
1761            } catch (InterruptedException e) {
1762                throw new InterruptedIOException(e.toString());
1763            }
1764            return location;
1765        }
1766        return metadata.producerSequenceIdTrackerLocation;
1767    }
1768
1769    private Location checkpointAckMessageFileMap() throws IOException {
1770        ByteArrayOutputStream baos = new ByteArrayOutputStream();
1771        ObjectOutputStream oout = new ObjectOutputStream(baos);
1772        oout.writeObject(metadata.ackMessageFileMap);
1773        oout.flush();
1774        oout.close();
1775        // using completion callback allows a disk sync to be avoided when enableJournalDiskSyncs = false
1776        Location location = store(new KahaAckMessageFileMapCommand().setAckMessageFileMap(new Buffer(baos.toByteArray())), nullCompletionCallback);
1777        try {
1778            location.getLatch().await();
1779        } catch (InterruptedException e) {
1780            throw new InterruptedIOException(e.toString());
1781        }
1782        return location;
1783    }
1784
1785    private Location checkpointSubscriptionCommand(KahaSubscriptionCommand subscription) throws IOException {
1786
1787        ByteSequence sequence = toByteSequence(subscription);
1788        Location location = journal.write(sequence, nullCompletionCallback) ;
1789
1790        try {
1791            location.getLatch().await();
1792        } catch (InterruptedException e) {
1793            throw new InterruptedIOException(e.toString());
1794        }
1795        return location;
1796    }
1797
1798    public HashSet<Integer> getJournalFilesBeingReplicated() {
1799        return journalFilesBeingReplicated;
1800    }
1801
1802    // /////////////////////////////////////////////////////////////////
1803    // StoredDestination related implementation methods.
1804    // /////////////////////////////////////////////////////////////////
1805
1806    protected final HashMap<String, StoredDestination> storedDestinations = new HashMap<String, StoredDestination>();
1807
1808    static class MessageKeys {
1809        final String messageId;
1810        final Location location;
1811
1812        public MessageKeys(String messageId, Location location) {
1813            this.messageId=messageId;
1814            this.location=location;
1815        }
1816
1817        @Override
1818        public String toString() {
1819            return "["+messageId+","+location+"]";
1820        }
1821    }
1822
1823    protected class MessageKeysMarshaller extends VariableMarshaller<MessageKeys> {
1824        final LocationSizeMarshaller locationSizeMarshaller = new LocationSizeMarshaller();
1825
1826        @Override
1827        public MessageKeys readPayload(DataInput dataIn) throws IOException {
1828            return new MessageKeys(dataIn.readUTF(), locationSizeMarshaller.readPayload(dataIn));
1829        }
1830
1831        @Override
1832        public void writePayload(MessageKeys object, DataOutput dataOut) throws IOException {
1833            dataOut.writeUTF(object.messageId);
1834            locationSizeMarshaller.writePayload(object.location, dataOut);
1835        }
1836    }
1837
1838    class LastAck {
1839        long lastAckedSequence;
1840        byte priority;
1841
1842        public LastAck(LastAck source) {
1843            this.lastAckedSequence = source.lastAckedSequence;
1844            this.priority = source.priority;
1845        }
1846
1847        public LastAck() {
1848            this.priority = MessageOrderIndex.HI;
1849        }
1850
1851        public LastAck(long ackLocation) {
1852            this.lastAckedSequence = ackLocation;
1853            this.priority = MessageOrderIndex.LO;
1854        }
1855
1856        public LastAck(long ackLocation, byte priority) {
1857            this.lastAckedSequence = ackLocation;
1858            this.priority = priority;
1859        }
1860
1861        @Override
1862        public String toString() {
1863            return "[" + lastAckedSequence + ":" + priority + "]";
1864        }
1865    }
1866
1867    protected class LastAckMarshaller implements Marshaller<LastAck> {
1868
1869        @Override
1870        public void writePayload(LastAck object, DataOutput dataOut) throws IOException {
1871            dataOut.writeLong(object.lastAckedSequence);
1872            dataOut.writeByte(object.priority);
1873        }
1874
1875        @Override
1876        public LastAck readPayload(DataInput dataIn) throws IOException {
1877            LastAck lastAcked = new LastAck();
1878            lastAcked.lastAckedSequence = dataIn.readLong();
1879            if (metadata.version >= 3) {
1880                lastAcked.priority = dataIn.readByte();
1881            }
1882            return lastAcked;
1883        }
1884
1885        @Override
1886        public int getFixedSize() {
1887            return 9;
1888        }
1889
1890        @Override
1891        public LastAck deepCopy(LastAck source) {
1892            return new LastAck(source);
1893        }
1894
1895        @Override
1896        public boolean isDeepCopySupported() {
1897            return true;
1898        }
1899    }
1900
1901
1902    class StoredDestination {
1903
1904        MessageOrderIndex orderIndex = new MessageOrderIndex();
1905        BTreeIndex<Location, Long> locationIndex;
1906        BTreeIndex<String, Long> messageIdIndex;
1907
1908        // These bits are only set for Topics
1909        BTreeIndex<String, KahaSubscriptionCommand> subscriptions;
1910        BTreeIndex<String, LastAck> subscriptionAcks;
1911        HashMap<String, MessageOrderCursor> subscriptionCursors;
1912        ListIndex<String, SequenceSet> ackPositions;
1913        ListIndex<String, Location> subLocations;
1914
1915        // Transient data used to track which Messages are no longer needed.
1916        final TreeMap<Long, Long> messageReferences = new TreeMap<Long, Long>();
1917        final HashSet<String> subscriptionCache = new LinkedHashSet<String>();
1918
1919        public void trackPendingAdd(Long seq) {
1920            orderIndex.trackPendingAdd(seq);
1921        }
1922
1923        public void trackPendingAddComplete(Long seq) {
1924            orderIndex.trackPendingAddComplete(seq);
1925        }
1926
1927        @Override
1928        public String toString() {
1929            return "nextSeq:" + orderIndex.nextMessageId + ",lastRet:" + orderIndex.cursor + ",pending:" + orderIndex.pendingAdditions.size();
1930        }
1931    }
1932
1933    protected class StoredDestinationMarshaller extends VariableMarshaller<StoredDestination> {
1934
1935        final MessageKeysMarshaller messageKeysMarshaller = new MessageKeysMarshaller();
1936
1937        @Override
1938        public StoredDestination readPayload(final DataInput dataIn) throws IOException {
1939            final StoredDestination value = new StoredDestination();
1940            value.orderIndex.defaultPriorityIndex = new BTreeIndex<Long, MessageKeys>(pageFile, dataIn.readLong());
1941            value.locationIndex = new BTreeIndex<Location, Long>(pageFile, dataIn.readLong());
1942            value.messageIdIndex = new BTreeIndex<String, Long>(pageFile, dataIn.readLong());
1943
1944            if (dataIn.readBoolean()) {
1945                value.subscriptions = new BTreeIndex<String, KahaSubscriptionCommand>(pageFile, dataIn.readLong());
1946                value.subscriptionAcks = new BTreeIndex<String, LastAck>(pageFile, dataIn.readLong());
1947                if (metadata.version >= 4) {
1948                    value.ackPositions = new ListIndex<String, SequenceSet>(pageFile, dataIn.readLong());
1949                } else {
1950                    // upgrade
1951                    pageFile.tx().execute(new Transaction.Closure<IOException>() {
1952                        @Override
1953                        public void execute(Transaction tx) throws IOException {
1954                            LinkedHashMap<String, SequenceSet> temp = new LinkedHashMap<String, SequenceSet>();
1955
1956                            if (metadata.version >= 3) {
1957                                // migrate
1958                                BTreeIndex<Long, HashSet<String>> oldAckPositions =
1959                                        new BTreeIndex<Long, HashSet<String>>(pageFile, dataIn.readLong());
1960                                oldAckPositions.setKeyMarshaller(LongMarshaller.INSTANCE);
1961                                oldAckPositions.setValueMarshaller(HashSetStringMarshaller.INSTANCE);
1962                                oldAckPositions.load(tx);
1963
1964
1965                                // Do the initial build of the data in memory before writing into the store
1966                                // based Ack Positions List to avoid a lot of disk thrashing.
1967                                Iterator<Entry<Long, HashSet<String>>> iterator = oldAckPositions.iterator(tx);
1968                                while (iterator.hasNext()) {
1969                                    Entry<Long, HashSet<String>> entry = iterator.next();
1970
1971                                    for(String subKey : entry.getValue()) {
1972                                        SequenceSet pendingAcks = temp.get(subKey);
1973                                        if (pendingAcks == null) {
1974                                            pendingAcks = new SequenceSet();
1975                                            temp.put(subKey, pendingAcks);
1976                                        }
1977
1978                                        pendingAcks.add(entry.getKey());
1979                                    }
1980                                }
1981                            }
1982                            // Now move the pending messages to ack data into the store backed
1983                            // structure.
1984                            value.ackPositions = new ListIndex<String, SequenceSet>(pageFile, tx.allocate());
1985                            value.ackPositions.setKeyMarshaller(StringMarshaller.INSTANCE);
1986                            value.ackPositions.setValueMarshaller(SequenceSet.Marshaller.INSTANCE);
1987                            value.ackPositions.load(tx);
1988                            for(String subscriptionKey : temp.keySet()) {
1989                                value.ackPositions.put(tx, subscriptionKey, temp.get(subscriptionKey));
1990                            }
1991
1992                        }
1993                    });
1994                }
1995
1996                if (metadata.version >= 5) {
1997                    value.subLocations = new ListIndex<String, Location>(pageFile, dataIn.readLong());
1998                } else {
1999                    // upgrade
2000                    pageFile.tx().execute(new Transaction.Closure<IOException>() {
2001                        @Override
2002                        public void execute(Transaction tx) throws IOException {
2003                            value.subLocations = new ListIndex<String, Location>(pageFile, tx.allocate());
2004                            value.subLocations.setKeyMarshaller(StringMarshaller.INSTANCE);
2005                            value.subLocations.setValueMarshaller(LocationMarshaller.INSTANCE);
2006                            value.subLocations.load(tx);
2007                        }
2008                    });
2009                }
2010            }
2011            if (metadata.version >= 2) {
2012                value.orderIndex.lowPriorityIndex = new BTreeIndex<Long, MessageKeys>(pageFile, dataIn.readLong());
2013                value.orderIndex.highPriorityIndex = new BTreeIndex<Long, MessageKeys>(pageFile, dataIn.readLong());
2014            } else {
2015                // upgrade
2016                pageFile.tx().execute(new Transaction.Closure<IOException>() {
2017                    @Override
2018                    public void execute(Transaction tx) throws IOException {
2019                        value.orderIndex.lowPriorityIndex = new BTreeIndex<Long, MessageKeys>(pageFile, tx.allocate());
2020                        value.orderIndex.lowPriorityIndex.setKeyMarshaller(LongMarshaller.INSTANCE);
2021                        value.orderIndex.lowPriorityIndex.setValueMarshaller(messageKeysMarshaller);
2022                        value.orderIndex.lowPriorityIndex.load(tx);
2023
2024                        value.orderIndex.highPriorityIndex = new BTreeIndex<Long, MessageKeys>(pageFile, tx.allocate());
2025                        value.orderIndex.highPriorityIndex.setKeyMarshaller(LongMarshaller.INSTANCE);
2026                        value.orderIndex.highPriorityIndex.setValueMarshaller(messageKeysMarshaller);
2027                        value.orderIndex.highPriorityIndex.load(tx);
2028                    }
2029                });
2030            }
2031
2032            return value;
2033        }
2034
2035        @Override
2036        public void writePayload(StoredDestination value, DataOutput dataOut) throws IOException {
2037            dataOut.writeLong(value.orderIndex.defaultPriorityIndex.getPageId());
2038            dataOut.writeLong(value.locationIndex.getPageId());
2039            dataOut.writeLong(value.messageIdIndex.getPageId());
2040            if (value.subscriptions != null) {
2041                dataOut.writeBoolean(true);
2042                dataOut.writeLong(value.subscriptions.getPageId());
2043                dataOut.writeLong(value.subscriptionAcks.getPageId());
2044                dataOut.writeLong(value.ackPositions.getHeadPageId());
2045                dataOut.writeLong(value.subLocations.getHeadPageId());
2046            } else {
2047                dataOut.writeBoolean(false);
2048            }
2049            dataOut.writeLong(value.orderIndex.lowPriorityIndex.getPageId());
2050            dataOut.writeLong(value.orderIndex.highPriorityIndex.getPageId());
2051        }
2052    }
2053
2054    static class KahaSubscriptionCommandMarshaller extends VariableMarshaller<KahaSubscriptionCommand> {
2055        final static KahaSubscriptionCommandMarshaller INSTANCE = new KahaSubscriptionCommandMarshaller();
2056
2057        @Override
2058        public KahaSubscriptionCommand readPayload(DataInput dataIn) throws IOException {
2059            KahaSubscriptionCommand rc = new KahaSubscriptionCommand();
2060            rc.mergeFramed((InputStream)dataIn);
2061            return rc;
2062        }
2063
2064        @Override
2065        public void writePayload(KahaSubscriptionCommand object, DataOutput dataOut) throws IOException {
2066            object.writeFramed((OutputStream)dataOut);
2067        }
2068    }
2069
2070    protected StoredDestination getStoredDestination(KahaDestination destination, Transaction tx) throws IOException {
2071        String key = key(destination);
2072        StoredDestination rc = storedDestinations.get(key);
2073        if (rc == null) {
2074            boolean topic = destination.getType() == KahaDestination.DestinationType.TOPIC || destination.getType() == KahaDestination.DestinationType.TEMP_TOPIC;
2075            rc = loadStoredDestination(tx, key, topic);
2076            // Cache it. We may want to remove/unload destinations from the
2077            // cache that are not used for a while
2078            // to reduce memory usage.
2079            storedDestinations.put(key, rc);
2080        }
2081        return rc;
2082    }
2083
2084    protected StoredDestination getExistingStoredDestination(KahaDestination destination, Transaction tx) throws IOException {
2085        String key = key(destination);
2086        StoredDestination rc = storedDestinations.get(key);
2087        if (rc == null && metadata.destinations.containsKey(tx, key)) {
2088            rc = getStoredDestination(destination, tx);
2089        }
2090        return rc;
2091    }
2092
2093    /**
2094     * @param tx
2095     * @param key
2096     * @param topic
2097     * @return
2098     * @throws IOException
2099     */
2100    private StoredDestination loadStoredDestination(Transaction tx, String key, boolean topic) throws IOException {
2101        // Try to load the existing indexes..
2102        StoredDestination rc = metadata.destinations.get(tx, key);
2103        if (rc == null) {
2104            // Brand new destination.. allocate indexes for it.
2105            rc = new StoredDestination();
2106            rc.orderIndex.allocate(tx);
2107            rc.locationIndex = new BTreeIndex<Location, Long>(pageFile, tx.allocate());
2108            rc.messageIdIndex = new BTreeIndex<String, Long>(pageFile, tx.allocate());
2109
2110            if (topic) {
2111                rc.subscriptions = new BTreeIndex<String, KahaSubscriptionCommand>(pageFile, tx.allocate());
2112                rc.subscriptionAcks = new BTreeIndex<String, LastAck>(pageFile, tx.allocate());
2113                rc.ackPositions = new ListIndex<String, SequenceSet>(pageFile, tx.allocate());
2114                rc.subLocations = new ListIndex<String, Location>(pageFile, tx.allocate());
2115            }
2116            metadata.destinations.put(tx, key, rc);
2117        }
2118
2119        // Configure the marshalers and load.
2120        rc.orderIndex.load(tx);
2121
2122        // Figure out the next key using the last entry in the destination.
2123        rc.orderIndex.configureLast(tx);
2124
2125        rc.locationIndex.setKeyMarshaller(new LocationSizeMarshaller());
2126        rc.locationIndex.setValueMarshaller(LongMarshaller.INSTANCE);
2127        rc.locationIndex.load(tx);
2128
2129        rc.messageIdIndex.setKeyMarshaller(StringMarshaller.INSTANCE);
2130        rc.messageIdIndex.setValueMarshaller(LongMarshaller.INSTANCE);
2131        rc.messageIdIndex.load(tx);
2132
2133        //go through an upgrade old index if older than version 6
2134        if (metadata.version < 6) {
2135            for (Iterator<Entry<Location, Long>> iterator = rc.locationIndex.iterator(tx); iterator.hasNext(); ) {
2136                Entry<Location, Long> entry = iterator.next();
2137                // modify so it is upgraded
2138                rc.locationIndex.put(tx, entry.getKey(), entry.getValue());
2139            }
2140            //upgrade the order index
2141            for (Iterator<Entry<Long, MessageKeys>> iterator = rc.orderIndex.iterator(tx); iterator.hasNext(); ) {
2142                Entry<Long, MessageKeys> entry = iterator.next();
2143                //call get so that the last priority is updated
2144                rc.orderIndex.get(tx, entry.getKey());
2145                rc.orderIndex.put(tx, rc.orderIndex.lastGetPriority(), entry.getKey(), entry.getValue());
2146            }
2147        }
2148
2149        // If it was a topic...
2150        if (topic) {
2151
2152            rc.subscriptions.setKeyMarshaller(StringMarshaller.INSTANCE);
2153            rc.subscriptions.setValueMarshaller(KahaSubscriptionCommandMarshaller.INSTANCE);
2154            rc.subscriptions.load(tx);
2155
2156            rc.subscriptionAcks.setKeyMarshaller(StringMarshaller.INSTANCE);
2157            rc.subscriptionAcks.setValueMarshaller(new LastAckMarshaller());
2158            rc.subscriptionAcks.load(tx);
2159
2160            rc.ackPositions.setKeyMarshaller(StringMarshaller.INSTANCE);
2161            rc.ackPositions.setValueMarshaller(SequenceSet.Marshaller.INSTANCE);
2162            rc.ackPositions.load(tx);
2163
2164            rc.subLocations.setKeyMarshaller(StringMarshaller.INSTANCE);
2165            rc.subLocations.setValueMarshaller(LocationMarshaller.INSTANCE);
2166            rc.subLocations.load(tx);
2167
2168            rc.subscriptionCursors = new HashMap<String, MessageOrderCursor>();
2169
2170            if (metadata.version < 3) {
2171
2172                // on upgrade need to fill ackLocation with available messages past last ack
2173                for (Iterator<Entry<String, LastAck>> iterator = rc.subscriptionAcks.iterator(tx); iterator.hasNext(); ) {
2174                    Entry<String, LastAck> entry = iterator.next();
2175                    for (Iterator<Entry<Long, MessageKeys>> orderIterator =
2176                            rc.orderIndex.iterator(tx, new MessageOrderCursor(entry.getValue().lastAckedSequence)); orderIterator.hasNext(); ) {
2177                        Long sequence = orderIterator.next().getKey();
2178                        addAckLocation(tx, rc, sequence, entry.getKey());
2179                    }
2180                    // modify so it is upgraded
2181                    rc.subscriptionAcks.put(tx, entry.getKey(), entry.getValue());
2182                }
2183            }
2184
2185            // Configure the message references index
2186            Iterator<Entry<String, SequenceSet>> subscriptions = rc.ackPositions.iterator(tx);
2187            while (subscriptions.hasNext()) {
2188                Entry<String, SequenceSet> subscription = subscriptions.next();
2189                SequenceSet pendingAcks = subscription.getValue();
2190                if (pendingAcks != null && !pendingAcks.isEmpty()) {
2191                    Long lastPendingAck = pendingAcks.getTail().getLast();
2192                    for (Long sequenceId : pendingAcks) {
2193                        Long current = rc.messageReferences.get(sequenceId);
2194                        if (current == null) {
2195                            current = new Long(0);
2196                        }
2197
2198                        // We always add a trailing empty entry for the next position to start from
2199                        // so we need to ensure we don't count that as a message reference on reload.
2200                        if (!sequenceId.equals(lastPendingAck)) {
2201                            current = current.longValue() + 1;
2202                        } else {
2203                            current = Long.valueOf(0L);
2204                        }
2205
2206                        rc.messageReferences.put(sequenceId, current);
2207                    }
2208                }
2209            }
2210
2211            // Configure the subscription cache
2212            for (Iterator<Entry<String, LastAck>> iterator = rc.subscriptionAcks.iterator(tx); iterator.hasNext(); ) {
2213                Entry<String, LastAck> entry = iterator.next();
2214                rc.subscriptionCache.add(entry.getKey());
2215            }
2216
2217            if (rc.orderIndex.nextMessageId == 0) {
2218                // check for existing durable sub all acked out - pull next seq from acks as messages are gone
2219                if (!rc.subscriptionAcks.isEmpty(tx)) {
2220                    for (Iterator<Entry<String, LastAck>> iterator = rc.subscriptionAcks.iterator(tx); iterator.hasNext();) {
2221                        Entry<String, LastAck> entry = iterator.next();
2222                        rc.orderIndex.nextMessageId =
2223                                Math.max(rc.orderIndex.nextMessageId, entry.getValue().lastAckedSequence +1);
2224                    }
2225                }
2226            } else {
2227                // update based on ackPositions for unmatched, last entry is always the next
2228                if (!rc.messageReferences.isEmpty()) {
2229                    Long nextMessageId = (Long) rc.messageReferences.keySet().toArray()[rc.messageReferences.size() - 1];
2230                    rc.orderIndex.nextMessageId =
2231                            Math.max(rc.orderIndex.nextMessageId, nextMessageId);
2232                }
2233            }
2234        }
2235
2236        if (metadata.version < VERSION) {
2237            // store again after upgrade
2238            metadata.destinations.put(tx, key, rc);
2239        }
2240        return rc;
2241    }
2242
2243    /**
2244     * Clear the counter for the destination, if one exists.
2245     *
2246     * @param kahaDestination
2247     */
2248    protected void clearStoreStats(KahaDestination kahaDestination) {
2249        MessageStoreStatistics storeStats = getStoreStats(key(kahaDestination));
2250        if (storeStats != null) {
2251            storeStats.reset();
2252        }
2253    }
2254
2255    /**
2256     * Update MessageStoreStatistics
2257     *
2258     * @param kahaDestination
2259     * @param size
2260     */
2261    protected void incrementAndAddSizeToStoreStat(KahaDestination kahaDestination, long size) {
2262        incrementAndAddSizeToStoreStat(key(kahaDestination), size);
2263    }
2264
2265    protected void incrementAndAddSizeToStoreStat(String kahaDestKey, long size) {
2266        MessageStoreStatistics storeStats = getStoreStats(kahaDestKey);
2267        if (storeStats != null) {
2268            storeStats.getMessageCount().increment();
2269            if (size > 0) {
2270                storeStats.getMessageSize().addSize(size);
2271            }
2272        }
2273    }
2274
2275    protected void decrementAndSubSizeToStoreStat(KahaDestination kahaDestination, long size) {
2276        decrementAndSubSizeToStoreStat(key(kahaDestination), size);
2277    }
2278
2279    protected void decrementAndSubSizeToStoreStat(String kahaDestKey, long size) {
2280        MessageStoreStatistics storeStats = getStoreStats(kahaDestKey);
2281        if (storeStats != null) {
2282            storeStats.getMessageCount().decrement();
2283            if (size > 0) {
2284                storeStats.getMessageSize().addSize(-size);
2285            }
2286        }
2287    }
2288
2289    /**
2290     * This is a map to cache DestinationStatistics for a specific
2291     * KahaDestination key
2292     */
2293    protected final ConcurrentMap<String, MessageStore> storeCache =
2294            new ConcurrentHashMap<String, MessageStore>();
2295
2296    /**
2297     * Locate the storeMessageSize counter for this KahaDestination
2298     * @param kahaDestination
2299     * @return
2300     */
2301    protected MessageStoreStatistics getStoreStats(String kahaDestKey) {
2302        MessageStoreStatistics storeStats = null;
2303        try {
2304            MessageStore messageStore = storeCache.get(kahaDestKey);
2305            if (messageStore != null) {
2306                storeStats = messageStore.getMessageStoreStatistics();
2307            }
2308        } catch (Exception e1) {
2309             LOG.error("Getting size counter of destination failed", e1);
2310        }
2311
2312        return storeStats;
2313    }
2314
2315    /**
2316     * Determine whether this Destination matches the DestinationType
2317     *
2318     * @param destination
2319     * @param type
2320     * @return
2321     */
2322    protected boolean matchType(Destination destination,
2323            KahaDestination.DestinationType type) {
2324        if (destination instanceof Topic
2325                && type.equals(KahaDestination.DestinationType.TOPIC)) {
2326            return true;
2327        } else if (destination instanceof Queue
2328                && type.equals(KahaDestination.DestinationType.QUEUE)) {
2329            return true;
2330        }
2331        return false;
2332    }
2333
2334    class LocationSizeMarshaller implements Marshaller<Location> {
2335
2336        public LocationSizeMarshaller() {
2337
2338        }
2339
2340        @Override
2341        public Location readPayload(DataInput dataIn) throws IOException {
2342            Location rc = new Location();
2343            rc.setDataFileId(dataIn.readInt());
2344            rc.setOffset(dataIn.readInt());
2345            if (metadata.version >= 6) {
2346                rc.setSize(dataIn.readInt());
2347            }
2348            return rc;
2349        }
2350
2351        @Override
2352        public void writePayload(Location object, DataOutput dataOut)
2353                throws IOException {
2354            dataOut.writeInt(object.getDataFileId());
2355            dataOut.writeInt(object.getOffset());
2356            dataOut.writeInt(object.getSize());
2357        }
2358
2359        @Override
2360        public int getFixedSize() {
2361            return 12;
2362        }
2363
2364        @Override
2365        public Location deepCopy(Location source) {
2366            return new Location(source);
2367        }
2368
2369        @Override
2370        public boolean isDeepCopySupported() {
2371            return true;
2372        }
2373    }
2374
2375    private void addAckLocation(Transaction tx, StoredDestination sd, Long messageSequence, String subscriptionKey) throws IOException {
2376        SequenceSet sequences = sd.ackPositions.get(tx, subscriptionKey);
2377        if (sequences == null) {
2378            sequences = new SequenceSet();
2379            sequences.add(messageSequence);
2380            sd.ackPositions.add(tx, subscriptionKey, sequences);
2381        } else {
2382            sequences.add(messageSequence);
2383            sd.ackPositions.put(tx, subscriptionKey, sequences);
2384        }
2385
2386        Long count = sd.messageReferences.get(messageSequence);
2387        if (count == null) {
2388            count = Long.valueOf(0L);
2389        }
2390        count = count.longValue() + 1;
2391        sd.messageReferences.put(messageSequence, count);
2392    }
2393
2394    // new sub is interested in potentially all existing messages
2395    private void addAckLocationForRetroactiveSub(Transaction tx, StoredDestination sd, String subscriptionKey) throws IOException {
2396        SequenceSet allOutstanding = new SequenceSet();
2397        Iterator<Map.Entry<String, SequenceSet>> iterator = sd.ackPositions.iterator(tx);
2398        while (iterator.hasNext()) {
2399            SequenceSet set = iterator.next().getValue();
2400            for (Long entry : set) {
2401                allOutstanding.add(entry);
2402            }
2403        }
2404        sd.ackPositions.put(tx, subscriptionKey, allOutstanding);
2405
2406        for (Long ackPosition : allOutstanding) {
2407            Long count = sd.messageReferences.get(ackPosition);
2408
2409            // There might not be a reference if the ackLocation was the last
2410            // one which is a placeholder for the next incoming message and
2411            // no value was added to the message references table.
2412            if (count != null) {
2413                count = count.longValue() + 1;
2414                sd.messageReferences.put(ackPosition, count);
2415            }
2416        }
2417    }
2418
2419    // on a new message add, all existing subs are interested in this message
2420    private void addAckLocationForNewMessage(Transaction tx, StoredDestination sd, Long messageSequence) throws IOException {
2421        for(String subscriptionKey : sd.subscriptionCache) {
2422            SequenceSet sequences = sd.ackPositions.get(tx, subscriptionKey);
2423            if (sequences == null) {
2424                sequences = new SequenceSet();
2425                sequences.add(new Sequence(messageSequence, messageSequence + 1));
2426                sd.ackPositions.add(tx, subscriptionKey, sequences);
2427            } else {
2428                sequences.add(new Sequence(messageSequence, messageSequence + 1));
2429                sd.ackPositions.put(tx, subscriptionKey, sequences);
2430            }
2431
2432            Long count = sd.messageReferences.get(messageSequence);
2433            if (count == null) {
2434                count = Long.valueOf(0L);
2435            }
2436            count = count.longValue() + 1;
2437            sd.messageReferences.put(messageSequence, count);
2438            sd.messageReferences.put(messageSequence + 1, Long.valueOf(0L));
2439        }
2440    }
2441
2442    private void removeAckLocationsForSub(KahaSubscriptionCommand command,
2443            Transaction tx, StoredDestination sd, String subscriptionKey) throws IOException {
2444        if (!sd.ackPositions.isEmpty(tx)) {
2445            SequenceSet sequences = sd.ackPositions.remove(tx, subscriptionKey);
2446            if (sequences == null || sequences.isEmpty()) {
2447                return;
2448            }
2449
2450            ArrayList<Long> unreferenced = new ArrayList<Long>();
2451
2452            for(Long sequenceId : sequences) {
2453                Long references = sd.messageReferences.get(sequenceId);
2454                if (references != null) {
2455                    references = references.longValue() - 1;
2456
2457                    if (references.longValue() > 0) {
2458                        sd.messageReferences.put(sequenceId, references);
2459                    } else {
2460                        sd.messageReferences.remove(sequenceId);
2461                        unreferenced.add(sequenceId);
2462                    }
2463                }
2464            }
2465
2466            for(Long sequenceId : unreferenced) {
2467                // Find all the entries that need to get deleted.
2468                ArrayList<Entry<Long, MessageKeys>> deletes = new ArrayList<Entry<Long, MessageKeys>>();
2469                sd.orderIndex.getDeleteList(tx, deletes, sequenceId);
2470
2471                // Do the actual deletes.
2472                for (Entry<Long, MessageKeys> entry : deletes) {
2473                    sd.locationIndex.remove(tx, entry.getValue().location);
2474                    sd.messageIdIndex.remove(tx, entry.getValue().messageId);
2475                    sd.orderIndex.remove(tx, entry.getKey());
2476                    decrementAndSubSizeToStoreStat(command.getDestination(), entry.getValue().location.getSize());
2477                }
2478            }
2479        }
2480    }
2481
2482    /**
2483     * @param tx
2484     * @param sd
2485     * @param subscriptionKey
2486     * @param messageSequence
2487     * @throws IOException
2488     */
2489    private void removeAckLocation(KahaRemoveMessageCommand command,
2490            Transaction tx, StoredDestination sd, String subscriptionKey,
2491            Long messageSequence) throws IOException {
2492        // Remove the sub from the previous location set..
2493        if (messageSequence != null) {
2494            SequenceSet range = sd.ackPositions.get(tx, subscriptionKey);
2495            if (range != null && !range.isEmpty()) {
2496                range.remove(messageSequence);
2497                if (!range.isEmpty()) {
2498                    sd.ackPositions.put(tx, subscriptionKey, range);
2499                } else {
2500                    sd.ackPositions.remove(tx, subscriptionKey);
2501                }
2502
2503                // Check if the message is reference by any other subscription.
2504                Long count = sd.messageReferences.get(messageSequence);
2505                if (count != null) {
2506                    long references = count.longValue() - 1;
2507                    if (references > 0) {
2508                        sd.messageReferences.put(messageSequence, Long.valueOf(references));
2509                        return;
2510                    } else {
2511                        sd.messageReferences.remove(messageSequence);
2512                    }
2513                }
2514
2515                // Find all the entries that need to get deleted.
2516                ArrayList<Entry<Long, MessageKeys>> deletes = new ArrayList<Entry<Long, MessageKeys>>();
2517                sd.orderIndex.getDeleteList(tx, deletes, messageSequence);
2518
2519                // Do the actual deletes.
2520                for (Entry<Long, MessageKeys> entry : deletes) {
2521                    sd.locationIndex.remove(tx, entry.getValue().location);
2522                    sd.messageIdIndex.remove(tx, entry.getValue().messageId);
2523                    sd.orderIndex.remove(tx, entry.getKey());
2524                    decrementAndSubSizeToStoreStat(command.getDestination(), entry.getValue().location.getSize());
2525                }
2526            }
2527        }
2528    }
2529
2530    public LastAck getLastAck(Transaction tx, StoredDestination sd, String subscriptionKey) throws IOException {
2531        return sd.subscriptionAcks.get(tx, subscriptionKey);
2532    }
2533
2534    public long getStoredMessageCount(Transaction tx, StoredDestination sd, String subscriptionKey) throws IOException {
2535        SequenceSet messageSequences = sd.ackPositions.get(tx, subscriptionKey);
2536        if (messageSequences != null) {
2537            long result = messageSequences.rangeSize();
2538            // if there's anything in the range the last value is always the nextMessage marker, so remove 1.
2539            return result > 0 ? result - 1 : 0;
2540        }
2541
2542        return 0;
2543    }
2544
2545    protected String key(KahaDestination destination) {
2546        return destination.getType().getNumber() + ":" + destination.getName();
2547    }
2548
2549    // /////////////////////////////////////////////////////////////////
2550    // Transaction related implementation methods.
2551    // /////////////////////////////////////////////////////////////////
2552    @SuppressWarnings("rawtypes")
2553    private final LinkedHashMap<TransactionId, List<Operation>> inflightTransactions = new LinkedHashMap<TransactionId, List<Operation>>();
2554    @SuppressWarnings("rawtypes")
2555    protected final LinkedHashMap<TransactionId, List<Operation>> preparedTransactions = new LinkedHashMap<TransactionId, List<Operation>>();
2556    protected final Set<String> ackedAndPrepared = new HashSet<String>();
2557    protected final Set<String> rolledBackAcks = new HashSet<String>();
2558
2559    // messages that have prepared (pending) acks cannot be re-dispatched unless the outcome is rollback,
2560    // till then they are skipped by the store.
2561    // 'at most once' XA guarantee
2562    public void trackRecoveredAcks(ArrayList<MessageAck> acks) {
2563        this.indexLock.writeLock().lock();
2564        try {
2565            for (MessageAck ack : acks) {
2566                ackedAndPrepared.add(ack.getLastMessageId().toProducerKey());
2567            }
2568        } finally {
2569            this.indexLock.writeLock().unlock();
2570        }
2571    }
2572
2573    public void forgetRecoveredAcks(ArrayList<MessageAck> acks, boolean rollback) throws IOException {
2574        if (acks != null) {
2575            this.indexLock.writeLock().lock();
2576            try {
2577                for (MessageAck ack : acks) {
2578                    final String id = ack.getLastMessageId().toProducerKey();
2579                    ackedAndPrepared.remove(id);
2580                    if (rollback) {
2581                        rolledBackAcks.add(id);
2582                    }
2583                }
2584            } finally {
2585                this.indexLock.writeLock().unlock();
2586            }
2587        }
2588    }
2589
2590    @SuppressWarnings("rawtypes")
2591    private List<Operation> getInflightTx(KahaTransactionInfo info) {
2592        TransactionId key = TransactionIdConversion.convert(info);
2593        List<Operation> tx;
2594        synchronized (inflightTransactions) {
2595            tx = inflightTransactions.get(key);
2596            if (tx == null) {
2597                tx = Collections.synchronizedList(new ArrayList<Operation>());
2598                inflightTransactions.put(key, tx);
2599            }
2600        }
2601        return tx;
2602    }
2603
2604    @SuppressWarnings("unused")
2605    private TransactionId key(KahaTransactionInfo transactionInfo) {
2606        return TransactionIdConversion.convert(transactionInfo);
2607    }
2608
2609    abstract class Operation <T extends JournalCommand<T>> {
2610        final T command;
2611        final Location location;
2612
2613        public Operation(T command, Location location) {
2614            this.command = command;
2615            this.location = location;
2616        }
2617
2618        public Location getLocation() {
2619            return location;
2620        }
2621
2622        public T getCommand() {
2623            return command;
2624        }
2625
2626        abstract public void execute(Transaction tx) throws IOException;
2627    }
2628
2629    class AddOperation extends Operation<KahaAddMessageCommand> {
2630        final IndexAware runWithIndexLock;
2631        public AddOperation(KahaAddMessageCommand command, Location location, IndexAware runWithIndexLock) {
2632            super(command, location);
2633            this.runWithIndexLock = runWithIndexLock;
2634        }
2635
2636        @Override
2637        public void execute(Transaction tx) throws IOException {
2638            long seq = updateIndex(tx, command, location);
2639            if (runWithIndexLock != null) {
2640                runWithIndexLock.sequenceAssignedWithIndexLocked(seq);
2641            }
2642        }
2643
2644    }
2645
2646    class RemoveOperation extends Operation<KahaRemoveMessageCommand> {
2647
2648        public RemoveOperation(KahaRemoveMessageCommand command, Location location) {
2649            super(command, location);
2650        }
2651
2652        @Override
2653        public void execute(Transaction tx) throws IOException {
2654            updateIndex(tx, command, location);
2655        }
2656    }
2657
2658    // /////////////////////////////////////////////////////////////////
2659    // Initialization related implementation methods.
2660    // /////////////////////////////////////////////////////////////////
2661
2662    private PageFile createPageFile() throws IOException {
2663        if( indexDirectory == null ) {
2664            indexDirectory = directory;
2665        }
2666        IOHelper.mkdirs(indexDirectory);
2667        PageFile index = new PageFile(indexDirectory, "db");
2668        index.setEnableWriteThread(isEnableIndexWriteAsync());
2669        index.setWriteBatchSize(getIndexWriteBatchSize());
2670        index.setPageCacheSize(indexCacheSize);
2671        index.setUseLFRUEviction(isUseIndexLFRUEviction());
2672        index.setLFUEvictionFactor(getIndexLFUEvictionFactor());
2673        index.setEnableDiskSyncs(isEnableIndexDiskSyncs());
2674        index.setEnableRecoveryFile(isEnableIndexRecoveryFile());
2675        index.setEnablePageCaching(isEnableIndexPageCaching());
2676        return index;
2677    }
2678
2679    private Journal createJournal() throws IOException {
2680        Journal manager = new Journal();
2681        manager.setDirectory(directory);
2682        manager.setMaxFileLength(getJournalMaxFileLength());
2683        manager.setCheckForCorruptionOnStartup(checkForCorruptJournalFiles);
2684        manager.setChecksum(checksumJournalFiles || checkForCorruptJournalFiles);
2685        manager.setWriteBatchSize(getJournalMaxWriteBatchSize());
2686        manager.setArchiveDataLogs(isArchiveDataLogs());
2687        manager.setSizeAccumulator(journalSize);
2688        manager.setEnableAsyncDiskSync(isEnableJournalDiskSyncs());
2689        manager.setPreallocationScope(Journal.PreallocationScope.valueOf(preallocationScope.trim().toUpperCase()));
2690        manager.setPreallocationStrategy(
2691                Journal.PreallocationStrategy.valueOf(preallocationStrategy.trim().toUpperCase()));
2692        if (getDirectoryArchive() != null) {
2693            IOHelper.mkdirs(getDirectoryArchive());
2694            manager.setDirectoryArchive(getDirectoryArchive());
2695        }
2696        return manager;
2697    }
2698
2699    private Metadata createMetadata() {
2700        Metadata md = new Metadata();
2701        md.producerSequenceIdTracker.setAuditDepth(getFailoverProducersAuditDepth());
2702        md.producerSequenceIdTracker.setMaximumNumberOfProducersToTrack(getMaxFailoverProducersToTrack());
2703        return md;
2704    }
2705
2706    public int getJournalMaxWriteBatchSize() {
2707        return journalMaxWriteBatchSize;
2708    }
2709
2710    public void setJournalMaxWriteBatchSize(int journalMaxWriteBatchSize) {
2711        this.journalMaxWriteBatchSize = journalMaxWriteBatchSize;
2712    }
2713
2714    public File getDirectory() {
2715        return directory;
2716    }
2717
2718    public void setDirectory(File directory) {
2719        this.directory = directory;
2720    }
2721
2722    public boolean isDeleteAllMessages() {
2723        return deleteAllMessages;
2724    }
2725
2726    public void setDeleteAllMessages(boolean deleteAllMessages) {
2727        this.deleteAllMessages = deleteAllMessages;
2728    }
2729
2730    public void setIndexWriteBatchSize(int setIndexWriteBatchSize) {
2731        this.setIndexWriteBatchSize = setIndexWriteBatchSize;
2732    }
2733
2734    public int getIndexWriteBatchSize() {
2735        return setIndexWriteBatchSize;
2736    }
2737
2738    public void setEnableIndexWriteAsync(boolean enableIndexWriteAsync) {
2739        this.enableIndexWriteAsync = enableIndexWriteAsync;
2740    }
2741
2742    boolean isEnableIndexWriteAsync() {
2743        return enableIndexWriteAsync;
2744    }
2745
2746    public boolean isEnableJournalDiskSyncs() {
2747        return enableJournalDiskSyncs;
2748    }
2749
2750    public void setEnableJournalDiskSyncs(boolean syncWrites) {
2751        this.enableJournalDiskSyncs = syncWrites;
2752    }
2753
2754    public long getCheckpointInterval() {
2755        return checkpointInterval;
2756    }
2757
2758    public void setCheckpointInterval(long checkpointInterval) {
2759        this.checkpointInterval = checkpointInterval;
2760    }
2761
2762    public long getCleanupInterval() {
2763        return cleanupInterval;
2764    }
2765
2766    public void setCleanupInterval(long cleanupInterval) {
2767        this.cleanupInterval = cleanupInterval;
2768    }
2769
2770    public void setJournalMaxFileLength(int journalMaxFileLength) {
2771        this.journalMaxFileLength = journalMaxFileLength;
2772    }
2773
2774    public int getJournalMaxFileLength() {
2775        return journalMaxFileLength;
2776    }
2777
2778    public void setMaxFailoverProducersToTrack(int maxFailoverProducersToTrack) {
2779        this.metadata.producerSequenceIdTracker.setMaximumNumberOfProducersToTrack(maxFailoverProducersToTrack);
2780    }
2781
2782    public int getMaxFailoverProducersToTrack() {
2783        return this.metadata.producerSequenceIdTracker.getMaximumNumberOfProducersToTrack();
2784    }
2785
2786    public void setFailoverProducersAuditDepth(int failoverProducersAuditDepth) {
2787        this.metadata.producerSequenceIdTracker.setAuditDepth(failoverProducersAuditDepth);
2788    }
2789
2790    public int getFailoverProducersAuditDepth() {
2791        return this.metadata.producerSequenceIdTracker.getAuditDepth();
2792    }
2793
2794    public PageFile getPageFile() throws IOException {
2795        if (pageFile == null) {
2796            pageFile = createPageFile();
2797        }
2798        return pageFile;
2799    }
2800
2801    public Journal getJournal() throws IOException {
2802        if (journal == null) {
2803            journal = createJournal();
2804        }
2805        return journal;
2806    }
2807
2808    public boolean isFailIfDatabaseIsLocked() {
2809        return failIfDatabaseIsLocked;
2810    }
2811
2812    public void setFailIfDatabaseIsLocked(boolean failIfDatabaseIsLocked) {
2813        this.failIfDatabaseIsLocked = failIfDatabaseIsLocked;
2814    }
2815
2816    public boolean isIgnoreMissingJournalfiles() {
2817        return ignoreMissingJournalfiles;
2818    }
2819
2820    public void setIgnoreMissingJournalfiles(boolean ignoreMissingJournalfiles) {
2821        this.ignoreMissingJournalfiles = ignoreMissingJournalfiles;
2822    }
2823
2824    public int getIndexCacheSize() {
2825        return indexCacheSize;
2826    }
2827
2828    public void setIndexCacheSize(int indexCacheSize) {
2829        this.indexCacheSize = indexCacheSize;
2830    }
2831
2832    public boolean isCheckForCorruptJournalFiles() {
2833        return checkForCorruptJournalFiles;
2834    }
2835
2836    public void setCheckForCorruptJournalFiles(boolean checkForCorruptJournalFiles) {
2837        this.checkForCorruptJournalFiles = checkForCorruptJournalFiles;
2838    }
2839
2840    public boolean isChecksumJournalFiles() {
2841        return checksumJournalFiles;
2842    }
2843
2844    public void setChecksumJournalFiles(boolean checksumJournalFiles) {
2845        this.checksumJournalFiles = checksumJournalFiles;
2846    }
2847
2848    @Override
2849    public void setBrokerService(BrokerService brokerService) {
2850        this.brokerService = brokerService;
2851    }
2852
2853    /**
2854     * @return the archiveDataLogs
2855     */
2856    public boolean isArchiveDataLogs() {
2857        return this.archiveDataLogs;
2858    }
2859
2860    /**
2861     * @param archiveDataLogs the archiveDataLogs to set
2862     */
2863    public void setArchiveDataLogs(boolean archiveDataLogs) {
2864        this.archiveDataLogs = archiveDataLogs;
2865    }
2866
2867    /**
2868     * @return the directoryArchive
2869     */
2870    public File getDirectoryArchive() {
2871        return this.directoryArchive;
2872    }
2873
2874    /**
2875     * @param directoryArchive the directoryArchive to set
2876     */
2877    public void setDirectoryArchive(File directoryArchive) {
2878        this.directoryArchive = directoryArchive;
2879    }
2880
2881    public boolean isArchiveCorruptedIndex() {
2882        return archiveCorruptedIndex;
2883    }
2884
2885    public void setArchiveCorruptedIndex(boolean archiveCorruptedIndex) {
2886        this.archiveCorruptedIndex = archiveCorruptedIndex;
2887    }
2888
2889    public float getIndexLFUEvictionFactor() {
2890        return indexLFUEvictionFactor;
2891    }
2892
2893    public void setIndexLFUEvictionFactor(float indexLFUEvictionFactor) {
2894        this.indexLFUEvictionFactor = indexLFUEvictionFactor;
2895    }
2896
2897    public boolean isUseIndexLFRUEviction() {
2898        return useIndexLFRUEviction;
2899    }
2900
2901    public void setUseIndexLFRUEviction(boolean useIndexLFRUEviction) {
2902        this.useIndexLFRUEviction = useIndexLFRUEviction;
2903    }
2904
2905    public void setEnableIndexDiskSyncs(boolean enableIndexDiskSyncs) {
2906        this.enableIndexDiskSyncs = enableIndexDiskSyncs;
2907    }
2908
2909    public void setEnableIndexRecoveryFile(boolean enableIndexRecoveryFile) {
2910        this.enableIndexRecoveryFile = enableIndexRecoveryFile;
2911    }
2912
2913    public void setEnableIndexPageCaching(boolean enableIndexPageCaching) {
2914        this.enableIndexPageCaching = enableIndexPageCaching;
2915    }
2916
2917    public boolean isEnableIndexDiskSyncs() {
2918        return enableIndexDiskSyncs;
2919    }
2920
2921    public boolean isEnableIndexRecoveryFile() {
2922        return enableIndexRecoveryFile;
2923    }
2924
2925    public boolean isEnableIndexPageCaching() {
2926        return enableIndexPageCaching;
2927    }
2928
2929    // /////////////////////////////////////////////////////////////////
2930    // Internal conversion methods.
2931    // /////////////////////////////////////////////////////////////////
2932
2933    class MessageOrderCursor{
2934        long defaultCursorPosition;
2935        long lowPriorityCursorPosition;
2936        long highPriorityCursorPosition;
2937        MessageOrderCursor(){
2938        }
2939
2940        MessageOrderCursor(long position){
2941            this.defaultCursorPosition=position;
2942            this.lowPriorityCursorPosition=position;
2943            this.highPriorityCursorPosition=position;
2944        }
2945
2946        MessageOrderCursor(MessageOrderCursor other){
2947            this.defaultCursorPosition=other.defaultCursorPosition;
2948            this.lowPriorityCursorPosition=other.lowPriorityCursorPosition;
2949            this.highPriorityCursorPosition=other.highPriorityCursorPosition;
2950        }
2951
2952        MessageOrderCursor copy() {
2953            return new MessageOrderCursor(this);
2954        }
2955
2956        void reset() {
2957            this.defaultCursorPosition=0;
2958            this.highPriorityCursorPosition=0;
2959            this.lowPriorityCursorPosition=0;
2960        }
2961
2962        void increment() {
2963            if (defaultCursorPosition!=0) {
2964                defaultCursorPosition++;
2965            }
2966            if (highPriorityCursorPosition!=0) {
2967                highPriorityCursorPosition++;
2968            }
2969            if (lowPriorityCursorPosition!=0) {
2970                lowPriorityCursorPosition++;
2971            }
2972        }
2973
2974        @Override
2975        public String toString() {
2976           return "MessageOrderCursor:[def:" + defaultCursorPosition
2977                   + ", low:" + lowPriorityCursorPosition
2978                   + ", high:" +  highPriorityCursorPosition + "]";
2979        }
2980
2981        public void sync(MessageOrderCursor other) {
2982            this.defaultCursorPosition=other.defaultCursorPosition;
2983            this.lowPriorityCursorPosition=other.lowPriorityCursorPosition;
2984            this.highPriorityCursorPosition=other.highPriorityCursorPosition;
2985        }
2986    }
2987
2988    class MessageOrderIndex {
2989        static final byte HI = 9;
2990        static final byte LO = 0;
2991        static final byte DEF = 4;
2992
2993        long nextMessageId;
2994        BTreeIndex<Long, MessageKeys> defaultPriorityIndex;
2995        BTreeIndex<Long, MessageKeys> lowPriorityIndex;
2996        BTreeIndex<Long, MessageKeys> highPriorityIndex;
2997        final MessageOrderCursor cursor = new MessageOrderCursor();
2998        Long lastDefaultKey;
2999        Long lastHighKey;
3000        Long lastLowKey;
3001        byte lastGetPriority;
3002        final List<Long> pendingAdditions = new LinkedList<Long>();
3003        final MessageKeysMarshaller messageKeysMarshaller = new MessageKeysMarshaller();
3004
3005        MessageKeys remove(Transaction tx, Long key) throws IOException {
3006            MessageKeys result = defaultPriorityIndex.remove(tx, key);
3007            if (result == null && highPriorityIndex!=null) {
3008                result = highPriorityIndex.remove(tx, key);
3009                if (result ==null && lowPriorityIndex!=null) {
3010                    result = lowPriorityIndex.remove(tx, key);
3011                }
3012            }
3013            return result;
3014        }
3015
3016        void load(Transaction tx) throws IOException {
3017            defaultPriorityIndex.setKeyMarshaller(LongMarshaller.INSTANCE);
3018            defaultPriorityIndex.setValueMarshaller(messageKeysMarshaller);
3019            defaultPriorityIndex.load(tx);
3020            lowPriorityIndex.setKeyMarshaller(LongMarshaller.INSTANCE);
3021            lowPriorityIndex.setValueMarshaller(messageKeysMarshaller);
3022            lowPriorityIndex.load(tx);
3023            highPriorityIndex.setKeyMarshaller(LongMarshaller.INSTANCE);
3024            highPriorityIndex.setValueMarshaller(messageKeysMarshaller);
3025            highPriorityIndex.load(tx);
3026        }
3027
3028        void allocate(Transaction tx) throws IOException {
3029            defaultPriorityIndex = new BTreeIndex<Long, MessageKeys>(pageFile, tx.allocate());
3030            if (metadata.version >= 2) {
3031                lowPriorityIndex = new BTreeIndex<Long, MessageKeys>(pageFile, tx.allocate());
3032                highPriorityIndex = new BTreeIndex<Long, MessageKeys>(pageFile, tx.allocate());
3033            }
3034        }
3035
3036        void configureLast(Transaction tx) throws IOException {
3037            // Figure out the next key using the last entry in the destination.
3038            TreeSet<Long> orderedSet = new TreeSet<Long>();
3039
3040            addLast(orderedSet, highPriorityIndex, tx);
3041            addLast(orderedSet, defaultPriorityIndex, tx);
3042            addLast(orderedSet, lowPriorityIndex, tx);
3043
3044            if (!orderedSet.isEmpty()) {
3045                nextMessageId = orderedSet.last() + 1;
3046            }
3047        }
3048
3049        private void addLast(TreeSet<Long> orderedSet, BTreeIndex<Long, MessageKeys> index, Transaction tx) throws IOException {
3050            if (index != null) {
3051                Entry<Long, MessageKeys> lastEntry = index.getLast(tx);
3052                if (lastEntry != null) {
3053                    orderedSet.add(lastEntry.getKey());
3054                }
3055            }
3056        }
3057
3058        void clear(Transaction tx) throws IOException {
3059            this.remove(tx);
3060            this.resetCursorPosition();
3061            this.allocate(tx);
3062            this.load(tx);
3063            this.configureLast(tx);
3064        }
3065
3066        void remove(Transaction tx) throws IOException {
3067            defaultPriorityIndex.clear(tx);
3068            defaultPriorityIndex.unload(tx);
3069            tx.free(defaultPriorityIndex.getPageId());
3070            if (lowPriorityIndex != null) {
3071                lowPriorityIndex.clear(tx);
3072                lowPriorityIndex.unload(tx);
3073
3074                tx.free(lowPriorityIndex.getPageId());
3075            }
3076            if (highPriorityIndex != null) {
3077                highPriorityIndex.clear(tx);
3078                highPriorityIndex.unload(tx);
3079                tx.free(highPriorityIndex.getPageId());
3080            }
3081        }
3082
3083        void resetCursorPosition() {
3084            this.cursor.reset();
3085            lastDefaultKey = null;
3086            lastHighKey = null;
3087            lastLowKey = null;
3088        }
3089
3090        void setBatch(Transaction tx, Long sequence) throws IOException {
3091            if (sequence != null) {
3092                Long nextPosition = new Long(sequence.longValue() + 1);
3093                lastDefaultKey = sequence;
3094                cursor.defaultCursorPosition = nextPosition.longValue();
3095                lastHighKey = sequence;
3096                cursor.highPriorityCursorPosition = nextPosition.longValue();
3097                lastLowKey = sequence;
3098                cursor.lowPriorityCursorPosition = nextPosition.longValue();
3099            }
3100        }
3101
3102        void setBatch(Transaction tx, LastAck last) throws IOException {
3103            setBatch(tx, last.lastAckedSequence);
3104            if (cursor.defaultCursorPosition == 0
3105                    && cursor.highPriorityCursorPosition == 0
3106                    && cursor.lowPriorityCursorPosition == 0) {
3107                long next = last.lastAckedSequence + 1;
3108                switch (last.priority) {
3109                    case DEF:
3110                        cursor.defaultCursorPosition = next;
3111                        cursor.highPriorityCursorPosition = next;
3112                        break;
3113                    case HI:
3114                        cursor.highPriorityCursorPosition = next;
3115                        break;
3116                    case LO:
3117                        cursor.lowPriorityCursorPosition = next;
3118                        cursor.defaultCursorPosition = next;
3119                        cursor.highPriorityCursorPosition = next;
3120                        break;
3121                }
3122            }
3123        }
3124
3125        void stoppedIterating() {
3126            if (lastDefaultKey!=null) {
3127                cursor.defaultCursorPosition=lastDefaultKey.longValue()+1;
3128            }
3129            if (lastHighKey!=null) {
3130                cursor.highPriorityCursorPosition=lastHighKey.longValue()+1;
3131            }
3132            if (lastLowKey!=null) {
3133                cursor.lowPriorityCursorPosition=lastLowKey.longValue()+1;
3134            }
3135            lastDefaultKey = null;
3136            lastHighKey = null;
3137            lastLowKey = null;
3138        }
3139
3140        void getDeleteList(Transaction tx, ArrayList<Entry<Long, MessageKeys>> deletes, Long sequenceId)
3141                throws IOException {
3142            if (defaultPriorityIndex.containsKey(tx, sequenceId)) {
3143                getDeleteList(tx, deletes, defaultPriorityIndex, sequenceId);
3144            } else if (highPriorityIndex != null && highPriorityIndex.containsKey(tx, sequenceId)) {
3145                getDeleteList(tx, deletes, highPriorityIndex, sequenceId);
3146            } else if (lowPriorityIndex != null && lowPriorityIndex.containsKey(tx, sequenceId)) {
3147                getDeleteList(tx, deletes, lowPriorityIndex, sequenceId);
3148            }
3149        }
3150
3151        void getDeleteList(Transaction tx, ArrayList<Entry<Long, MessageKeys>> deletes,
3152                BTreeIndex<Long, MessageKeys> index, Long sequenceId) throws IOException {
3153
3154            Iterator<Entry<Long, MessageKeys>> iterator = index.iterator(tx, sequenceId, null);
3155            deletes.add(iterator.next());
3156        }
3157
3158        long getNextMessageId(int priority) {
3159            return nextMessageId++;
3160        }
3161
3162        MessageKeys get(Transaction tx, Long key) throws IOException {
3163            MessageKeys result = defaultPriorityIndex.get(tx, key);
3164            if (result == null) {
3165                result = highPriorityIndex.get(tx, key);
3166                if (result == null) {
3167                    result = lowPriorityIndex.get(tx, key);
3168                    lastGetPriority = LO;
3169                } else {
3170                    lastGetPriority = HI;
3171                }
3172            } else {
3173                lastGetPriority = DEF;
3174            }
3175            return result;
3176        }
3177
3178        MessageKeys put(Transaction tx, int priority, Long key, MessageKeys value) throws IOException {
3179            if (priority == javax.jms.Message.DEFAULT_PRIORITY) {
3180                return defaultPriorityIndex.put(tx, key, value);
3181            } else if (priority > javax.jms.Message.DEFAULT_PRIORITY) {
3182                return highPriorityIndex.put(tx, key, value);
3183            } else {
3184                return lowPriorityIndex.put(tx, key, value);
3185            }
3186        }
3187
3188        Iterator<Entry<Long, MessageKeys>> iterator(Transaction tx) throws IOException{
3189            return new MessageOrderIterator(tx,cursor,this);
3190        }
3191
3192        Iterator<Entry<Long, MessageKeys>> iterator(Transaction tx, MessageOrderCursor m) throws IOException{
3193            return new MessageOrderIterator(tx,m,this);
3194        }
3195
3196        public byte lastGetPriority() {
3197            return lastGetPriority;
3198        }
3199
3200        public boolean alreadyDispatched(Long sequence) {
3201            return (cursor.highPriorityCursorPosition > 0 && cursor.highPriorityCursorPosition >= sequence) ||
3202                    (cursor.defaultCursorPosition > 0 && cursor.defaultCursorPosition >= sequence) ||
3203                    (cursor.lowPriorityCursorPosition > 0 && cursor.lowPriorityCursorPosition >= sequence);
3204        }
3205
3206        public void trackPendingAdd(Long seq) {
3207            synchronized (pendingAdditions) {
3208                pendingAdditions.add(seq);
3209            }
3210        }
3211
3212        public void trackPendingAddComplete(Long seq) {
3213            synchronized (pendingAdditions) {
3214                pendingAdditions.remove(seq);
3215            }
3216        }
3217
3218        public Long minPendingAdd() {
3219            synchronized (pendingAdditions) {
3220                if (!pendingAdditions.isEmpty()) {
3221                    return pendingAdditions.get(0);
3222                } else {
3223                    return null;
3224                }
3225            }
3226        }
3227
3228        class MessageOrderIterator implements Iterator<Entry<Long, MessageKeys>>{
3229            Iterator<Entry<Long, MessageKeys>>currentIterator;
3230            final Iterator<Entry<Long, MessageKeys>>highIterator;
3231            final Iterator<Entry<Long, MessageKeys>>defaultIterator;
3232            final Iterator<Entry<Long, MessageKeys>>lowIterator;
3233
3234            MessageOrderIterator(Transaction tx, MessageOrderCursor m, MessageOrderIndex messageOrderIndex) throws IOException {
3235                Long pendingAddLimiter = messageOrderIndex.minPendingAdd();
3236                this.defaultIterator = defaultPriorityIndex.iterator(tx, m.defaultCursorPosition, pendingAddLimiter);
3237                if (highPriorityIndex != null) {
3238                    this.highIterator = highPriorityIndex.iterator(tx, m.highPriorityCursorPosition, pendingAddLimiter);
3239                } else {
3240                    this.highIterator = null;
3241                }
3242                if (lowPriorityIndex != null) {
3243                    this.lowIterator = lowPriorityIndex.iterator(tx, m.lowPriorityCursorPosition, pendingAddLimiter);
3244                } else {
3245                    this.lowIterator = null;
3246                }
3247            }
3248
3249            @Override
3250            public boolean hasNext() {
3251                if (currentIterator == null) {
3252                    if (highIterator != null) {
3253                        if (highIterator.hasNext()) {
3254                            currentIterator = highIterator;
3255                            return currentIterator.hasNext();
3256                        }
3257                        if (defaultIterator.hasNext()) {
3258                            currentIterator = defaultIterator;
3259                            return currentIterator.hasNext();
3260                        }
3261                        if (lowIterator.hasNext()) {
3262                            currentIterator = lowIterator;
3263                            return currentIterator.hasNext();
3264                        }
3265                        return false;
3266                    } else {
3267                        currentIterator = defaultIterator;
3268                        return currentIterator.hasNext();
3269                    }
3270                }
3271                if (highIterator != null) {
3272                    if (currentIterator.hasNext()) {
3273                        return true;
3274                    }
3275                    if (currentIterator == highIterator) {
3276                        if (defaultIterator.hasNext()) {
3277                            currentIterator = defaultIterator;
3278                            return currentIterator.hasNext();
3279                        }
3280                        if (lowIterator.hasNext()) {
3281                            currentIterator = lowIterator;
3282                            return currentIterator.hasNext();
3283                        }
3284                        return false;
3285                    }
3286
3287                    if (currentIterator == defaultIterator) {
3288                        if (lowIterator.hasNext()) {
3289                            currentIterator = lowIterator;
3290                            return currentIterator.hasNext();
3291                        }
3292                        return false;
3293                    }
3294                }
3295                return currentIterator.hasNext();
3296            }
3297
3298            @Override
3299            public Entry<Long, MessageKeys> next() {
3300                Entry<Long, MessageKeys> result = currentIterator.next();
3301                if (result != null) {
3302                    Long key = result.getKey();
3303                    if (highIterator != null) {
3304                        if (currentIterator == defaultIterator) {
3305                            lastDefaultKey = key;
3306                        } else if (currentIterator == highIterator) {
3307                            lastHighKey = key;
3308                        } else {
3309                            lastLowKey = key;
3310                        }
3311                    } else {
3312                        lastDefaultKey = key;
3313                    }
3314                }
3315                return result;
3316            }
3317
3318            @Override
3319            public void remove() {
3320                throw new UnsupportedOperationException();
3321            }
3322        }
3323    }
3324
3325    private static class HashSetStringMarshaller extends VariableMarshaller<HashSet<String>> {
3326        final static HashSetStringMarshaller INSTANCE = new HashSetStringMarshaller();
3327
3328        @Override
3329        public void writePayload(HashSet<String> object, DataOutput dataOut) throws IOException {
3330            ByteArrayOutputStream baos = new ByteArrayOutputStream();
3331            ObjectOutputStream oout = new ObjectOutputStream(baos);
3332            oout.writeObject(object);
3333            oout.flush();
3334            oout.close();
3335            byte[] data = baos.toByteArray();
3336            dataOut.writeInt(data.length);
3337            dataOut.write(data);
3338        }
3339
3340        @Override
3341        @SuppressWarnings("unchecked")
3342        public HashSet<String> readPayload(DataInput dataIn) throws IOException {
3343            int dataLen = dataIn.readInt();
3344            byte[] data = new byte[dataLen];
3345            dataIn.readFully(data);
3346            ByteArrayInputStream bais = new ByteArrayInputStream(data);
3347            ObjectInputStream oin = new ObjectInputStream(bais);
3348            try {
3349                return (HashSet<String>) oin.readObject();
3350            } catch (ClassNotFoundException cfe) {
3351                IOException ioe = new IOException("Failed to read HashSet<String>: " + cfe);
3352                ioe.initCause(cfe);
3353                throw ioe;
3354            }
3355        }
3356    }
3357
3358    public File getIndexDirectory() {
3359        return indexDirectory;
3360    }
3361
3362    public void setIndexDirectory(File indexDirectory) {
3363        this.indexDirectory = indexDirectory;
3364    }
3365
3366    interface IndexAware {
3367        public void sequenceAssignedWithIndexLocked(long index);
3368    }
3369
3370    public String getPreallocationScope() {
3371        return preallocationScope;
3372    }
3373
3374    public void setPreallocationScope(String preallocationScope) {
3375        this.preallocationScope = preallocationScope;
3376    }
3377
3378    public String getPreallocationStrategy() {
3379        return preallocationStrategy;
3380    }
3381
3382    public void setPreallocationStrategy(String preallocationStrategy) {
3383        this.preallocationStrategy = preallocationStrategy;
3384    }
3385}