com.cinchapi.concourse.server.storage.temp.Limbo.java Source code

Java tutorial

Introduction

Here is the source code for com.cinchapi.concourse.server.storage.temp.Limbo.java

Source

/*
 * Copyright (c) 2013-2016 Cinchapi Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.cinchapi.concourse.server.storage.temp;

import java.util.Comparator;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;

import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;

import com.cinchapi.common.base.TernaryTruth;
import com.cinchapi.concourse.server.model.TObjectSorter;
import com.cinchapi.concourse.server.model.Text;
import com.cinchapi.concourse.server.model.Value;
import com.cinchapi.concourse.server.storage.Action;
import com.cinchapi.concourse.server.storage.BaseStore;
import com.cinchapi.concourse.server.storage.Inventory;
import com.cinchapi.concourse.server.storage.PermanentStore;
import com.cinchapi.concourse.server.storage.db.Database;
import com.cinchapi.concourse.thrift.Operator;
import com.cinchapi.concourse.thrift.TObject;
import com.cinchapi.concourse.time.Time;
import com.cinchapi.concourse.util.MultimapViews;
import com.cinchapi.concourse.util.TMaps;
import com.cinchapi.concourse.util.TStrings;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import static com.google.common.collect.Maps.newLinkedHashMap;

/**
 * {@link Limbo} is a lightweight in-memory proxy store that is a suitable cache
 * or fast, albeit temporary, store for data that will eventually be persisted
 * to a {@link PermanentStore}.
 * <p>
 * The store is designed to write data very quickly <strong>
 * <em>at the expense of much slower read time.</em></strong> {@code Limbo} does
 * not index<sup>1</sup> any of the data it stores, so reads are not as
 * efficient as they would normally be in the {@link Database}.
 * </p>
 * <p>
 * This class provides naive read implementations for the methods specified in
 * the {@link WritableStore} interface, but the subclass is free to override
 * those methods to provide smarter implementations of introduce concurrency
 * controls.
 * </p>
 * <sup>1</sup> - All reads are O(n) because {@code Limbo} uses an
 * {@link #iterator()} to traverse the {@link Write} objects that it stores.
 * 
 * @author Jeff Nelson
 */
@NotThreadSafe
public abstract class Limbo extends BaseStore implements Iterable<Write> {

    /**
     * Return {@code true} if {@code input} matches {@code operator} in relation
     * to {@code values}.
     * 
     * @param input
     * @param operator
     * @param values
     * @return {@code true} if {@code input} matches
     */
    protected static boolean matches(Value input, Operator operator, TObject... values) {
        Value v1 = Value.wrap(values[0]);
        switch (operator) {
        case EQUALS:
            return v1.equals(input);
        case NOT_EQUALS:
            return !v1.equals(input);
        case GREATER_THAN:
            return v1.compareTo(input) < 0;
        case GREATER_THAN_OR_EQUALS:
            return v1.compareTo(input) <= 0;
        case LESS_THAN:
            return v1.compareTo(input) > 0;
        case LESS_THAN_OR_EQUALS:
            return v1.compareTo(input) >= 0;
        case BETWEEN:
            Preconditions.checkArgument(values.length > 1);
            Value v2 = Value.wrap(values[1]);
            return v1.compareTo(input) <= 0 && v2.compareTo(input) > 0;
        case REGEX:
            return input.getObject().toString().matches(v1.getObject().toString());
        case NOT_REGEX:
            return !input.getObject().toString().matches(v1.getObject().toString());
        default:
            throw new UnsupportedOperationException();
        }
    }

    /**
     * A Predicate that is used to filter out empty sets.
     */
    protected static final Predicate<Set<? extends Object>> emptySetFilter = new Predicate<Set<? extends Object>>() {

        @Override
        public boolean apply(@Nullable Set<? extends Object> input) {
            return !input.isEmpty();
        }

    };

    @Override
    public Map<Long, String> audit(long record) {
        Map<Long, String> audit = Maps.newTreeMap();
        for (Iterator<Write> it = iterator(); it.hasNext();) {
            Write write = it.next();
            if (write.getRecord().longValue() == record) {
                audit.put(write.getVersion(), write.toString());
            }
        }
        return audit;

    }

