diff --git a/modules/accord b/modules/accord index 154c05f50d..756f2a1f88 160000 --- a/modules/accord +++ b/modules/accord @@ -1 +1 @@ -Subproject commit 154c05f50dcfe9cb69d14e4a67a137ecd0600e88 +Subproject commit 756f2a1f88f079e74c34fd8e0f3bb5aa98760bef diff --git a/src/java/org/apache/cassandra/service/accord/AccordCacheEntry.java b/src/java/org/apache/cassandra/service/accord/AccordCacheEntry.java index 6ad5c58b20..58bf4cdf75 100644 --- a/src/java/org/apache/cassandra/service/accord/AccordCacheEntry.java +++ b/src/java/org/apache/cassandra/service/accord/AccordCacheEntry.java @@ -389,6 +389,14 @@ public class AccordCacheEntry extends IntrusiveLinkedListNode return (V)unwrap(); } + public V tryGetExclusive() + { + Invariants.require(owner == null || owner.commandStore == null || owner.commandStore.executor().isOwningThread()); + if (!isLoaded() || isShrunk()) + return null; + return (V)unwrap(); + } + private Object unwrap() { return isNested() ? ((Nested)state).state : state; diff --git a/src/java/org/apache/cassandra/service/accord/AccordTask.java b/src/java/org/apache/cassandra/service/accord/AccordTask.java index 8a7c4986a1..f037ab8df1 100644 --- a/src/java/org/apache/cassandra/service/accord/AccordTask.java +++ b/src/java/org/apache/cassandra/service/accord/AccordTask.java @@ -42,6 +42,7 @@ import accord.api.Journal; import accord.api.RoutingKey; import accord.local.Command; import accord.local.CommandStore; +import accord.local.MaxDecidedRX; import accord.local.PreLoadContext; import accord.local.SafeCommandStore; import accord.local.cfk.CommandsForKey; @@ -1091,8 +1092,9 @@ public abstract class AccordTask extends SubmittableTask implements Runnable, void startInternal(Caches caches) { + MaxDecidedRX maxDecidedRX = commandStore.unsafeGetMaxDecidedRX(); summaryLoader = commandStore.commandsForRanges().loader(preLoadContext.primaryTxnId(), preLoadContext.keyHistory(), keysOrRanges); - summaryLoader.forEachInCache(summary -> summaries.put(summary.txnId, summary), caches); + summaryLoader.forEachInCache(keysOrRanges, summary -> summaries.put(summary.txnId, summary), caches); caches.commands().register(commandWatcher); } diff --git a/src/java/org/apache/cassandra/service/accord/CommandsForRanges.java b/src/java/org/apache/cassandra/service/accord/CommandsForRanges.java index 470822906f..f484e9dc9d 100644 --- a/src/java/org/apache/cassandra/service/accord/CommandsForRanges.java +++ b/src/java/org/apache/cassandra/service/accord/CommandsForRanges.java @@ -18,6 +18,7 @@ package org.apache.cassandra.service.accord; +import java.util.Comparator; import java.util.Iterator; import java.util.Map; import java.util.NavigableMap; @@ -28,27 +29,121 @@ import java.util.function.Consumer; import java.util.function.UnaryOperator; import javax.annotation.Nullable; +import accord.api.RoutingKey; import accord.local.Command; import accord.local.CommandSummaries; import accord.local.CommandSummaries.Summary; import accord.local.KeyHistory; +import accord.local.MaxDecidedRX; import accord.local.RedundantBefore; +import accord.primitives.AbstractRanges; +import accord.primitives.AbstractUnseekableKeys; +import accord.primitives.Range; +import accord.primitives.RangeRoute; import accord.primitives.Ranges; import accord.primitives.Routable; import accord.primitives.Timestamp; +import accord.primitives.Txn; import accord.primitives.Txn.Kind.Kinds; import accord.primitives.TxnId; import accord.primitives.Unseekable; import accord.primitives.Unseekables; import accord.utils.Invariants; -import org.agrona.collections.ObjectHashSet; +import accord.utils.UnhandledEnum; +import org.agrona.collections.Object2ObjectHashMap; import org.apache.cassandra.service.accord.api.TokenKey; +import org.apache.cassandra.utils.btree.IntervalBTree; import static accord.local.CommandSummaries.SummaryStatus.NOT_DIRECTLY_WITNESSED; +import static org.apache.cassandra.utils.btree.IntervalBTree.InclusiveEndKeyComparatorHelper.intervalEndWithKeyEnd; +import static org.apache.cassandra.utils.btree.IntervalBTree.InclusiveEndKeyComparatorHelper.intervalEndWithKeyStart; +import static org.apache.cassandra.utils.btree.IntervalBTree.InclusiveEndKeyComparatorHelper.intervalStartWithKeyEnd; +import static org.apache.cassandra.utils.btree.IntervalBTree.InclusiveEndKeyComparatorHelper.intervalStartWithKeyStart; +import static org.apache.cassandra.utils.btree.IntervalBTree.InclusiveEndKeyComparatorHelper.keyEndWithIntervalEnd; +import static org.apache.cassandra.utils.btree.IntervalBTree.InclusiveEndKeyComparatorHelper.keyEndWithIntervalStart; +import static org.apache.cassandra.utils.btree.IntervalBTree.InclusiveEndKeyComparatorHelper.keyStartWithIntervalEnd; +import static org.apache.cassandra.utils.btree.IntervalBTree.InclusiveEndKeyComparatorHelper.keyStartWithIntervalStart; // TODO (expected): move to accord-core, merge with existing logic there public class CommandsForRanges extends TreeMap implements CommandSummaries.ByTxnIdSnapshot { + static final IntervalComparators COMPARATORS = new IntervalComparators(); + static final IntervalKeyComparators KEY_COMPARATORS = new IntervalKeyComparators(); + static class TxnIdInterval implements Comparable + { + final RoutingKey start, end; + final TxnId txnId; + + TxnIdInterval(RoutingKey start, RoutingKey end, TxnId txnId) + { + this.start = start; + this.end = end; + this.txnId = txnId; + } + + TxnIdInterval(Range range, TxnId txnId) + { + this.start = range.start(); + this.end = range.end(); + this.txnId = txnId; + } + + @Override + public int compareTo(TxnIdInterval that) + { + int c = this.start.compareTo(that.start); + if (c == 0) c = this.end.compareTo(that.end); + if (c == 0) c = this.txnId.compareTo(that.txnId); + return c; + } + } + + static class IntervalComparators implements IntervalBTree.IntervalComparators + { + @Override public Comparator totalOrder() { return TxnIdInterval::compareTo; } + @Override public Comparator startWithStartComparator() { return (a, b) -> a.start.compareTo(b.start); } + @Override public Comparator startWithEndComparator() { return (a, b) -> a.start.compareTo(b.end); } + @Override public Comparator endWithStartComparator() { return (a, b) -> a.end.compareTo(b.start); } + @Override public Comparator endWithEndComparator() { return (a, b) -> a.end.compareTo(b.end); } + } + + static class IntervalKeyComparators implements IntervalBTree.IntervalComparators + { + @Override public Comparator totalOrder() { throw new UnsupportedOperationException(); } + + @Override + public Comparator startWithStartComparator() + { + return (a, b) -> a.getClass() == TxnIdInterval.class + ? intervalStartWithKeyStart(((TxnIdInterval) a).start.compareTo((RoutingKey)b)) + : keyStartWithIntervalStart(((RoutingKey)a).compareTo(((TxnIdInterval)b).start)); + } + + @Override + public Comparator startWithEndComparator() + { + return (a, b) -> a.getClass() == TxnIdInterval.class + ? intervalStartWithKeyEnd(((TxnIdInterval)a).start.compareTo((RoutingKey)b)) + : keyStartWithIntervalEnd(((RoutingKey)a).compareTo(((TxnIdInterval)b).end)); + } + + @Override + public Comparator endWithStartComparator() + { + return (a, b) -> a.getClass() == TxnIdInterval.class + ? intervalEndWithKeyStart(((TxnIdInterval)a).end.compareTo((RoutingKey)b)) + : keyEndWithIntervalStart(((RoutingKey)a).compareTo(((TxnIdInterval)b).start)); + } + + @Override + public Comparator endWithEndComparator() + { + return (a, b) -> a.getClass() == TxnIdInterval.class + ? intervalEndWithKeyEnd(((TxnIdInterval)a).end.compareTo((RoutingKey)b)) + : keyEndWithIntervalEnd(((RoutingKey)a).compareTo(((TxnIdInterval)b).end)); + } + } + public CommandsForRanges(Map m) { super(m); @@ -64,8 +159,10 @@ public class CommandsForRanges extends TreeMap implements Co { private final AccordCommandStore commandStore; private final RangeSearcher searcher; - private AtomicReference> transitive = new AtomicReference<>(new TreeMap<>()); - private final ObjectHashSet cachedRangeTxns = new ObjectHashSet<>(); + private final AtomicReference> transitive = new AtomicReference<>(new TreeMap<>()); + // TODO (desired): manage memory consumed by this auxillary information + private final Object2ObjectHashMap cachedRangeTxnsById = new Object2ObjectHashMap<>(); + private Object[] cachedRangeTxnsByRange = IntervalBTree.empty(); public Manager(AccordCommandStore commandStore) { @@ -78,11 +175,27 @@ public class CommandsForRanges extends TreeMap implements Co } @Override - public void onAdd(AccordCacheEntry state) + public void onUpdate(AccordCacheEntry state) { TxnId txnId = state.key(); if (txnId.is(Routable.Domain.Range)) - cachedRangeTxns.add(txnId); + { + Command cmd = state.tryGetExclusive(); + if (cmd != null) + { + RangeRoute upd = (RangeRoute) cmd.route(); + if (upd != null) + { + RangeRoute cur = cachedRangeTxnsById.put(cmd.txnId(), upd); + if (!upd.equals(cur)) + { + if (cur != null) + remove(txnId, cur); + cachedRangeTxnsByRange = IntervalBTree.update(cachedRangeTxnsByRange, toMap(txnId, upd), COMPARATORS); + } + } + } + } } @Override @@ -90,7 +203,36 @@ public class CommandsForRanges extends TreeMap implements Co { TxnId txnId = state.key(); if (txnId.is(Routable.Domain.Range)) - cachedRangeTxns.remove(txnId); + { + RangeRoute cur = cachedRangeTxnsById.remove(txnId); + if (cur != null) + remove(txnId, cur); + } + } + + private void remove(TxnId txnId, RangeRoute route) + { + cachedRangeTxnsByRange = IntervalBTree.subtract(cachedRangeTxnsByRange, toMap(txnId, route), COMPARATORS); + } + + static Object[] toMap(TxnId txnId, RangeRoute route) + { + int size = route.size(); + switch (size) + { + case 0: return IntervalBTree.empty(); + case 1: return IntervalBTree.singleton(new TxnIdInterval(route.get(0), txnId)); + default: + { + try (IntervalBTree.FastInteralTreeBuilder builder = IntervalBTree.fastBuilder(COMPARATORS.endWithEndComparator())) + { + for (int i = 0 ; i < size ; ++i) + builder.add(new TxnIdInterval(route.get(i), txnId)); + return builder.build(); + } + } + } + } public CommandsForRanges.Loader loader(@Nullable TxnId primaryTxnId, KeyHistory keyHistory, Unseekables keysOrRanges) @@ -99,9 +241,12 @@ public class CommandsForRanges extends TreeMap implements Co return Loader.loader(redundantBefore, primaryTxnId, keyHistory, keysOrRanges, this::newLoader); } - private Loader newLoader(Unseekables searchKeysOrRanges, RedundantBefore redundantBefore, Kinds testKind, TxnId minTxnId, Timestamp maxTxnId, @Nullable TxnId findAsDep) + private Loader newLoader(@Nullable TxnId primaryTxnId, Unseekables searchKeysOrRanges, RedundantBefore redundantBefore, Kinds testKind, TxnId minTxnId, Timestamp maxTxnId, @Nullable TxnId findAsDep) { - return new Loader(this, searchKeysOrRanges, redundantBefore, testKind, minTxnId, maxTxnId, findAsDep); + MaxDecidedRX maxDecidedRX = null; + if (primaryTxnId != null && primaryTxnId.is(Txn.Kind.ExclusiveSyncPoint) && findAsDep == null) + maxDecidedRX = commandStore.unsafeGetMaxDecidedRX(); + return new Loader(this, searchKeysOrRanges, redundantBefore, testKind, minTxnId, maxTxnId, findAsDep, maxDecidedRX); } private void updateTransitive(UnaryOperator> update) @@ -150,15 +295,20 @@ public class CommandsForRanges extends TreeMap implements Co public static class Loader extends Summary.Loader { private final Manager manager; + private final MaxDecidedRX maxDecidedRX; + private final TxnId minRelevantId; - public Loader(Manager manager, Unseekables searchKeysOrRanges, RedundantBefore redundantBefore, Kinds testKinds, TxnId minTxnId, Timestamp maxTxnId, @Nullable TxnId findAsDep) + public Loader(Manager manager, Unseekables searchKeysOrRanges, RedundantBefore redundantBefore, Kinds testKinds, TxnId minTxnId, Timestamp maxTxnId, @Nullable TxnId findAsDep, MaxDecidedRX maxDecidedRX) { - super(searchKeysOrRanges, redundantBefore, testKinds, minTxnId, maxTxnId, findAsDep); + super(null, searchKeysOrRanges, redundantBefore, testKinds, minTxnId, maxTxnId, findAsDep); this.manager = manager; + this.maxDecidedRX = maxDecidedRX; + this.minRelevantId = maxDecidedRX == null ? null : TxnId.nonNullOrMax(TxnId.NONE, maxDecidedRX.foldl(searchKeysOrRanges, TxnId::nonNullOrMin, null)); } public void intersects(Consumer forEach) { + // TODO (expected): use the ranges we find to filter results by MaxDecidedRX (don't just consume the TxnId) switch (searchKeysOrRanges.domain()) { case Range: @@ -173,27 +323,76 @@ public class CommandsForRanges extends TreeMap implements Co NavigableMap transitive = manager.transitive.get(); if (!transitive.isEmpty()) { - for (Map.Entry e : transitive.tailMap(minTxnId, true).entrySet()) - { - if (e.getValue().intersects(searchKeysOrRanges)) - forEach.accept(e.getKey()); - } + for (Map.Entry e : transitive.tailMap(minTxnId, true).entrySet()) + { + if (e.getValue().intersects(searchKeysOrRanges)) + forEach.accept(e.getKey()); + } } } - public void forEachInCache(Consumer forEach, AccordCommandStore.Caches caches) + boolean isRelevant(TxnIdInterval txnIdInterval) { - for (TxnId txnId : manager.cachedRangeTxns) + if (maxDecidedRX == null) + return true; + if (txnIdInterval.txnId.compareTo(minRelevantId) < 0) + return false; + return maxDecidedRX.foldl(txnIdInterval.start, txnIdInterval.end, (decided, anyUndecided, test, ignore) -> test.compareTo(decided) >= 0, false, txnIdInterval.txnId, null); + } + + boolean isMaybeRelevant(TxnId txnId) + { + return maxDecidedRX == null || txnId.compareTo(minRelevantId) >= 0; + } + + public void forEachInCache(Unseekables keysOrRanges, Consumer forEach, AccordCommandStore.Caches caches) + { + switch (keysOrRanges.domain()) { - AccordCacheEntry state = caches.commands().getUnsafe(txnId); - Summary summary = ifRelevant(state); - if (summary != null) - forEach.accept(summary); + default: throw new UnhandledEnum(keysOrRanges.domain()); + case Key: + { + for (RoutingKey key : (AbstractUnseekableKeys)keysOrRanges) + { + IntervalBTree.accumulate(manager.cachedRangeTxnsByRange, KEY_COMPARATORS, key, (f, s, i, c) -> { + TxnIdInterval interval = (TxnIdInterval)i; + if (isRelevant(interval)) + { + TxnId txnId = ((TxnIdInterval)i).txnId; + Summary summary = ifRelevant(c.getUnsafe(txnId)); + if (summary != null) + f.accept(summary); + } + return c; + }, forEach, this, caches.commands()); + } + break; + } + case Range: + { + for (Range range : (AbstractRanges)keysOrRanges) + { + IntervalBTree.accumulate(manager.cachedRangeTxnsByRange, COMPARATORS, new TxnIdInterval(range.start(), range.end(), TxnId.NONE), (f, s, i, c) -> { + if (isRelevant(i)) + { + TxnId txnId = i.txnId; + Summary summary = ifRelevant(c.getUnsafe(txnId)); + if (summary != null) + f.accept(summary); + } + return c; + }, forEach, this, caches.commands()); + } + break; + } } } public Summary load(TxnId txnId) { + if (!isMaybeRelevant(txnId)) + return null; + if (findAsDep == null) { Command.Minimal cmd = manager.commandStore.loadMinimal(txnId); diff --git a/src/java/org/apache/cassandra/service/accord/interop/AccordInteropApply.java b/src/java/org/apache/cassandra/service/accord/interop/AccordInteropApply.java index e5f6ba4857..717f9232d6 100644 --- a/src/java/org/apache/cassandra/service/accord/interop/AccordInteropApply.java +++ b/src/java/org/apache/cassandra/service/accord/interop/AccordInteropApply.java @@ -208,12 +208,6 @@ public class AccordInteropApply extends Apply implements LocalListeners.ComplexL listeners.forEach((i, v) -> v.cancel()); } - @Override - public TxnId primaryTxnId() - { - return txnId; - } - @Override public Unseekables keys() { diff --git a/src/java/org/apache/cassandra/utils/PeekingSearchIterator.java b/src/java/org/apache/cassandra/utils/PeekingSearchIterator.java new file mode 100644 index 0000000000..52bc137fed --- /dev/null +++ b/src/java/org/apache/cassandra/utils/PeekingSearchIterator.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.cassandra.utils; + +public interface PeekingSearchIterator extends SearchIterator +{ + /** + * @return true if iterator has any elements left, false otherwise + */ + boolean hasNext(); + + V next(); + + V peek(); +} diff --git a/src/java/org/apache/cassandra/utils/btree/BTree.java b/src/java/org/apache/cassandra/utils/btree/BTree.java index ad01bb2c1c..701cfeee2e 100644 --- a/src/java/org/apache/cassandra/utils/btree/BTree.java +++ b/src/java/org/apache/cassandra/utils/btree/BTree.java @@ -32,8 +32,12 @@ import com.google.common.collect.Ordering; import accord.utils.Invariants; import org.apache.cassandra.utils.BiLongAccumulator; import org.apache.cassandra.utils.BulkIterator; +import org.apache.cassandra.utils.IndexedSearchIterator; import org.apache.cassandra.utils.LongAccumulator; import org.apache.cassandra.utils.ObjectSizes; +import org.apache.cassandra.utils.PeekingSearchIterator; +import org.apache.cassandra.utils.SearchIterator; +import org.apache.cassandra.utils.btree.IntervalBTree.IntervalMaxIndex; import org.apache.cassandra.utils.caching.TinyThreadLocalPool; import static java.lang.Math.max; @@ -66,7 +70,7 @@ public class BTree */ public static final int BRANCH_SHIFT = BTREE_BRANCH_SHIFT.getInt(); - private static final int BRANCH_FACTOR = 1 << BRANCH_SHIFT; + static final int BRANCH_FACTOR = 1 << BRANCH_SHIFT; public static final int MIN_KEYS = BRANCH_FACTOR / 2 - 1; public static final int MAX_KEYS = BRANCH_FACTOR - 1; public static final long STOP_SENTINEL_VALUE = Long.MAX_VALUE; @@ -559,6 +563,7 @@ public class BTree for (int i = keyCount; i <= keyCount * 2; ++i) reverseInSitu((Object[]) tree[i], height - 1, copySizeMaps); + // TODO (expected): support IntervalMaxIndex int[] sizeMap = (int[]) tree[2 * keyCount + 1]; if (sizeMap != DENSE_SIZE_MAPS[height - 2]) // no need to reverse a dense map; same in both directions { @@ -701,7 +706,7 @@ public class BTree while (!isLeaf(tree)) { - final int[] sizeMap = getSizeMap(tree); + final int[] sizeMap = sizeMap(tree); int boundary = Arrays.binarySearch(sizeMap, index); if (boundary >= 0) { @@ -769,7 +774,7 @@ public class BTree if (!exact) i = -1 - i; - int[] sizeMap = getSizeMap(node); + int[] sizeMap = sizeMap(node); if (exact) return lb + sizeMap[i]; else if (i > 0) @@ -798,7 +803,7 @@ public class BTree return (V) node[index]; } - int[] sizeMap = getSizeMap(node); + int[] sizeMap = sizeMap(node); int boundary = Arrays.binarySearch(sizeMap, index); if (boundary >= 0) { @@ -931,20 +936,12 @@ public class BTree return branchNode.length / 2; } - /** - * @return the size map for the branch node - */ - static int[] getSizeMap(Object[] branchNode) - { - return (int[]) branchNode[getChildEnd(branchNode)]; - } - /** * @return the size map for the branch node */ static int lookupSizeMap(Object[] branchNode, int index) { - return getSizeMap(branchNode)[index]; + return sizeMap(branchNode)[index]; } // get the size from the btree's index (fails if not present) @@ -956,7 +953,7 @@ public class BTree // length - 1 == getChildEnd == getPositionOfSizeMap // (length / 2) - 1 == getChildCount - 1 == position of full tree size // hard code this, as will be used often; - return ((int[]) tree[length - 1])[(length / 2) - 1]; + return sizeMap(tree)[(length / 2) - 1]; } public static long sizeOfStructureOnHeap(Object[] tree) @@ -1378,7 +1375,7 @@ public class BTree appendBranchOrLeaf(builder, (Object[]) branch[i]); } // add sizeMap - builder.append(", ").append(Arrays.toString((int[]) branch[branch.length - 1])); + builder.append(", ").append(Arrays.toString(sizeMap(branch))); return builder; } @@ -1400,7 +1397,7 @@ public class BTree { if (isLeaf(root)) return keyIndex; - int[] sizeMap = getSizeMap(root); + int[] sizeMap = sizeMap(root); if ((keyIndex >= 0) & (keyIndex < sizeMap.length)) return sizeMap[keyIndex]; // we support asking for -1 or size, so that we can easily use this for iterator bounds checking @@ -2191,7 +2188,7 @@ public class BTree // length - 1 == getChildEnd == getPositionOfSizeMap // (length / 2) - 1 == getChildCount - 1 == position of full tree size // hard code this, as will be used often; - return ((int[]) branch[length - 1])[(length / 2) - 1]; + return sizeMap(branch)[(length / 2) - 1]; } /** @@ -2207,7 +2204,11 @@ public class BTree */ static int[] sizeMap(Object[] branch) { - return (int[]) branch[branch.length - 1]; + Object map = branch[branch.length - 1]; + if (map.getClass() == int[].class) + return (int[])map; + + return ((IntervalMaxIndex)map).sizeMap; } public static long sizeOnHeapOf(Object[] tree) @@ -2334,7 +2335,7 @@ public class BTree * Base class for AbstractFastBuilder.BranchBuilder, LeafBuilder and AbstractFastBuilder, * containing shared behaviour and declaring some useful abstract methods. */ - private static abstract class LeafOrBranchBuilder + static abstract class LeafOrBranchBuilder { final int height; final LeafOrBranchBuilder child; @@ -2471,12 +2472,21 @@ public class BTree */ final BranchBuilder ensureParent() { - if (parent == null) - parent = new BranchBuilder(this); - parent.inUse = true; + if (parent == null) parent = allocateParent(); + else if (!parent.inUse) initParent(); return parent; } + BranchBuilder allocateParent() + { + return new BranchBuilder(this); + } + + void initParent() + { + parent.inUse = true; + } + /** * Mark a branch builder as utilised, so that we must clear it when resetting any {@link AbstractFastBuilder} * @@ -2959,7 +2969,8 @@ public class BTree // assumes sizes != null, since only makes sense to use this method in that context int predKeys = shallowSizeOfBranch(pred); - int[] sizeMap = (int[]) pred[2 * predKeys + 1]; + // TODO (desired): handle/copy IntervalMaxIndex + int[] sizeMap = sizeMap(pred); int newKeys = 1 + predKeys; if (newKeys + count <= MAX_KEYS) { @@ -3252,6 +3263,13 @@ public class BTree for (int i = 0; i < count; ++i) out[outOffset + i] = in[inOffset + i] - (1 + in[inOffset + i - 1]); } + + void reset() + { + Arrays.fill(buffer, null); + count = 0; + inUse = false; + } } /** @@ -3293,7 +3311,7 @@ public class BTree public static class FastBuilder extends AbstractFastBuilder implements AutoCloseable { private static final TinyThreadLocalPool> POOL = new TinyThreadLocalPool<>(); - private TinyThreadLocalPool.TinyPool> pool; + TinyThreadLocalPool.TinyPool pool; FastBuilder() { @@ -3341,9 +3359,7 @@ public class BTree BranchBuilder branch = leaf().parent; while (branch != null && branch.inUse) { - Arrays.fill(branch.buffer, null); - branch.count = 0; - branch.inUse = false; + branch.reset(); branch = branch.parent; } } @@ -3442,10 +3458,10 @@ public class BTree * Searches within both trees to accelerate the process of modification, instead of performing a simple * iteration over the new tree. */ - private static class Updater extends AbstractUpdater implements AutoCloseable + static class Updater extends AbstractUpdater implements AutoCloseable { static final TinyThreadLocalPool POOL = new TinyThreadLocalPool<>(); - TinyThreadLocalPool.TinyPool pool; + TinyThreadLocalPool.TinyPool pool; // the new tree we navigate linearly, and are always on a key or at the end final SimpleTreeKeysIterator insert = new SimpleTreeKeysIterator<>(); @@ -3453,7 +3469,6 @@ public class BTree Comparator comparator; UpdateFunction updateF; - static Updater get() { TinyThreadLocalPool.TinyPool pool = POOL.get(); @@ -3626,6 +3641,7 @@ public class BTree reset(); pool.offer(this); pool = null; + comparator = null; } void reset() @@ -3655,7 +3671,7 @@ public class BTree *

* The approach taken here hopefully balances simplicity, garbage generation and execution time. */ - private static abstract class AbstractTransformer extends AbstractUpdater implements AutoCloseable + static abstract class AbstractTransformer extends AbstractUpdater implements AutoCloseable { /** * An iterator over the tree we are updating @@ -3840,7 +3856,7 @@ public class BTree * we refuse to construct a leaf and return null. Otherwise we propagate the branch to its parent's buffer * and return the branch we have constructed. */ - private boolean finish(LeafOrBranchBuilder level, Object[] unode) + final boolean finish(LeafOrBranchBuilder level, Object[] unode) { if (!level.isSufficient()) return false; @@ -3857,7 +3873,7 @@ public class BTree * does not, we recursively apply the stealing procedure to obtain a non-empty parent. If this process manages * to reach the root and still find no preceding branch, this will result in making this branch the new root. */ - private Object[] finishAndDrain(boolean skipLeaf) + final Object[] finishAndDrain(boolean skipLeaf) { LeafOrBranchBuilder level = leaf(); if (skipLeaf) @@ -3974,6 +3990,191 @@ public class BTree } } + /** + * Implement set subtraction/difference using a modified version of the Transformer logic + */ + static abstract class Subtraction extends AbstractTransformer implements AutoCloseable + { + /** + * An iterator over the tree we are updating + */ + PeekingSearchIterator remove; + Comparator comparator; + + Object[] apply(Object[] update, PeekingSearchIterator remove) + { + int height = this.update.init(update); + this.remove = remove; + if (queuedToFinish.length < height - 1) + queuedToFinish = new Object[height - 1][]; + return apply(); + } + + @Override + T apply(T v) + { + throw new UnsupportedOperationException(); + } + + /** + * We base our operation on the shape of {@code update}, trying to steal as much of the original tree as + * possible for our new tree + */ + private Object[] apply() + { + Object[] unode = update.node(); + int upos = update.position(), usz = sizeOfLeaf(unode); + while (true) + { + // we always start the loop on a leaf node, for both input and output + boolean propagatedOriginalLeaf = false; + if (leaf().count == 0 && upos == 0) + { + int prev = 0; + while (upos < usz) + { + // fast path - buffer is empty and input unconsumed, so may be able to propagate original + if (null == remove.next((T)unode[upos])) + { + if (!remove.hasNext()) + break; + upos = exponentialSearch(comparator, unode, upos + 1, usz, remove.peek()); + if (upos < 0) + { + upos = -1 - upos; + continue; + } + } + + leaf().copyNoOverflow(unode, prev, upos - prev); + ++upos; + prev = upos; + } + if (propagatedOriginalLeaf = (prev == 0)) + { + // if input is unmodified by transformation, propagate the input node + markUsed(parent).addChild(unode, usz); + } + else + { + leaf().copyNoOverflow(unode, prev, usz - prev); + } + } + else + { + int prev = upos; + while (upos < usz) + { + if (null == remove.next((T)unode[upos])) + { + if (!remove.hasNext()) + break; + upos = exponentialSearch(comparator, unode, upos + 1, usz, remove.peek()); + if (upos < 0) + { + upos = -1 - upos; + continue; + } + } + + leaf().copy(unode, prev, upos - prev); + ++upos; + prev = upos; + } + leaf().copy(unode, prev, usz - prev); + } + + // we've finished a leaf, and have to hand it to a parent alongside its right-hand key + // so now we try to do two things: + // 1) find the next unfiltered key from our unfinished parent + // 2) determine how many parents are "finished" and whose buffers we should also attempt to propagate + // we do (1) unconditionally, because: + // a) we need to handle the branch keys somewhere, and it may as well happen in one place + // b) we either need more keys for our incomplete leaf; or + // c) we need a key to go after our last propagated node in any unfinished parent + + int finishToHeight = 0; + T next; + do + { + if (!update.ascendToParent()) + return finishAndDrain(propagatedOriginalLeaf); + + BranchBuilder level = parent; + unode = update.node(); + upos = update.position(); + usz = shallowSizeOfBranch(unode); + + while (upos == usz) + { + queuedToFinish[level.height - 2] = unode; + finishToHeight = max(finishToHeight, level.height); + + if (!update.ascendToParent()) + return finishAndDrain(propagatedOriginalLeaf); + + level = level.ensureParent(); + unode = update.node(); + upos = update.position(); + usz = shallowSizeOfBranch(unode); + } + + next = (T) unode[upos]; + if (null != remove.next(next)) + next = null; + + update.descendIntoNextLeaf(unode, upos, usz); + unode = update.node(); + upos = update.position(); + usz = sizeOfLeaf(unode); + + // nextKey might have been filtered, so we may need to look in this next leaf for it + while (next == null && upos < usz) + { + next = (T)unode[upos++]; + if (null != remove.next(next)) + next = null; + } + + // if we still found no key loop and try again on the next parent, leaf, parent... ad infinitum + } while (next == null); + + // we always end with unode a leaf, though it may be that upos == usz and that we will do nothing with it + + // we've found a non-null key, now decide what to do with it: + // 1) if we have insufficient keys in our leaf, simply append to the leaf and continue; + // 2) otherwise, walk our parent branches finishing those *before* {@code finishTo} + // 2a) if any cannot be finished, append our new key to it and stop finishing further parents; they + // will be finished the next time we ascend to their level with a complete chain of finishable branches + // 2b) otherwise, add our new key to {@code finishTo} + + if (!propagatedOriginalLeaf && !finish(leaf(), null)) + { + leaf().addKeyNoOverflow(next); + continue; + } + + BranchBuilder finish = parent; + while (true) + { + if (finish.height <= finishToHeight) + { + Object[] originalNode = queuedToFinish[finish.height - 2]; + if (finish(finish, originalNode)) + { + finish = finish.parent; + continue; + } + } + + // add our key to the last unpropagated parent branch buffer + finish.addKey(next); + break; + } + } + } + } + private static class Transformer extends AbstractTransformer { @@ -4131,7 +4332,6 @@ public class BTree } } - private static class SimpleTreeKeysIterator { int leafSize; diff --git a/src/java/org/apache/cassandra/utils/btree/BTreeRemoval.java b/src/java/org/apache/cassandra/utils/btree/BTreeRemoval.java index b887298b0b..d79e3a42d0 100644 --- a/src/java/org/apache/cassandra/utils/btree/BTreeRemoval.java +++ b/src/java/org/apache/cassandra/utils/btree/BTreeRemoval.java @@ -21,6 +21,8 @@ package org.apache.cassandra.utils.btree; import java.util.Arrays; import java.util.Comparator; +import static org.apache.cassandra.utils.btree.BTree.sizeMap; + public class BTreeRemoval { /** @@ -45,7 +47,7 @@ public class BTreeRemoval index = lb + i; else { - final int indexInNode = BTree.getSizeMap(node)[i]; + final int indexInNode = sizeMap(node)[i]; index = lb + indexInNode - 1; elemToSwap = BTree.findByIndex(node, indexInNode - 1); } @@ -56,7 +58,7 @@ public class BTreeRemoval i = -1 - i; if (i > 0) - lb += BTree.getSizeMap(node)[i - 1] + 1; + lb += sizeMap(node)[i - 1] + 1; node = (Object[]) node[keyEnd + i]; } @@ -80,9 +82,9 @@ public class BTreeRemoval while (!BTree.isLeaf(node)) { final int keyEnd = BTree.getBranchKeyEnd(node); - int i = -1 - Arrays.binarySearch(BTree.getSizeMap(node), index); + int i = -1 - Arrays.binarySearch(sizeMap(node), index); if (i > 0) - index -= (1 + BTree.getSizeMap(node)[i - 1]); + index -= (1 + sizeMap(node)[i - 1]); Object[] nextNode = (Object[]) node[keyEnd + i]; boolean nextNodeNeedsCopy = true; if (BTree.getKeyEnd(nextNode) > BTree.MIN_KEYS) @@ -124,7 +126,7 @@ public class BTreeRemoval if (node != null) { - final int[] sizeMap = BTree.getSizeMap(node); + final int[] sizeMap = sizeMap(node); for (int j = i; j < sizeMap.length; ++j) sizeMap[j] -= 1; if (prevNode != null) @@ -160,7 +162,7 @@ public class BTreeRemoval copyWithKeyAndChildInserted(nextNode, nextKeyEnd, node[i], BTree.getChildCount(nextNode), newChild); node[i] = rightNeighbour[0]; node[keyEnd + i + 1] = copyWithKeyAndChildRemoved(rightNeighbour, 0, 0, true); - BTree.getSizeMap(node)[i] += + sizeMap(node)[i] += leaves ? 1 : 1 + BTree.size((Object[]) newNextNode[BTree.getChildEnd(newNextNode) - 1]); return newNextNode; } @@ -176,7 +178,7 @@ public class BTreeRemoval final Object[] newNextNode = copyWithKeyAndChildInserted(nextNode, 0, node[i - 1], 0, newChild); node[i - 1] = leftNeighbour[leftNeighbourEndKey - 1]; node[keyEnd + i - 1] = copyWithKeyAndChildRemoved(leftNeighbour, leftNeighbourEndKey - 1, leftNeighbourEndKey, true); - BTree.getSizeMap(node)[i - 1] -= leaves ? 1 : 1 + BTree.getSizeMap(newNextNode)[0]; + sizeMap(node)[i - 1] -= leaves ? 1 : 1 + sizeMap(newNextNode)[0]; return newNextNode; } @@ -211,7 +213,7 @@ public class BTreeRemoval copy, keyEnd + childIndex + 2, keyEnd - childIndex + 1); - final int[] sizeMap = BTree.getSizeMap(node); + final int[] sizeMap = sizeMap(node); final int[] newSizeMap = new int[sizeMap.length + 1]; if (childIndex > 0) System.arraycopy(sizeMap, 0, newSizeMap, 0, childIndex); @@ -241,7 +243,7 @@ public class BTreeRemoval if (!leaf) { offset = copyChildren(node, newNode, offset, childIndex); - final int[] nodeSizeMap = BTree.getSizeMap(node); + final int[] nodeSizeMap = sizeMap(node); final int[] newNodeSizeMap = new int[nodeSizeMap.length - 1]; int pos = 0; final int sizeToRemove = BTree.size((Object[])node[BTree.getChildStart(node) + childIndex]) + 1; @@ -272,8 +274,8 @@ public class BTreeRemoval { offset = copyChildren(left, result, offset); offset = copyChildren(right, result, offset); - final int[] leftSizeMap = BTree.getSizeMap(left); - final int[] rightSizeMap = BTree.getSizeMap(right); + final int[] leftSizeMap = sizeMap(left); + final int[] rightSizeMap = sizeMap(right); final int[] newSizeMap = new int[leftSizeMap.length + rightSizeMap.length]; offset = 0; offset = copySizeMap(leftSizeMap, newSizeMap, offset, 0); @@ -335,7 +337,7 @@ public class BTreeRemoval System.arraycopy(node, 0, copy, 0, node.length); if (!BTree.isLeaf(node)) { - final int[] sizeMap = BTree.getSizeMap(node); + final int[] sizeMap = sizeMap(node); final int[] copySizeMap = new int[sizeMap.length]; System.arraycopy(sizeMap, 0, copySizeMap, 0, sizeMap.length); copy[copy.length - 1] = copySizeMap; diff --git a/src/java/org/apache/cassandra/utils/btree/BTreeSearchIterator.java b/src/java/org/apache/cassandra/utils/btree/BTreeSearchIterator.java index 2ad7f40148..900033dda4 100644 --- a/src/java/org/apache/cassandra/utils/btree/BTreeSearchIterator.java +++ b/src/java/org/apache/cassandra/utils/btree/BTreeSearchIterator.java @@ -21,9 +21,10 @@ package org.apache.cassandra.utils.btree; import java.util.Iterator; import org.apache.cassandra.utils.IndexedSearchIterator; +import org.apache.cassandra.utils.PeekingSearchIterator; -public interface BTreeSearchIterator extends IndexedSearchIterator, Iterator +public interface BTreeSearchIterator extends IndexedSearchIterator, Iterator, PeekingSearchIterator { /** * Reset this Iterator to its starting position diff --git a/src/java/org/apache/cassandra/utils/btree/FullBTreeSearchIterator.java b/src/java/org/apache/cassandra/utils/btree/FullBTreeSearchIterator.java index c19d4477b1..7c2fd628c0 100644 --- a/src/java/org/apache/cassandra/utils/btree/FullBTreeSearchIterator.java +++ b/src/java/org/apache/cassandra/utils/btree/FullBTreeSearchIterator.java @@ -79,8 +79,7 @@ public class FullBTreeSearchIterator extends TreeCursor implements BTre state = END; break; case BEFORE_FIRST: - seekTo(index = forwards ? lowerBound : upperBound); - state = (byte) (upperBound == lowerBound ? LAST : MIDDLE); + moveStart(); case LAST: case MIDDLE: state |= ON_ITEM; @@ -92,6 +91,29 @@ public class FullBTreeSearchIterator extends TreeCursor implements BTre return current(); } + private void moveStart() + { + seekTo(index = forwards ? lowerBound : upperBound); + state = (byte) (upperBound == lowerBound ? LAST : MIDDLE); + } + + @Override + public V peek() + { + if (state == END) + throw new NoSuchElementException(); + + if (state == BEFORE_FIRST) + moveStart(); + + if ((state & ON_ITEM) == 1) + { + state &= ~ON_ITEM; + index = moveOne(forwards); + } + return (V) currentValue(); + } + public V next(K target) { if (!hasNext()) diff --git a/src/java/org/apache/cassandra/utils/btree/IntervalBTree.java b/src/java/org/apache/cassandra/utils/btree/IntervalBTree.java new file mode 100644 index 0000000000..f04cf08917 --- /dev/null +++ b/src/java/org/apache/cassandra/utils/btree/IntervalBTree.java @@ -0,0 +1,502 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.cassandra.utils.btree; + +import java.util.Arrays; +import java.util.Comparator; + +import accord.utils.Invariants; +import accord.utils.QuadFunction; +import io.netty.util.concurrent.FastThreadLocal; +import org.apache.cassandra.utils.caching.TinyThreadLocalPool; + +import static org.apache.cassandra.utils.btree.BTree.BRANCH_FACTOR; +import static org.apache.cassandra.utils.btree.BTree.Dir.ASC; +import static org.apache.cassandra.utils.btree.BTree.getBranchKeyEnd; +import static org.apache.cassandra.utils.btree.BTree.getChildCount; +import static org.apache.cassandra.utils.btree.BTree.getChildStart; +import static org.apache.cassandra.utils.btree.BTree.getKeyEnd; +import static org.apache.cassandra.utils.btree.BTree.getLeafKeyEnd; +import static org.apache.cassandra.utils.btree.BTree.isEmpty; +import static org.apache.cassandra.utils.btree.BTree.isLeaf; +import static org.apache.cassandra.utils.btree.BTree.size; +import static org.apache.cassandra.utils.btree.BTree.sizeMap; +import static org.apache.cassandra.utils.btree.BTree.slice; + +/** + * A very simple extension to BTree to provide an Augmented Interval BTree. + * + * TODO (desired): there are a number of obvious performance improvements that should be pursued at the earliest suitable opportunity: + * - IntervalMaxIndex should include an index over any direct child leaves to bound worst case to O(lgN + m) rather than O(lgN + m.B) + * - IntervalMaxIndex should include the immediately preceding self-key in any max key, so we can avoid the O(N) loop and simply pre-visit the self-key + * - IntervalMaxIndex should omit entirely keys/nodes that do not extend past the following key + * - Updater/Subtraction should re-use parts of IntervalMaxIndex where appropriate + */ +public class IntervalBTree +{ + public interface IntervalComparators + { + Comparator totalOrder(); + Comparator startWithStartComparator(); + Comparator startWithEndComparator(); + Comparator endWithStartComparator(); + Comparator endWithEndComparator(); + } + + public static class InclusiveEndKeyComparatorHelper + { + public static int keyStartWithIntervalStart(int c) { return equalsMeansBefore(c); } + public static int intervalStartWithKeyStart(int c) { return equalsMeansAfter(c); } + public static int keyStartWithIntervalEnd(int c) { return equalsMeansBefore(c); } + public static int intervalStartWithKeyEnd(int c) { return equalsMeansAfter(c); } + public static int keyEndWithIntervalStart(int c) { return equalsMeansAfter(c); } + public static int intervalEndWithKeyStart(int c) { return equalsMeansAfter(c); } + public static int intervalEndWithKeyEnd(int c) { return equalsMeansAfter(c); } + public static int keyEndWithIntervalEnd(int c) { return equalsMeansBefore(c); } + private static int equalsMeansAfter(int c) { return c == 0 ? 1 : c; } + private static int equalsMeansBefore(int c) { return c == 0 ? -1 : c; } + } + + /** + * Apply the accumulation function over all intersecting intervals in the tree + */ + public static V accumulate(Object[] btree, IntervalComparators comparators, R intersects, QuadFunction function, P1 p1, P2 p2, V accumulate) + { + if (isLeaf(btree)) + { + Comparator comparator = comparators.startWithEndComparator(); + int keyEnd = getLeafKeyEnd(btree); + for (int i = 0; i < keyEnd; ++i) + { + R v = (R) btree[i]; + if (comparator.compare(v, intersects) < 0 && comparator.compare(intersects, v) < 0) + accumulate = function.apply(p1, p2, v, accumulate); + } + } + else + { + int startKey = Arrays.binarySearch(btree, 0, getKeyEnd(btree), intersects, (Comparator) comparators.startWithStartComparator()); + int startChild; + if (startKey >= 0) startChild = startKey + 1; + else startChild = (startKey = -1 - startKey); + int endKey = Arrays.binarySearch(btree, startKey, getKeyEnd(btree), intersects, (Comparator) comparators.startWithEndComparator()); + int endChild; + if (endKey >= 0) endChild = 1 + endKey; + else endChild = 1 + (endKey = -1 - endKey); + + { // descend anything with a max that overlaps us that we won't already visit + if (startChild > 0 || startKey > 0) + accumulate = accumulateMaxOnly(startChild + (startChild == endChild ? 1 : 0), startKey, btree, comparators, intersects, function, p1, p2, accumulate); + } + + int childOffset = getChildStart(btree); + if (startChild == startKey && startChild < endChild) + accumulate = accumulate((Object[]) btree[childOffset + startChild++], comparators, intersects, function, p1, p2, accumulate); + + if (startKey < startChild && startKey < endKey) + accumulate = function.apply(p1, p2, (R) btree[startKey++], accumulate); + + while (startChild < endChild - 1) + { + Invariants.require(startKey == startChild); + accumulate = accumulate((Object[]) btree[childOffset + startChild++], function, p1, p2, accumulate); + accumulate = function.apply(p1, p2, (R) btree[startKey++], accumulate); + } + if (startKey < endKey) + accumulate = function.apply(p1, p2, (R) btree[startKey], accumulate); + if (startChild < endChild) + accumulate = accumulate((Object[]) btree[childOffset + startChild], comparators, intersects, function, p1, p2, accumulate); + } + return accumulate; + } + + public static V accumulate(Object[] btree, QuadFunction function, P1 p1, P2 p2, V accumulate) + { + if (isLeaf(btree)) + { + for (int i = 0, maxi = getLeafKeyEnd(btree); i < maxi; ++i) + accumulate = function.apply(p1, p2, (R) btree[i], accumulate); + } + else + { + int keyEnd = getBranchKeyEnd(btree); + for (int i = 0; i < keyEnd; ++i) + { + accumulate = accumulate((Object[]) btree[keyEnd + i], function, p1, p2, accumulate); + accumulate = function.apply(p1, p2, (R) btree[i], accumulate); + } + accumulate = accumulate((Object[]) btree[2 * keyEnd], function, p1, p2, accumulate); + } + return accumulate; + } + + private static V accumulateMaxOnly(int ifChildBefore, int ifKeyBefore, Object[] btree, IntervalComparators comparators, R intersects, QuadFunction function, P1 p1, P2 p2, V accumulate) + { + if (isLeaf(btree)) + { + Invariants.require(ifChildBefore == Integer.MAX_VALUE); + Comparator comparator = comparators.startWithEndComparator(); + for (int i = 0, maxi = getLeafKeyEnd(btree); i < maxi; ++i) + { + R v = (R) btree[i]; + if (comparator.compare(intersects, v) < 0) + { + Invariants.paranoid(comparator.compare(v, intersects) < 0); + accumulate = function.apply(p1, p2, v, accumulate); + } + } + } + else + { + int keyEnd = getChildStart(btree); + IntervalMaxIndex intervalMaxIndex = getIntervalMaxIndex(btree); + int descendMaxStart = Arrays.binarySearch(intervalMaxIndex.sortedByEnd, intersects, (Comparator) comparators.endWithStartComparator()); + if (descendMaxStart < 0) + descendMaxStart = -1 - descendMaxStart; + int[] sortedByEndIndex = intervalMaxIndex.sortedByEndIndex; + for (int i = descendMaxStart; i < sortedByEndIndex.length; ++i) + { + int index = sortedByEndIndex[i]; + if (index < ifChildBefore) + accumulate = accumulateMaxOnly(Integer.MAX_VALUE, Integer.MAX_VALUE, (Object[]) btree[keyEnd + index], comparators, intersects, function, p1, p2, accumulate); + } + Comparator comparator = comparators.startWithEndComparator(); + for (int i = 0, maxi = Math.min(ifKeyBefore, keyEnd); i < maxi; ++i) + { + R v = (R) btree[i]; + if (comparator.compare(intersects, v) < 0) + { + Invariants.paranoid(comparator.compare(v, intersects) < 0); + accumulate = function.apply(p1, p2, v, accumulate); + } + } + } + return accumulate; + } + + static class IntervalIndexAdapter implements Comparator + { + static class SortEntry + { + Object sort; + int index; + } + + SortEntry[] sort = new SortEntry[BRANCH_FACTOR]; + Comparator endSorter; + + public void override(Object[] branch) + { + int[] sizeMap = sizeMap(branch); + int childStart = getChildStart(branch), childCount = getChildCount(branch); + for (int i = 0; i < childCount; ++i) + { + Object[] child = (Object[]) branch[i + childStart]; + Object max = maxChild(child); + if (sort[i] == null) + sort[i] = new SortEntry(); + sort[i].index = i; + sort[i].sort = max; + } + Arrays.sort(sort, 0, childCount, this); + Object[] sortedByEnd = new Object[childCount]; + int[] sortedByEndIndex = new int[childCount]; + for (int i = 0; i < childCount; ++i) + { + sortedByEnd[i] = sort[i].sort; + sortedByEndIndex[i] = sort[i].index; + } + branch[branch.length - 1] = new IntervalMaxIndex(sortedByEnd, sortedByEndIndex, sizeMap); + } + + private Object maxChild(Object[] child) + { + Object max = child[0]; + for (int j = 1, jend = getKeyEnd(child); j < jend; ++j) + { + if (endSorter.compare(child[j], max) > 0) + max = child[j]; + } + if (!isLeaf(child)) + { + IntervalMaxIndex childIndex = getIntervalMaxIndex(child); + Object maxChild = childIndex.sortedByEnd[childIndex.sortedByEnd.length - 1]; + if (endSorter.compare(maxChild, max) > 0) + max = maxChild; + } + return max; + } + + @Override + public int compare(SortEntry o1, SortEntry o2) + { + return endSorter.compare(o1.sort, o2.sort); + } + } + + static class IntervalBranchBuilder extends BTree.BranchBuilder + { + IntervalIndexAdapter adapter; + + IntervalBranchBuilder(BTree.LeafOrBranchBuilder child) + { + super(child); + } + + @Override + BTree.BranchBuilder allocateParent() + { + return new IntervalBranchBuilder(this).init(adapter); + } + + @Override + void initParent() + { + super.initParent(); + ((IntervalBranchBuilder) parent).init(adapter); + } + + private IntervalBranchBuilder init(IntervalIndexAdapter adapter) + { + this.adapter = adapter; + return this; + } + + @Override + int setDrainSizeMap(Object[] original, int keysInOriginal, Object[] branch, int keysInBranch) + { + int result = super.setDrainSizeMap(original, keysInOriginal, branch, keysInBranch); + adapter.override(branch); + return result; + } + + @Override + void setRedistributedSizeMap(Object[] branch, int steal) + { + super.setRedistributedSizeMap(branch, steal); + adapter.override(branch); + } + + @Override + int setOverflowSizeMap(Object[] branch, int keys) + { + int result = super.setOverflowSizeMap(branch, keys); + adapter.override(branch); + return result; + } + } + + public static class FastInteralTreeBuilder extends BTree.FastBuilder + { + private static final TinyThreadLocalPool> POOL = new TinyThreadLocalPool<>(); + final IntervalIndexAdapter adapter = new IntervalIndexAdapter(); + + @Override + BTree.BranchBuilder allocateParent() + { + return new IntervalBranchBuilder(this).init(adapter); + } + + @Override + void initParent() + { + super.initParent(); + ((IntervalBranchBuilder) parent).init(adapter); + } + } + + static class IntervalUpdater extends BTree.Updater + { + static final TinyThreadLocalPool POOL = new TinyThreadLocalPool<>(); + private final IntervalIndexAdapter adapter = new IntervalIndexAdapter(); + + static IntervalUpdater get(Comparator compareEnds) + { + TinyThreadLocalPool.TinyPool pool = POOL.get(); + IntervalUpdater updater = pool.poll(); + if (updater == null) + updater = new IntervalUpdater<>(); + updater.pool = pool; + updater.adapter.endSorter = compareEnds; + return updater; + } + + @Override + BTree.BranchBuilder allocateParent() + { + return new IntervalBranchBuilder(this).init(adapter); + } + + @Override + void initParent() + { + super.initParent(); + ((IntervalBranchBuilder) parent).init(adapter); + } + + @Override + public void close() + { + adapter.endSorter = null; + } + } + + public static Object[] empty() + { + return BTree.empty(); + } + + public static Object[] singleton(Object value) + { + return BTree.singleton(value); + } + + static class Subtraction extends BTree.Subtraction + { + static final FastThreadLocal SHARED = new FastThreadLocal<>(); + private final IntervalIndexAdapter adapter = new IntervalIndexAdapter(); + + Subtraction() + { + ((IntervalBranchBuilder)parent).adapter = adapter; + } + + static Subtraction get(IntervalComparators comparators) + { + Subtraction subtraction = SHARED.get(); + if (subtraction == null) + SHARED.set(subtraction = new Subtraction()); + subtraction.comparator = comparators.totalOrder(); + subtraction.adapter.endSorter = comparators.endWithEndComparator(); + return subtraction; + } + + @Override + BTree.BranchBuilder allocateParent() + { + return new IntervalBranchBuilder(this).init(adapter); + } + + @Override + void initParent() + { + super.initParent(); + ((IntervalBranchBuilder) parent).init(adapter); + } + + @Override + public void close() + { + reset(); + comparator = null; + adapter.endSorter = null; + } + } + + /** + * Subtracts {@code insert} into {@code update}, applying {@code updateF} to each new item in {@code insert}, + * as well as any matched items in {@code update}. + *

+ * Note that {@code UpdateFunction.noOp} is assumed to indicate a lack of interest in which value survives. + */ + public static Object[] subtract(Object[] toUpdate, Object[] subtract, IntervalComparators comparators) + { + try (Subtraction subtraction = IntervalBTree.Subtraction.get(comparators)) + { + return subtraction.apply(toUpdate, slice(subtract, comparators.totalOrder(), ASC)); + } + } + + /** + * Inserts {@code insert} into {@code update}, applying {@code updateF} to each new item in {@code insert}, + * as well as any matched items in {@code update}. + *

+ * Note that {@code UpdateFunction.noOp} is assumed to indicate a lack of interest in which value survives. + */ + public static Object[] update(Object[] existing, + Object[] insert, + IntervalComparators comparators) + { + // perform some initial obvious optimisations + if (isEmpty(insert)) + return existing; // do nothing if update is empty + + if (isEmpty(existing)) + return insert; + + if (isLeaf(insert)) + { + // consider flipping the order of application, if update is much larger than insert and applying unary no-op + int updateSize = size(existing); + int insertSize = size(insert); + int scale = Integer.numberOfLeadingZeros(updateSize) - Integer.numberOfLeadingZeros(insertSize); + if (scale >= 4) + { + // i.e. at roughly 16x the size, or one tier deeper - very arbitrary, should pick more carefully + // experimentally, at least at 64x the size the difference in performance is ~10x + Object[] tmp = insert; + insert = existing; + existing = tmp; + } + } + + try (IntervalUpdater updater = IntervalUpdater.get(comparators.endWithEndComparator())) + { + return updater.update(existing, insert, comparators.totalOrder(), (UpdateFunction) UpdateFunction.noOp); + } + } + + + /** + * Build a tree of unknown size, in order. + */ + public static FastInteralTreeBuilder fastBuilder(Comparator endSorter) + { + TinyThreadLocalPool.TinyPool> pool = FastInteralTreeBuilder.POOL.get(); + FastInteralTreeBuilder builder = (FastInteralTreeBuilder) pool.poll(); + if (builder == null) + builder = new FastInteralTreeBuilder<>(); + builder.pool = pool; + builder.adapter.endSorter = endSorter; + return builder; + } + + // TODO (desired): index leaf nodes + static class IntervalMaxIndex + { + final Object[] sortedByEnd; + final int[] sortedByEndIndex; + final int[] sizeMap; + + IntervalMaxIndex(Object[] sortedByEnd, int[] sortedByEndIndex, int[] sizeMap) + { + this.sortedByEnd = sortedByEnd; + this.sortedByEndIndex = sortedByEndIndex; + this.sizeMap = sizeMap; + } + } + + /** + * @return the size map for the branch node + */ + static IntervalMaxIndex getIntervalMaxIndex(Object[] branchNode) + { + return (IntervalMaxIndex) branchNode[branchNode.length - 1]; + } +} diff --git a/src/java/org/apache/cassandra/utils/btree/LeafBTreeSearchIterator.java b/src/java/org/apache/cassandra/utils/btree/LeafBTreeSearchIterator.java index 3c1991afd1..a464a15c65 100644 --- a/src/java/org/apache/cassandra/utils/btree/LeafBTreeSearchIterator.java +++ b/src/java/org/apache/cassandra/utils/btree/LeafBTreeSearchIterator.java @@ -65,6 +65,15 @@ public class LeafBTreeSearchIterator implements BTreeSearchIterator return elem; } + @Override + public V peek() + { + if (!hasNext) + throw new NoSuchElementException(); + + return (V) keys[nextPos]; + } + public boolean hasNext() { return hasNext; diff --git a/src/java/org/apache/cassandra/utils/btree/TreeCursor.java b/src/java/org/apache/cassandra/utils/btree/TreeCursor.java index 60c0eb938f..e99663dcc4 100644 --- a/src/java/org/apache/cassandra/utils/btree/TreeCursor.java +++ b/src/java/org/apache/cassandra/utils/btree/TreeCursor.java @@ -234,7 +234,7 @@ class TreeCursor extends NodeCursor return; } - int[] sizeMap = getSizeMap(node); + int[] sizeMap = sizeMap(node); int boundary = Arrays.binarySearch(sizeMap, relativeIndex); if (boundary >= 0) { diff --git a/test/unit/org/apache/cassandra/utils/RangeTreeTest.java b/test/unit/org/apache/cassandra/utils/RangeTreeTest.java index 695894603d..e576d667c2 100644 --- a/test/unit/org/apache/cassandra/utils/RangeTreeTest.java +++ b/test/unit/org/apache/cassandra/utils/RangeTreeTest.java @@ -37,6 +37,7 @@ import org.junit.runners.Parameterized; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import accord.api.RoutingKey; import accord.impl.IntKey; import accord.impl.IntKey.Routing; import accord.primitives.Range; @@ -44,11 +45,21 @@ import accord.utils.Gen; import accord.utils.Gens; import accord.utils.RandomSource; import accord.utils.SearchableRangeList; +import accord.utils.btree.BTree; import org.agrona.collections.IntArrayList; import org.agrona.collections.LongArrayList; +import org.apache.cassandra.utils.btree.IntervalBTree; import org.assertj.core.api.Assertions; import static accord.utils.Property.qt; +import static org.apache.cassandra.utils.btree.IntervalBTree.InclusiveEndKeyComparatorHelper.intervalEndWithKeyEnd; +import static org.apache.cassandra.utils.btree.IntervalBTree.InclusiveEndKeyComparatorHelper.intervalEndWithKeyStart; +import static org.apache.cassandra.utils.btree.IntervalBTree.InclusiveEndKeyComparatorHelper.intervalStartWithKeyEnd; +import static org.apache.cassandra.utils.btree.IntervalBTree.InclusiveEndKeyComparatorHelper.intervalStartWithKeyStart; +import static org.apache.cassandra.utils.btree.IntervalBTree.InclusiveEndKeyComparatorHelper.keyEndWithIntervalEnd; +import static org.apache.cassandra.utils.btree.IntervalBTree.InclusiveEndKeyComparatorHelper.keyEndWithIntervalStart; +import static org.apache.cassandra.utils.btree.IntervalBTree.InclusiveEndKeyComparatorHelper.keyStartWithIntervalEnd; +import static org.apache.cassandra.utils.btree.IntervalBTree.InclusiveEndKeyComparatorHelper.keyStartWithIntervalStart; @RunWith(Parameterized.class) public class RangeTreeTest @@ -157,7 +168,7 @@ public class RangeTreeTest // Having different models makes sure that the tree is flexiable enough and can be used with the semantics the user // needs (with regard to inclusivity). It also adds more confidence that the search logic is correct as different // algorithems help validate this. - private enum ModelType {List, IntervalTree, SearchableRangeList} + private enum ModelType { RTree, SearchableRangeList, IntervalTree, IntervalBTree } private final Pattern pattern; private final ModelType modelType; @@ -189,7 +200,7 @@ public class RangeTreeTest LongArrayList byRangeLength = new LongArrayList(samples * examples, -1); qt().withExamples(examples).check(rs -> { var map = create(modelType); - var model = createModel(modelType); + var model = createOracleForValidating(modelType); Gen rangeGen = rangeGen(rs, pattern, samples); for (int i = 0; i < samples; i++) @@ -199,8 +210,9 @@ public class RangeTreeTest map.put(range, value); model.put(range, value); } + map.done(); model.done(); - Assertions.assertThat(map.actual()).hasSize(samples); +// Assertions.assertThat(map.actual()).hasSize(samples); if (rangeGen instanceof NoOverlap) ((NoOverlap) rangeGen).reset(); Gen.IntGen tokenGe = TOKEN_DISTRIBUTION.next(rs); @@ -356,28 +368,83 @@ public class RangeTreeTest void done(); } - private static RangeTreeModel create(ModelType modelType) + private static Model create(ModelType modelType) { switch (modelType) { - case List: case SearchableRangeList: - return new RangeTreeModel(new RTree<>(COMPARATOR, END_INCLUSIVE)); - case IntervalTree: return new RangeTreeModel(new RTree<>(COMPARATOR, ALL_INCLUSIVE)); + return new SearchableRangeListModel(); + case RTree: + return new RangeTreeModel(); + case IntervalTree: + return new IntervalTreeModel(); + case IntervalBTree: + return new IntervalBTreeModel(); default: throw new AssertionError("Unknown type: " + modelType); } } - private static Model createModel(ModelType modelType) + private static Model createOracleForValidating(ModelType modelType) { - switch (modelType) + if (modelType == ModelType.IntervalTree) + return new ListModel(); + return new IntervalTreeModel(); + } + + static class Entry implements Comparable, Map.Entry + { + final Range range; + final int value; + + Entry(Range range, int value) { - case List: return new ListModel(); - case SearchableRangeList: return new SearchableRangeListModel(); - case IntervalTree: return new IntervalTreeModel(); - default: - throw new AssertionError("Unknown type: " + modelType); + this.range = range; + this.value = value; + } + + @Override + public int compareTo(Entry that) + { + return Integer.compare(this.value, that.value); + } + + @Override + public Range getKey() + { + return range; + } + + @Override + public Integer getValue() + { + return value; + } + + @Override + public Integer setValue(Integer value) + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean equals(Object obj) + { + if (obj instanceof Entry) + return equals((Entry) obj); + if (obj instanceof Map.Entry) + return equals((Map.Entry) obj); + return false; + } + + public boolean equals(Entry that) + { + return this.range.equals(that.range) && this.value == that.value; + } + + public boolean equals(Map.Entry that) + { + return this.range.equals(that.getKey()) && that.getKey().equals(value); } } @@ -385,9 +452,9 @@ public class RangeTreeTest { private final RangeTree tree; - private RangeTreeModel(RangeTree tree) + private RangeTreeModel() { - this.tree = tree; + this.tree = new RTree<>(COMPARATOR, END_INCLUSIVE); } @Override @@ -434,7 +501,7 @@ public class RangeTreeTest @Override public void put(Range range, int value) { - actual.add(Map.entry(range, value)); + actual.add(new Entry(range, value)); } @Override @@ -456,17 +523,16 @@ public class RangeTreeTest @Override public void done() { - } } private static class IntervalTreeModel implements Model { - IntervalTree.Builder> builder = IntervalTree.builder(); - IntervalTree> actual = null; + IntervalTree.Builder> builder = IntervalTree.builder(); + IntervalTree> actual = null; @Override - public IntervalTree> actual() + public IntervalTree> actual() { return actual; } @@ -474,7 +540,11 @@ public class RangeTreeTest @Override public void put(Range range, int value) { - builder.add(new Interval<>((Routing) range.start(), (Routing) range.end(), value)); + // Interval is inclusive/inclusive, Range is exclusive/inclusive + Routing start = (Routing) range.start(); + start = new Routing(start.key + 1); + + builder.add(new Interval<>(start, (Routing) range.end(), new Entry(range, value))); } @Override @@ -486,12 +556,13 @@ public class RangeTreeTest @Override public List> intersects(Range range) { - return map(actual.matches(new Interval<>((Routing) range.start(), (Routing) range.end(), null))); + return map(actual.matches(new Interval<>(new Routing(((Routing) range.start()).key + 1), (Routing) range.end(), null))); } - private static List> map(List> matches) + private static List> map(List> matches) { - return matches.stream().map(i -> Map.entry(IntKey.range(i.min, i.max), i.data)).collect(Collectors.toList()); + return matches.stream().map(v -> v.data) + .collect(Collectors.toList()); } @Override @@ -565,4 +636,138 @@ public class RangeTreeTest list = SearchableRangeList.build(this.ranges = ranges.toArray(Range[]::new)); } } + + private static class IntervalBTreeModel implements Model + { + static class ItemComparators implements IntervalBTree.IntervalComparators + { + static final ItemComparators INSTANCE = new ItemComparators(); + + @Override + public Comparator totalOrder() + { + return (a, b) -> { + int c = a.start.compareTo(b.start); + if (c == 0) c = a.end.compareTo(b.end); + if (c == 0) c = Integer.compare(a.value, b.value); + if (c == 0) c = Integer.compare(a.uniqueId, b.uniqueId); + return c; + }; + } + + @Override + public Comparator startWithStartComparator() + { + return (a, b) -> a.start.compareTo(b.start); + } + + @Override + public Comparator startWithEndComparator() + { + return (a, b) -> a.start.compareTo(b.end); + } + + @Override + public Comparator endWithStartComparator() + { + return (a, b) -> a.end.compareTo(b.start); + } + + @Override + public Comparator endWithEndComparator() + { + return (a, b) -> a.end.compareTo(b.end); + } + } + + static class ItemKeyComparators implements IntervalBTree.IntervalComparators + { + private static final ItemKeyComparators INSTANCE = new ItemKeyComparators(); + @Override public Comparator totalOrder() { throw new UnsupportedOperationException(); } + + @Override + public Comparator startWithStartComparator() + { + return (a, b) -> a.getClass() == Item.class + ? intervalStartWithKeyStart(((Item) a).start.compareTo((RoutingKey)b)) + : keyStartWithIntervalStart(((RoutingKey)a).compareTo(((Item)b).start)); + } + + @Override + public Comparator startWithEndComparator() + { + return (a, b) -> a.getClass() == Item.class + ? intervalStartWithKeyEnd(((Item)a).start.compareTo((RoutingKey)b)) + : keyStartWithIntervalEnd(((RoutingKey)a).compareTo(((Item)b).end)); + } + + @Override + public Comparator endWithStartComparator() + { + return (a, b) -> a.getClass() == Item.class + ? intervalEndWithKeyStart(((Item)a).end.compareTo((RoutingKey)b)) + : keyEndWithIntervalStart(((RoutingKey)a).compareTo(((Item)b).start)); + } + + @Override + public Comparator endWithEndComparator() + { + return (a, b) -> a.getClass() == Item.class + ? intervalEndWithKeyEnd(((Item)a).end.compareTo((RoutingKey)b)) + : keyEndWithIntervalEnd(((RoutingKey)a).compareTo(((Item)b).end)); + } + } + + static class Item extends Entry + { + final RoutingKey start, end; + final int uniqueId; + + Item(RoutingKey start, RoutingKey end, Range key, int value, int uniqueId) + { + super(key, value); + this.start = start; + this.end = end; + this.uniqueId = uniqueId; + } + } + + private Object[] btree = IntervalBTree.empty(); + private int counter; + + private IntervalBTreeModel() + { + } + + @Override + public Object actual() + { + return btree; + } + + @Override + public void put(Range range, int value) + { + btree = org.apache.cassandra.utils.btree.IntervalBTree.update(btree, BTree.singleton(new Item(range.start(), range.end(), range, value, counter++)), ItemComparators.INSTANCE); + } + + @Override + public List> intersectsToken(Routing key) + { + return IntervalBTree.>>accumulate(btree, ItemKeyComparators.INSTANCE, key, (i1, i2, item, list) -> { list.add((Item)item); return list; }, null, null, new ArrayList<>()); + } + + @Override + public List> intersects(Range range) + { + return IntervalBTree.>>accumulate(btree, ItemComparators.INSTANCE, new Item(range.start(), range.end(), range, 0, 0), (i1, i2, item, list) -> { list.add(item); return list; }, null, null, new ArrayList<>()); + } + + @Override + public void done() + { + } + } + + } \ No newline at end of file diff --git a/test/unit/org/apache/cassandra/utils/btree/BTreeRemovalTest.java b/test/unit/org/apache/cassandra/utils/btree/BTreeRemovalTest.java index e764b3f9a1..a28b16193a 100644 --- a/test/unit/org/apache/cassandra/utils/btree/BTreeRemovalTest.java +++ b/test/unit/org/apache/cassandra/utils/btree/BTreeRemovalTest.java @@ -57,7 +57,7 @@ public class BTreeRemovalTest { for (int i = BTree.getChildStart(btree); i < BTree.getChildEnd(btree); ++i) result[i] = copy((Object[]) btree[i]); - final int[] sizeMap = BTree.getSizeMap(btree); + final int[] sizeMap = BTree.sizeMap(btree); final int[] resultSizeMap = new int[sizeMap.length]; System.arraycopy(sizeMap, 0, resultSizeMap, 0, sizeMap.length); result[result.length - 1] = resultSizeMap; @@ -96,7 +96,7 @@ public class BTreeRemovalTest assertEquals(expected[i], result[i]); for (int i = BTree.getChildStart(expected); i < BTree.getChildEnd(expected); ++i) assertBTree((Object[]) expected[i], (Object[]) result[i]); - assertArrayEquals(BTree.getSizeMap(expected), BTree.getSizeMap(result)); + assertArrayEquals(BTree.sizeMap(expected), BTree.sizeMap(result)); } } diff --git a/test/unit/org/apache/cassandra/utils/btree/IntervalBTreeTest.java b/test/unit/org/apache/cassandra/utils/btree/IntervalBTreeTest.java new file mode 100644 index 0000000000..421e3a037f --- /dev/null +++ b/test/unit/org/apache/cassandra/utils/btree/IntervalBTreeTest.java @@ -0,0 +1,256 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.cassandra.utils.btree; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Random; +import java.util.TreeSet; + +import org.junit.Test; + +import accord.utils.Invariants; + +import static org.apache.cassandra.utils.btree.BTree.getChildCount; +import static org.apache.cassandra.utils.btree.BTree.getChildStart; +import static org.apache.cassandra.utils.btree.BTree.getKeyEnd; +import static org.apache.cassandra.utils.btree.BTree.isLeaf; + +public class IntervalBTreeTest +{ + static class TestInterval implements Comparable + { + final int start, end; + final int value; + + TestInterval(int start, int end, int value) + { + this.start = start; + this.end = end; + this.value = value; + } + + @Override + public String toString() + { + return start + "," + end + ':' + value; + } + + @Override + public int compareTo(TestInterval that) + { + int c = Integer.compare(this.start, that.start); + if (c == 0) c = Integer.compare(this.end, that.end); + if (c == 0) c = Integer.compare(this.value, that.value); + return c; + } + } + + static class TestComparators implements IntervalBTree.IntervalComparators + { + static final TestComparators INSTANCE = new TestComparators(); + + @Override + public Comparator totalOrder() + { + return TestInterval::compareTo; + } + + @Override + public Comparator startWithStartComparator() + { + return (a, b) -> Integer.compare(a.start, b.start); + } + + @Override + public Comparator startWithEndComparator() + { + return (a, b) -> Integer.compare(a.start, b.end); + } + + @Override + public Comparator endWithStartComparator() + { + return (a, b) -> Integer.compare(a.end, b.start); + } + + @Override + public Comparator endWithEndComparator() + { + return (a, b) -> Integer.compare(a.end, b.end); + } + } + + @Test + public void testN() + { +// testOne(4391017837511000309L); + Random seeds = new Random(); + for (int i = 0 ; i < 1000 ; ++i) + testOne(seeds.nextLong()); + } + + public static void testOne(long seed) + { + try + { + List list = new ArrayList<>(); + Random random = new Random(); + random.setSeed(seed); + System.out.println(seed); + + int count = 1 << random.nextInt(11); + int maxRemoveSize = count == 1 ? 1 : 1 + random.nextInt(count - 1); + count = count + random.nextInt(count); + + TreeSet unique = new TreeSet<>(); + for (int i = 0 ; i < count ; ++i) + { + TestInterval interval = newInterval(random, 0, 10000); + if (unique.add(interval)) + list.add(interval); + } + + Object[] tree = BTree.empty(); + for (TestInterval v : list) + { + tree = IntervalBTree.update(tree, BTree.singleton(v), TestComparators.INSTANCE); + } + + for (int i1 = 0 ; i1 < list.size() ; ++i1) + { + TestInterval iv = list.get(i1); + TreeSet collect = collect(list, 0, iv); + remove(tree, collect, iv); + Invariants.require(collect.isEmpty()); + iv = newInterval(random, 0, 10000); + collect = collect(list, 0, iv); + remove(tree, collect, iv); + Invariants.require(collect.isEmpty()); + } + + Collections.shuffle(list, random); + for (int i = 0 ; i < list.size() ;) + { + int remaining = list.size() - i; + int c = remaining == 1 ? 1 : 1 + random.nextInt(Math.min(remaining, maxRemoveSize)); + + TestInterval iv = list.get(i++); + Object[] remove; + { + remove = IntervalBTree.singleton(iv); + while (--c > 0) + remove = IntervalBTree.update(remove, IntervalBTree.singleton(list.get(i++)), TestComparators.INSTANCE); + + int notPresentCount; + switch (random.nextInt(4)) + { + default: throw new IllegalStateException(); + case 0: notPresentCount = 0; break; + case 1: notPresentCount = random.nextInt(5); break; + case 2: notPresentCount = random.nextInt(Math.max(2, count/2)); break; + case 3: notPresentCount = random.nextInt(count*2); break; + } + + while (--notPresentCount > 0) + { + TestInterval add = newInterval(random, 0, 10000); + if (!unique.contains(add)) + remove = IntervalBTree.update(remove, IntervalBTree.singleton(add), TestComparators.INSTANCE); + } + } + tree = IntervalBTree.subtract(tree, remove, TestComparators.INSTANCE); + validate(tree, TestComparators.INSTANCE.endWithEndComparator()); + TreeSet collect = collect(list, i, iv); + remove(tree, collect, iv); + Invariants.require(collect.isEmpty()); + iv = newInterval(random, 0, 10000); + collect = collect(list, i, iv); + remove(tree, collect, iv); + Invariants.require(collect.isEmpty()); + } + } + catch (Throwable t) + { + throw new AssertionError("Failed with seed " + seed, t); + } + } + + private static TreeSet collect(List list, int from, TestInterval intersects) + { + TreeSet collect = new TreeSet<>(); + for (int i = from; i < list.size() ; ++i) + { + TestInterval iv2 = list.get(i); + if (intersects.start < iv2.end && iv2.start < intersects.end) + collect.add(iv2); + } + return collect; + } + + private static void remove(Object[] tree, TreeSet removeFrom, TestInterval intersects) + { + IntervalBTree.accumulate(tree, TestComparators.INSTANCE, intersects, (c, v, i, s) -> { + Invariants.require(c.remove(i)); + return null; + }, removeFrom, null, null); + } + + private static TestInterval newInterval(Random random, int from, int to) + { + int end = 1 + from + random.nextInt(to - (1 + from)); + int start = from + random.nextInt(end - from); + return new TestInterval(start, end, random.nextInt(10000)); + } + + static Object validate(Object[] tree, Comparator endSorter) + { + if (isLeaf(tree)) + return max(tree, endSorter); + + IntervalBTree.IntervalMaxIndex index = (IntervalBTree.IntervalMaxIndex) tree[tree.length - 1]; + Object[] tmp = new Object[getChildCount(tree)]; + for (int i = 0 ; i < index.sortedByEndIndex.length ; ++i) + tmp[index.sortedByEndIndex[i]] = index.sortedByEnd[i]; + + Object max = max(tree, endSorter); + for (int i = 0 ; i < getChildCount(tree) ; ++i) + { + Object childMax = validate((Object[])tree[getChildStart(tree) + i], endSorter); + if (endSorter.compare(childMax, max) > 0) + max = childMax; + Invariants.require(endSorter.compare(childMax, tmp[i]) == 0); + } + return max; + } + + private static Object max(Object[] tree, Comparator endSorter) + { + Object max = tree[0]; + for (int i = 1; i < getKeyEnd(tree) ; ++i) + { + if (endSorter.compare(tree[i], max) > 0) + max = tree[i]; + } + return max; + } + +}