All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.apple.foundationdb.map.BunchedMapIterator Maven / Gradle / Ivy

There is a newer version: 2.8.110.0
Show newest version
/*
 * BunchedMapIterator.java
 *
 * This source file is part of the FoundationDB open source project
 *
 * Copyright 2015-2018 Apple Inc. and the FoundationDB project authors
 *
 * 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.apple.foundationdb.map;

import com.apple.foundationdb.annotation.API;
import com.apple.foundationdb.KeyValue;
import com.apple.foundationdb.ReadTransaction;
import com.apple.foundationdb.async.AsyncPeekIterator;
import com.apple.foundationdb.async.AsyncUtil;
import com.apple.foundationdb.subspace.Subspace;
import com.apple.foundationdb.tuple.ByteArrayUtil;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.CompletableFuture;

/**
 * An iterator implementation that will iterate over the keys of a {@link BunchedMap}.
 * This handles "unbunching" the keys in the database so that the user can ignore
 * the underlying complexities of the on-disk format. The iterator may return keys
 * in either ascending or descending order depending on whether it is initialized to
 * be a reverse or non-reverse scan. This class is not thread safe.
 *
 * @param  type of keys in the map
 * @param  type of values in the map
 */
@API(API.Status.EXPERIMENTAL)
public class BunchedMapIterator implements AsyncPeekIterator> {
    @Nonnull private final AsyncPeekIterator underlying;
    @Nonnull private final Subspace subspace;
    @Nonnull private final ReadTransaction tr;
    @Nonnull private final byte[] subspaceKey;
    @Nonnull private final BunchedSerializer serializer;
    @Nonnull private final Comparator keyComparator;
    @Nullable private final K continuationKey;
    private final boolean reverse;
    private final int limit;

    @Nullable private CompletableFuture hasNextFuture;
    private boolean continuationSatisfied;
    @Nullable private List> currEntryList;
    private int currEntryIndex;
    @Nullable private K lastKey;
    private int returned;
    private boolean done;

    // In general, this class should not be instantiated by code outside
    // this package. Instead, it should use the scan() method on BunchedMaps.
    BunchedMapIterator(@Nonnull AsyncPeekIterator underlying,
                       @Nonnull ReadTransaction tr,
                       @Nonnull Subspace subspace,
                       @Nonnull byte[] subspaceKey,
                       @Nonnull BunchedSerializer serializer,
                       @Nonnull Comparator keyComparator,
                       @Nullable K continuationKey,
                       int limit,
                       boolean reverse) {
        this.underlying = underlying;
        this.tr = tr;
        this.subspace = subspace;
        this.subspaceKey = subspaceKey;
        this.serializer = serializer;
        this.keyComparator = keyComparator;
        this.continuationKey = continuationKey;
        this.continuationSatisfied = continuationKey == null;
        this.limit = limit;
        this.reverse = reverse;
        this.currEntryList = null;
        this.currEntryIndex = -1;
        this.lastKey = null;
        this.returned = 0;
        this.done = false;
    }