    @Override
    public Map<Long, String> audit(String key, long record) {
        Map<Long, String> audit = Maps.newTreeMap();
        for (Iterator<Write> it = iterator(); it.hasNext();) {
            Write write = it.next();
            if (write.getKey().toString().equals(key) && write.getRecord().longValue() == record) {
                audit.put(write.getVersion(), write.toString());
            }
        }
        return audit;

    }

    @Override
    public Map<TObject, Set<Long>> browse(String key) {
        return browse(key, Time.NONE);
    }

    @Override
    public Map<TObject, Set<Long>> browse(String key, long timestamp) {
        Map<TObject, Set<Long>> context = Maps.newTreeMap(TObjectSorter.INSTANCE);
        return browse(key, timestamp, context);
    }

    /**
     * Calculate the browsable view of {@code key} at {@code timestamp} using
     * prior {@code context} as if it were also a part of the Buffer.
     * 
     * @param key
     * @param timestamp
     * @param context
     * @return a possibly empty Map of data
     */
    public Map<TObject, Set<Long>> browse(String key, long timestamp, Map<TObject, Set<Long>> context) {
        if (timestamp >= getOldestWriteTimestamp()) {
            for (Iterator<Write> it = iterator(); it.hasNext();) {
                Write write = it.next();
                if (write.getKey().toString().equals(key) && write.getVersion() <= timestamp) {
                    Set<Long> records = context.get(write.getValue().getTObject());
                    if (records == null) {
                        records = Sets.newLinkedHashSet();
                        context.put(write.getValue().getTObject(), records);
                    }
                    if (write.getType() == Action.ADD) {
                        records.add(write.getRecord().longValue());
                    } else {
                        records.remove(write.getRecord().longValue());
                    }
                } else if (write.getVersion() > timestamp) {
                    break;
                } else {
                    continue;
                }
            }
        }
        return Maps.newTreeMap((SortedMap<TObject, Set<Long>>) Maps.filterValues(context, emptySetFilter));
    }

    @Override
    public Map<Long, Set<TObject>> chronologize(String key, long record, long start, long end) {
        Map<Long, Set<TObject>> context = Maps.newLinkedHashMap();
        return chronologize(key, record, start, end, context);
    }

    /**
     * Return a time series that contains the values stored for {@code key} in
     * {@code record} at each modification timestamp between {@code start}
     * (inclusive) and {@code end} (exclusive).
     * 
     * @param key the field name
     * @param record the record id
     * @param start the start timestamp (inclusive)
     * @param end the end timestamp (exclusive)
     * @param context the prior context
     * @return a {@link Map mapping} from modification timestamp to a non-empty
     *         {@link Set} of values that were contained at that timestamp
     */
    public Map<Long, Set<TObject>> chronologize(String key, long record, long start, long end,
            Map<Long, Set<TObject>> context) {
        Set<TObject> snapshot = Iterables.getLast(context.values(), Sets.<TObject>newLinkedHashSet());
        if (snapshot.isEmpty() && !context.isEmpty()) {
            // CON-474: Empty set is placed in the context if it was the last
            // snapshot know to the database
            context.remove(Time.NONE);
        }
        for (Iterator<Write> it = iterator(); it.hasNext();) {
            Write write = it.next();
            long timestamp = write.getVersion();
            if (timestamp >= end) {
                break;
            } else {
                Text writeKey = write.getKey();
                long writeRecord = write.getRecord().longValue();
                Action action = write.getType();
                if (writeKey.toString().equals(key) && writeRecord == record) {
                    snapshot = Sets.newLinkedHashSet(snapshot);
                    Value writeValue = write.getValue();
                    if (action == Action.ADD) {
                        snapshot.add(writeValue.getTObject());
                    } else if (action == Action.REMOVE) {
                        snapshot.remove(writeValue.getTObject());
                    }
                    if (timestamp >= start && !snapshot.isEmpty()) {
                        context.put(timestamp, snapshot);
                    }
                }
            }
        }
        return context;
    }

    @Override
    public boolean contains(long record) {
        for (Iterator<Write> it = iterator(); it.hasNext();) {
            Write write = it.next();
            if (write.getRecord().longValue() == record) {
                return true;
            }
        }
        return false;
    }

    @Override
    public Set<Long> getAllRecords() {
        Set<Long> records = Sets.newHashSet();
        for (Iterator<Write> it = iterator(); it.hasNext();) {
            Write write = it.next();
            records.add(write.getRecord().longValue());
        }
        return records;
    }

    /**
     * Calculate the description for {@code record} using prior {@code context}
     * as if it were also a part of the Buffer.
     * 
     * @param record
     * @param timestamp
     * @param context
     * @return a possibly empty Set of keys
     */
    public Set<String> describe(long record, long timestamp, Map<String, Set<TObject>> context) {
        if (timestamp >= getOldestWriteTimestamp()) {
            for (Iterator<Write> it = iterator(); it.hasNext();) {
                Write write = it.next();
                if (write.getRecord().longValue() == record && write.getVersion() <= timestamp) {
                    Set<TObject> values;
                    values = context.get(write.getKey().toString());
                    if (values == null) {
                        values = Sets.newHashSet();
                        context.put(write.getKey().toString(), values);
                    }
                    if (write.getType() == Action.ADD) {
                        values.add(write.getValue().getTObject());
                    } else {
                        values.remove(write.getValue().getTObject());
                    }
                } else if (write.getVersion() > timestamp) {
                    break;
                } else {
                    continue;
                }
            }
        }
        return newLinkedHashMap(Maps.filterValues(context, emptySetFilter)).keySet();
    }

    /**
     * This is an implementation of the {@code findAndBrowse} routine that takes
     * in a prior {@code context}. Find and browse will return a mapping from
     * records that match a criteria (expressed as {@code key} filtered by
     * {@code operator} in relation to one or more {@code values}) to the set of
     * values that cause that record to match the criteria.
     * 
     * @param context
     * @param timestamp
     * @param key
     * @param operator
     * @param values
     * @return the relevant data for the records that satisfy the find query
     */
    public Map<Long, Set<TObject>> explore(Map<Long, Set<TObject>> context, long timestamp, String key,
            Operator operator, TObject... values) {
        if (timestamp >= getOldestWriteTimestamp()) {
            for (Iterator<Write> it = iterator(); it.hasNext();) {
                Write write = it.next();
                long record = write.getRecord().longValue();
                if (write.getVersion() <= timestamp) {
                    if (write.getKey().toString().equals(key) && matches(write.getValue(), operator, values)) {
                        if (write.getType() == Action.ADD) {
                            MultimapViews.put(context, record, write.getValue().getTObject());
                        } else {
                            MultimapViews.remove(context, record, write.getValue().getTObject());
                        }
                    }
                } else {
                    break;
                }
            }
        }
        return TMaps.asSortedMap(context);
    }

    /**
     * Return the number of milliseconds that this store desires any back to
     * back transport requests to pause in between.
     * 
     * @return the pause time
     */
    public int getDesiredTransportSleepTimeInMs() {
        return 0;
    }

    /**
     * Insert {@code write} into the store <strong>without performing any
     * validity checks</strong>.
     * <p>
     * This method is <em>only</em> safe to call from a context that performs
     * its own validity checks (i.e. a {@link BufferedStore}).
     * 
     * @param write
     * @return {@code true}
     */
    public final boolean insert(Write write) {
        return insert(write, true);
    }

    /**
     * Insert {@code write} into the store <strong>without performing any
     * validity checks</strong> and specify whether a {@code sync} should occur
     * or not. By default, syncs are meaningless in {@link Limbo}, but some
     * implementations may wish to provide guarantees that the write will be
     * durably stored.
     * <p>
     * This method is <em>only</em> safe to call from a context that performs
     * its own validity checks (i.e. a {@link BufferedStore}).
     * 
     * @param write - The write to append
     * @param sync - a flag that controls whether this instance will make an
     *            attempt to durably persist the data to some backing store.
     *            Simply ignore this flag if the implementation does not support
     *            durability
     * @return {@code true}
     */
    public abstract boolean insert(Write write, boolean sync);

    /**
     * {@inheritDoc}
     * <p>
     * <strong>NOTE:</strong> The subclass <em>may</em> override this method to
     * provide an iterator with granular locking functionality for increased
     * throughput.
     * </p>
     */
    @Override
    public abstract Iterator<Write> iterator();

    @Override
    public Set<Long> search(String key, String query) {
        Map<Long, Set<Value>> rtv = Maps.newHashMap();
        String[] needle = TStrings.stripStopWordsAndTokenize(query.toLowerCase());
        if (needle.length > 0) {
            for (Iterator<Write> it = getSearchIterator(key); it.hasNext();) {
                Write write = it.next();
                Value value = write.getValue();
                long record = write.getRecord().longValue();
                if (isPossibleSearchMatch(key, write, value)) {
                    /*
                     * NOTE: It is not enough to merely check if the stored text
                     * contains the query because the Database does infix
                     * indexing/searching, which has some subtleties:
                     * 1. Stop words are removed from the both stored indices
                     * and the search query
                     * 2. A query and document are considered to match if the
                     * document contains a sequence of terms where each term or
                     * a substring of the term matches the term in the same
                     * relative position of the query.
                     */
                    // CON-10: compare lowercase for case insensitive search
                    String stored = (String) (value.getObject());
                    String[] haystack = TStrings.stripStopWordsAndTokenize(stored.toLowerCase());
                    if (haystack.length > 0 && TStrings.isInfixSearchMatch(needle, haystack)) {
                        Set<Value> values = rtv.get(record);
                        if (values == null) {
                            values = Sets.newHashSet();
                            rtv.put(record, values);
                        }
                        if (write.getType() == Action.REMOVE) {
                            values.remove(value);
                        } else {
                            values.add(value);
                        }

                    }
                }
            }
        }
        // FIXME sort search results based on frequency (see
        // SearchRecord#search())
        return newLinkedHashMap(Maps.filterValues(rtv, emptySetFilter)).keySet();
    }

    @Override
    public Map<String, Set<TObject>> select(long record) {
        return select(record, Time.NONE);
    }

    @Override
    public Map<String, Set<TObject>> select(long record, long timestamp) {
        Map<String, Set<TObject>> context = Maps.newTreeMap(new Comparator<String>() {

            @Override
            public int compare(String s1, String s2) {
                return s1.compareToIgnoreCase(s2);
            }

        });
        return select(record, timestamp, context);
    }

    /**
     * Calculate the browsable view of {@code record} at {@code timestamp} using
     * prior {@code context} as if it were also a part of the Buffer.
     * 
     * @param key
     * @param timestamp
     * @param context
     * @return a possibly empty Map of data
     */
    public Map<String, Set<TObject>> select(long record, long timestamp, Map<String, Set<TObject>> context) {
        if (timestamp >= getOldestWriteTimestamp()) {
            for (Iterator<Write> it = iterator(); it.hasNext();) {
                Write write = it.next();
                if (write.getRecord().longValue() == record && write.getVersion() <= timestamp) {
                    Set<TObject> values;
                    values = context.get(write.getKey().toString());
                    if (values == null) {
                        values = Sets.newHashSet();
                        context.put(write.getKey().toString(), values);
                    }
                    if (write.getType() == Action.ADD) {
                        values.add(write.getValue().getTObject());
                    } else {
                        values.remove(write.getValue().getTObject());
                    }
                } else if (write.getVersion() > timestamp) {
                    break;
                } else {
                    continue;
                }
            }
        }
        return Maps.newTreeMap((SortedMap<String, Set<TObject>>) Maps.filterValues(context, emptySetFilter));
    }

    @Override
    public Set<TObject> select(String key, long record) {
        return select(key, record, Time.NONE);
    }

    @Override
    public Set<TObject> select(String key, long record, long timestamp) {
        return select(key, record, timestamp, Sets.<TObject>newLinkedHashSet());
    }

    /**
     * Fetch the values mapped from {@code key} in {@code record} at
     * {@code timestamp} using prior {@code context} as if it were also a part
     * of the Buffer.
     * 
     * @param key
     * @param record
     * @param timestamp
     * @param context
     * @return the values
     */
    public Set<TObject> select(String key, long record, long timestamp, Set<TObject> context) {
        if (timestamp >= getOldestWriteTimestamp()) {
            for (Iterator<Write> it = iterator(); it.hasNext();) {
                Write write = it.next();
                if (write.getVersion() <= timestamp) {
                    if (key.equals(write.getKey().toString()) && record == write.getRecord().longValue()) {
                        if (write.getType() == Action.ADD) {
                            context.add(write.getValue().getTObject());
                        } else {
                            context.remove(write.getValue().getTObject());
                        }
                    }
                } else {
                    break;
                }
            }
        }
        return context;
    }

    /**
     * If the implementation supports durable storage, this method guarantees
     * that all the data contained here-within is durably persisted. Otherwise,
     * this method is meaningless and returns immediately.
     */
    public void sync() {
        /* noop */}

    /**
     * Transport the content of this store to {@code destination}.
     * 
     * @param destination
     */
    public final void transport(PermanentStore destination) {
        transport(destination, true);
    }

    /**
     * Transport the content of this store to {@code destination} with the
     * directive to {@code sync} or not. A sync guarantees that the transported
     * data is durably persisted within the {@link PermanentStore}.
     * 
     * @param destination - the recipient store for the data
     * @param syncAfterEach - a flag that controls whether a call is always made
     *            to durably persist (i.e. fsync) in the {@code destination}
     *            after each write is transported
     */
    public void transport(PermanentStore destination, boolean syncAfterEach) {
        for (Iterator<Write> it = iterator(); it.hasNext();) {
            destination.accept(it.next(), syncAfterEach);
            it.remove();
        }
    }

    @Override
    public boolean verify(String key, TObject value, long record) {
        return verify(key, value, record, Time.NONE);
    }

    @Override
    public boolean verify(String key, TObject value, long record, long timestamp) {
        return verify(Write.notStorable(key, value, record), timestamp);
    }

    /**
     * Return {@code true} if {@code write} represents a data mapping that
     * currently exists using {@code exists} as prior context.
     * <p>
     * <strong>This method is called from
     * {@link BufferedStore#verify(String, TObject, long)}.</strong>
     * </p>
     * 
     * @param write
     * @return {@code true} if {@code write} currently appears an odd number of
     *         times
     */
    public boolean verify(Write write, boolean exists) {
        return verify(write, Time.NONE, exists);
    }

    /**
     * Return {@code true} if {@code write} represents a data mapping that
     * exists at {@code timestamp}.
     * <p>
     * <strong>This method is called from
     * {@link BufferedStore#verify(String, TObject, long, long)}.</strong>
     * </p>
     * 
     * @param write
     * @param timestamp
     * @return {@code true} if {@code write} appears an odd number of times at
     *         {@code timestamp}
     */
    public boolean verify(Write write, long timestamp) {
        return verify(write, timestamp, false);
    }

    /**
     * Return {@code true} if {@code write} represents a data mapping that
     * exists at {@code timestamp}, using {@code exists} as prior context.
     * <p>
     * <strong>NOTE: ALL OTHER VERIFY METHODS DEFER TO THIS ONE.</strong>
     * </p>
     * 
     * @param write
     * @param timestamp
     * @param exists
     * @return {@code true} if {@code write} appears an odd number of times at
     *         {@code timestamp}
     */
    public boolean verify(Write write, long timestamp, boolean exists) {
        if (timestamp >= getOldestWriteTimestamp()) {
            for (Iterator<Write> it = iterator(); it.hasNext();) {
                Write stored = it.next();
                if (stored.getVersion() <= timestamp) {
                    if (stored.equals(write)) {
                        exists ^= true; // toggle boolean
                    }
                } else {
                    break;
                }
            }
        }
        return exists;
    }

    /**
     * A specialized implementation to possibly verify the existence of
     * {@code write} using three-valued logic. This routine allows the caller to
     * get a potentially definitive answer by only consulting this store instead
     * of having to gather prior context beforehand.
     * <p>
     * This method will respond in one of three ways when verifying the
     * existence of {@code write}:
     * <ul>
     * <li>Definitively {@link TernaryTruth#TRUE true} if the {@code write}
     * appears in this store at least once and the most recent appearance is the
     * result of an {@link Action#ADD add} operation.</li>
     * <li>Definitively {@link TernaryTruth#TRUE false} if the {@code write}
     * appears in the Buffer at least once and the most recent appearance is the
     * result of a {@link Action#REMOVE remove} operation.</li>
     * <li>{@link TernaryTruth#UNSURE} if the {@code write}'s
     * {@link Write#getRecord()} appears is in the inventory AND the
     * {@code write} does not appear in the Buffer.</li>
     * </ul>
     * </p>
     * 
     * @param write the {@link Write} to verify
     * @return the appropriate {@link TernaryTruth} value that corresponds to
     *         the Buffer's ability to verify the existence of {@code write}
     */
    public final TernaryTruth verifyFast(Write write) {
        return verifyFast(write, Time.NONE);
    }

    /**
     * A specialized implementation to possibly verify the existence of
     * {@code write} at {@code timestamp} using three-valued logic.
     * This routine allows the caller to get a potentially definitive answer by
     * only consulting the Buffer instead of having to gather prior context
     * beforehand.
     * <p>
     * This method will respond in one of three ways when verifying the
     * existence of {@code write} at {@code timestamp}:
     * <ul>
     * <li>Definitively {@link TernaryTruth#TRUE true} if the {@code write}'s
     * {@link Write#getRecord record} is in the {@link #inventory} AND the
     * {@code write} appears in the Buffer at least once on or before timestamp
     * and the appearance most recent to {@code timestamp} is the result of an
     * {@link Action#ADD add} operation.</li>
     * <li>Definitively {@link TernaryTruth#TRUE false} if the {@code write}'s
     * {@link Write#getRecord record} is NOT in the {@link #inventory} OR the
     * {@code write} appears in the Buffer at least once on or before timestamp
     * and the appearance most recent to {@code timestamp} is the result of a
     * {@link Action#REMOVE remove} operation.</li>
     * <li>{@link TernaryTruth#UNSURE} if the {@code write}'s
     * {@link Write#getRecord()} does not appear in this store at
     * {@code timestamp}</li>
     * </ul>
     * </p>
     * 
     * @param write the {@link Write} to verify
     * @param timestamp the timestamp at which the verification should happen
     * @return the appropriate {@link TernaryTruth} value that corresponds to
     *         the store's ability to verify the existence of {@code write} at
     *         {@code timestamp}
     */
    public TernaryTruth verifyFast(Write write, long timestamp) {
        Action action = getLastWriteAction(write, timestamp);
        if (action == Action.ADD) {
            return TernaryTruth.TRUE;
        } else if (action == Action.REMOVE) {
            return TernaryTruth.FALSE;
        } else {
            return TernaryTruth.UNSURE;
        }
    }

    /**
     * A specialized implementation to possibly verify the existence of
     * {@code write} using three-valued logic. This routine allows the caller to
     * get a potentially definitive answer by only consulting this store instead
     * of having to gather prior context beforehand.
     * <p>
     * This method will respond in one of three ways when verifying the
     * existence of {@code write}:
     * <ul>
     * <li>Definitively {@link TernaryTruth#TRUE true} if the {@code write}
     * appears in this store at least once and the most recent appearance is the
     * result of an {@link Action#ADD add} operation.</li>
     * <li>Definitively {@link TernaryTruth#TRUE false} if the {@code write}
     * appears in the Buffer at least once and the most recent appearance is the
     * result of a {@link Action#REMOVE remove} operation.</li>
     * <li>{@link TernaryTruth#UNSURE} if the {@code write}'s
     * {@link Write#getRecord()} appears is in the inventory AND the
     * {@code write} does not appear in the Buffer.</li>
     * </ul>
     * </p>
     * 
     * @param write the {@link Write} to verify
     * @param inventory an {@link Inventory} instance to possibly speed up the
     *            verify process
     * @return the appropriate {@link TernaryTruth} value that corresponds to
     *         the Buffer's ability to verify the existence of {@code write}
     */
    public final TernaryTruth verifyFast(Write write, Inventory inventory) {
        return verifyFast(write, Time.NONE, inventory);
    }

    /**
     * A specialized implementation to possibly verify the existence of
     * {@code write} at {@code timestamp} using three-valued logic.
     * This routine allows the caller to get a potentially definitive answer by
     * only consulting the Buffer instead of having to gather prior context
     * beforehand.
     * <p>
     * This method will respond in one of three ways when verifying the
     * existence of {@code write} at {@code timestamp}:
     * <ul>
     * <li>Definitively {@link TernaryTruth#TRUE true} if the {@code write}'s
     * {@link Write#getRecord record} is in the {@link #inventory} AND the
     * {@code write} appears in the Buffer at least once on or before timestamp
     * and the appearance most recent to {@code timestamp} is the result of an
     * {@link Action#ADD add} operation.</li>
     * <li>Definitively {@link TernaryTruth#TRUE false} if the {@code write}'s
     * {@link Write#getRecord record} is NOT in the {@link #inventory} OR the
     * {@code write} appears in the Buffer at least once on or before timestamp
     * and the appearance most recent to {@code timestamp} is the result of a
     * {@link Action#REMOVE remove} operation.</li>
     * <li>{@link TernaryTruth#UNSURE} if the {@code write}'s
     * {@link Write#getRecord()} does not appear in this store at
     * {@code timestamp}</li>
     * </ul>
     * </p>
     * 
     * @param write the {@link Write} to verify
     * @param timestamp the timestamp at which the verification should happen
     * @param inventory an {@link Inventory} instance to possibly speed up the
     *            verify process
     * @return the appropriate {@link TernaryTruth} value that corresponds to
     *         the store's ability to verify the existence of {@code write} at
     *         {@code timestamp}
     */
    public TernaryTruth verifyFast(Write write, long timestamp, Inventory inventory) {
        if (inventory.contains(write.getRecord().longValue())) {
            return verifyFast(write, timestamp);
        } else {
            return TernaryTruth.FALSE;
        }
    }

    /**
     * Wait (block) until the Buffer has enough data to complete a transport.
     * This method should be called from the external service to avoid busy
     * waiting if continuously transporting data in the background.
     */
    public void waitUntilTransportable() {
        return; // do nothing because Limbo is assumed to always be
                // transportable. But the Buffer will override this method with
                // the appropriate conditions.
    }

    @Override
    protected Map<Long, Set<TObject>> doExplore(long timestamp, String key, Operator operator, TObject... values) {
        return explore(Maps.<Long, Set<TObject>>newLinkedHashMap(), timestamp, key, operator, values);
    }

    @Override
    protected Map<Long, Set<TObject>> doExplore(String key, Operator operator, TObject... values) {
        return explore(Time.NONE, key, operator, values);
    }

    /**
     * Return the timestamp for the oldest write available.
     * 
     * @return {@code timestamp}
     */
    protected abstract long getOldestWriteTimestamp();

    /**
     * Return the {@link Action} associated with the most recent instance of
     * {@code write} at {@code timestamp} in the the store. For example, if
     * {@code timestamp} {@code write} was most recently added, then this method
     * will return {@link Action#ADD}.
     * 
     * @param write the comparison {@link Write} whose most recent action is of
     *            interest
     * @param timestamp the latest timestamp to use when searching
     * @return the most recent write {@link Action action} or {@code null} if
     *         {@code write} was not present in the store at {@code timestamp}
     */
    @Nullable
    protected Action getLastWriteAction(Write write, long timestamp) {
        Action action = null;
        if (timestamp >= getOldestWriteTimestamp()) {
            Iterator<Write> it = iterator();
            while (it.hasNext()) {
                Write stored = it.next();
                if (stored.getVersion() <= timestamp) {
                    if (stored.equals(write)) {
                        action = stored.getType();
                    }
                } else {
                    break;
                }
            }
        }
        return action;
    }

    /**
     * Return the iterator to use in the {@link #search(String, String)} method.
     * 
     * @param key
     * @return the appropriate iterator to use for searching
     */
    protected abstract Iterator<Write> getSearchIterator(String key);

    /**
     * Allows the subclass to define some criteria for the search logic to
     * determine if {@code write} with {@code value} is a possible search match
     * for {@code key}.
     * <p>
     * <strong>NOTE:</strong> This method should NOT check to see if
     * {@code write} is an true search match for {@code value} because that
     * logic is handled in the {@link #search(String, String)} method. The
     * purpose of this method is merely to help quickly eliminate writes that
     * can't possibly be a search match (i.e. because the write has a non-string
     * value or a different key).
     * </p>
     * <p>
     * <strong>NOTE:</strong> The {@link Buffer} uses this method to optimize
     * the check since the iterator it returns in
     * {@link #getSearchIterator(String)} already ensures that {@code write} has
     * the same key component as {@code key}.
     * </p>
     * 
     * @param key
     * @param write
     * @param value
     * @return {@code true} if the write is a possible search match
     */
    protected abstract boolean isPossibleSearchMatch(String key, Write write, Value value);

}