    @Override
    public CompletableFuture onHasNext() {
        if (done) {
            return AsyncUtil.READY_FALSE;
        }
        if (currEntryList != null && currEntryIndex >= 0 && currEntryIndex < currEntryList.size()) {
            return AsyncUtil.READY_TRUE;
        }
        if (hasNextFuture == null) {
            hasNextFuture = underlying.onHasNext().thenCompose(doesHaveNext -> {
                if (doesHaveNext) {
                    return AsyncUtil.whileTrue(() -> {
                        KeyValue underlyingNext = underlying.peek();
                        if (!subspace.contains(underlyingNext.getKey())) {
                            if (ByteArrayUtil.compareUnsigned(underlyingNext.getKey(), subspaceKey) * (reverse ? -1 : 1) < 0) {
                                // We haven't gotten to this subspace yet, so try the next key.
                                underlying.next();
                                return underlying.onHasNext();
                            } else {
                                // We are done iterating through this subspace. Return
                                // without advancing the scan to support scanning
                                // over multiple subspaces.
                                done = true;
                                return AsyncUtil.READY_FALSE;
                            }
                        }
                        underlying.next(); // Advance the underlying scan.
                        final K boundaryKey = serializer.deserializeKey(underlyingNext.getKey(), subspaceKey.length);
                        List> nextEntryList = serializer.deserializeEntries(boundaryKey, underlyingNext.getValue());
                        if (nextEntryList.isEmpty()) {
                            // No entries in list. Try next key.
                            return underlying.onHasNext();
                        }
                        int nextItemIndex = reverse ? nextEntryList.size() - 1 : 0;
                        if (!continuationSatisfied) {
                            while (nextItemIndex >= 0 && nextItemIndex < nextEntryList.size()
                                    && keyComparator.compare(continuationKey, nextEntryList.get(nextItemIndex).getKey()) * (reverse ? -1 : 1) >= 0) {
                                nextItemIndex += reverse ? -1 : 1;
                            }
                            if (nextItemIndex < 0 || nextItemIndex >= nextEntryList.size()) {
                                // The continuation still wasn't satisfied after going
                                // through this entry. Move on to the next key in the database.
                                return underlying.onHasNext();
                            } else {
                                continuationSatisfied = true;
                            }
                        }
                        // TODO: We can be more exact about conflict ranges here.
                        currEntryIndex = nextItemIndex;
                        currEntryList = nextEntryList;
                        return AsyncUtil.READY_FALSE;
                    }, tr.getExecutor()).thenApply(vignore -> {
                        if (currEntryList == null || currEntryIndex < 0 || currEntryIndex >= currEntryList.size()) {
                            done = true;
                        }
                        return !done;
                    });
                } else {
                    // Exhausted scan.
                    done = true;
                    return AsyncUtil.READY_FALSE;
                }
            });
        }
        return hasNextFuture;
    }

    @Override
    public boolean hasNext() {
        return onHasNext().join();
    }

    @Override
    @Nonnull
    public Map.Entry peek() {
        if (hasNext()) {
            // The hasNext method should enforce that currEntryList is not null
            // and that currEntryIndex is in the right range.
            if (currEntryList == null || currEntryIndex >= currEntryList.size() || currEntryIndex < 0) {
                throw new IllegalStateException();
            }
            return currEntryList.get(currEntryIndex);
        } else {
            throw new NoSuchElementException();
        }
    }

    @Override
    @Nonnull
    public Map.Entry next() {
        Map.Entry nextEntry = peek();
        lastKey = nextEntry.getKey();
        hasNextFuture = null;
        currEntryIndex += reverse ? -1 : 1;
        returned += 1;
        if (limit != ReadTransaction.ROW_LIMIT_UNLIMITED && returned >= limit) {
            done = true;
        }
        return nextEntry;
    }

    /**
     * Returns a continuation that can be passed to future calls of
     * {@link BunchedMap#scan(ReadTransaction, Subspace, byte[], int, boolean) BunchedMap.scan()}.
     * If the iterator has not yet returned anything or if the iterator is exhausted,
     * then this will return null. An iterator will be marked as exhausted
     * only if there are no more elements in the map.
     *
     * @return a continuation that can be used to resume iteration later
     */
    @Nullable
    public byte[] getContinuation() {
        if (lastKey == null || done && (limit == ReadTransaction.ROW_LIMIT_UNLIMITED || returned < limit)) {
            // We exhausted the scan.
            return null;
        } else {
            // We (potentially) hit the limit applied to this scan.
            // It's possible we both exhausted the underlying scan
            // and hit the limit, but the next time this gets called,
            // it just won't return anything.
            return serializer.serializeKey(lastKey);
        }
    }

    /**
     * Returns whether this iterator returns keys in reverse order. Forward order
     * is defined to be ascending order (by the key according to the key comparator
     * used in the corresponding {@link BunchedMap} that generated this iterator).
     * Reverse order is thus equivalent to descending order.
     *
     * @return whether this iterator returns values in reverse order
     */
    public boolean isReverse() {
        return reverse;
    }

    /**
     * Get the limit from the iterator. This is the maximum number of entries that
     * the iterator should return. If this iterator has no limit, it will return
     * {@link ReadTransaction#ROW_LIMIT_UNLIMITED}.
     *
     * @return the limit of this iterator
     */
    public int getLimit() {
        return limit;
    }

    @Override
    public void cancel() {
        underlying.cancel();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy