(Accord) Fix:

- Attempt to fix CommandsForKey StackOverflowError (presumed to be reachable via SaveStatus->InternalStatus map returning null)
 - Bound recursion with SafeCommandStore.tryRecurse()
 - IndexOutOfBoundsException in CINTIA checkpoint list encoding bounds logic
 - Maintain MaxDecidedRX to save majority of work when deciding RX that are newer than those we have previously agreed
 - Introduce IntervalBTree and use in CommandsForRanges to limit time spent in critical section when there are millions of RX

patch by Benedict; reviewed by Alex Petrov for CASSANDRA-20727
This commit is contained in:
Benedict Elliott Smith 2025-06-22 18:28:12 +01:00
parent 6d303e6843
commit fadc64c17d
16 changed files with 1536 additions and 106 deletions

@ -1 +1 @@
Subproject commit 154c05f50dcfe9cb69d14e4a67a137ecd0600e88
Subproject commit 756f2a1f88f079e74c34fd8e0f3bb5aa98760bef

View File

@ -389,6 +389,14 @@ public class AccordCacheEntry<K, V> 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;

View File

@ -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<R> 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);
}

View File

@ -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<Timestamp, Summary> implements CommandSummaries.ByTxnIdSnapshot
{
static final IntervalComparators COMPARATORS = new IntervalComparators();
static final IntervalKeyComparators KEY_COMPARATORS = new IntervalKeyComparators();
static class TxnIdInterval implements Comparable<TxnIdInterval>
{
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<TxnIdInterval>
{
@Override public Comparator<TxnIdInterval> totalOrder() { return TxnIdInterval::compareTo; }
@Override public Comparator<TxnIdInterval> startWithStartComparator() { return (a, b) -> a.start.compareTo(b.start); }
@Override public Comparator<TxnIdInterval> startWithEndComparator() { return (a, b) -> a.start.compareTo(b.end); }
@Override public Comparator<TxnIdInterval> endWithStartComparator() { return (a, b) -> a.end.compareTo(b.start); }
@Override public Comparator<TxnIdInterval> endWithEndComparator() { return (a, b) -> a.end.compareTo(b.end); }
}
static class IntervalKeyComparators implements IntervalBTree.IntervalComparators<Object>
{
@Override public Comparator<Object> totalOrder() { throw new UnsupportedOperationException(); }
@Override
public Comparator<Object> 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<Object> 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<Object> 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<Object> 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<? extends Timestamp, ? extends Summary> m)
{
super(m);
@ -64,8 +159,10 @@ public class CommandsForRanges extends TreeMap<Timestamp, Summary> implements Co
{
private final AccordCommandStore commandStore;
private final RangeSearcher searcher;
private AtomicReference<NavigableMap<TxnId, Ranges>> transitive = new AtomicReference<>(new TreeMap<>());
private final ObjectHashSet<TxnId> cachedRangeTxns = new ObjectHashSet<>();
private final AtomicReference<NavigableMap<TxnId, Ranges>> transitive = new AtomicReference<>(new TreeMap<>());
// TODO (desired): manage memory consumed by this auxillary information
private final Object2ObjectHashMap<TxnId, RangeRoute> cachedRangeTxnsById = new Object2ObjectHashMap<>();
private Object[] cachedRangeTxnsByRange = IntervalBTree.empty();
public Manager(AccordCommandStore commandStore)
{
@ -78,11 +175,27 @@ public class CommandsForRanges extends TreeMap<Timestamp, Summary> implements Co
}
@Override
public void onAdd(AccordCacheEntry<TxnId, Command> state)
public void onUpdate(AccordCacheEntry<TxnId, Command> 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<Timestamp, Summary> 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<TxnIdInterval> 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<Timestamp, Summary> 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<NavigableMap<TxnId, Ranges>> update)
@ -150,15 +295,20 @@ public class CommandsForRanges extends TreeMap<Timestamp, Summary> 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<TxnId> 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<Timestamp, Summary> implements Co
NavigableMap<TxnId, Ranges> transitive = manager.transitive.get();
if (!transitive.isEmpty())
{
for (Map.Entry<TxnId, Ranges> e : transitive.tailMap(minTxnId, true).entrySet())
{
if (e.getValue().intersects(searchKeysOrRanges))
forEach.accept(e.getKey());
}
for (Map.Entry<TxnId, Ranges> e : transitive.tailMap(minTxnId, true).entrySet())
{
if (e.getValue().intersects(searchKeysOrRanges))
forEach.accept(e.getKey());
}
}
}
public void forEachInCache(Consumer<Summary> 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<Summary> forEach, AccordCommandStore.Caches caches)
{
switch (keysOrRanges.domain())
{
AccordCacheEntry<TxnId, Command> 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);

View File

@ -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()
{

View File

@ -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<K, V> extends SearchIterator<K, V>
{
/**
* @return true if iterator has any elements left, false otherwise
*/
boolean hasNext();
V next();
V peek();
}

View File

@ -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<V> extends AbstractFastBuilder implements AutoCloseable
{
private static final TinyThreadLocalPool<FastBuilder<?>> POOL = new TinyThreadLocalPool<>();
private TinyThreadLocalPool.TinyPool<FastBuilder<?>> 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<Compare, Existing extends Compare, Insert extends Compare> extends AbstractUpdater implements AutoCloseable
static class Updater<Compare, Existing extends Compare, Insert extends Compare> extends AbstractUpdater implements AutoCloseable
{
static final TinyThreadLocalPool<Updater> POOL = new TinyThreadLocalPool<>();
TinyThreadLocalPool.TinyPool<Updater> pool;
TinyThreadLocalPool.TinyPool pool;
// the new tree we navigate linearly, and are always on a key or at the end
final SimpleTreeKeysIterator<Compare, Insert> insert = new SimpleTreeKeysIterator<>();
@ -3453,7 +3469,6 @@ public class BTree
Comparator<? super Compare> comparator;
UpdateFunction<Insert, Existing> updateF;
static <Compare, Existing extends Compare, Insert extends Compare> Updater<Compare, Existing, Insert> get()
{
TinyThreadLocalPool.TinyPool<Updater> 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
* <p>
* The approach taken here hopefully balances simplicity, garbage generation and execution time.
*/
private static abstract class AbstractTransformer<I, O> extends AbstractUpdater implements AutoCloseable
static abstract class AbstractTransformer<I, O> 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<K, T extends K> extends AbstractTransformer<T, T> implements AutoCloseable
{
/**
* An iterator over the tree we are updating
*/
PeekingSearchIterator<K, ? extends K> remove;
Comparator<K> comparator;
Object[] apply(Object[] update, PeekingSearchIterator<K, ? extends K> 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<I, O> extends AbstractTransformer<I, O>
{
@ -4131,7 +4332,6 @@ public class BTree
}
}
private static class SimpleTreeKeysIterator<Compare, Insert extends Compare>
{
int leafSize;

View File

@ -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;

View File

@ -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<K, V> extends IndexedSearchIterator<K, V>, Iterator<V>
public interface BTreeSearchIterator<K, V> extends IndexedSearchIterator<K, V>, Iterator<V>, PeekingSearchIterator<K, V>
{
/**
* Reset this Iterator to its starting position

View File

@ -79,8 +79,7 @@ public class FullBTreeSearchIterator<K, V> extends TreeCursor<K> 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<K, V> extends TreeCursor<K> 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())

View File

@ -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<V>
{
Comparator<V> totalOrder();
Comparator<V> startWithStartComparator();
Comparator<V> startWithEndComparator();
Comparator<V> endWithStartComparator();
Comparator<V> 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 <R, P1, P2, V> V accumulate(Object[] btree, IntervalComparators<R> comparators, R intersects, QuadFunction<P1, P2, R, V, V> 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 <R, P1, P2, V> V accumulate(Object[] btree, QuadFunction<P1, P2, R, V, V> 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 <R, P1, P2, V> V accumulateMaxOnly(int ifChildBefore, int ifKeyBefore, Object[] btree, IntervalComparators<R> comparators, R intersects, QuadFunction<P1, P2, R, V, V> 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<IntervalIndexAdapter.SortEntry>
{
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<V> extends BTree.FastBuilder<V>
{
private static final TinyThreadLocalPool<FastInteralTreeBuilder<?>> 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<Compare, Existing extends Compare, Insert extends Compare> extends BTree.Updater<Compare, Existing, Insert>
{
static final TinyThreadLocalPool<IntervalUpdater> POOL = new TinyThreadLocalPool<>();
private final IntervalIndexAdapter adapter = new IntervalIndexAdapter();
static <Compare, Existing extends Compare, Insert extends Compare> IntervalUpdater<Compare, Existing, Insert> get(Comparator<Compare> compareEnds)
{
TinyThreadLocalPool.TinyPool<IntervalUpdater> pool = POOL.get();
IntervalUpdater<Compare, Existing, Insert> 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<K, T extends K> extends BTree.Subtraction<K, T>
{
static final FastThreadLocal<Subtraction> SHARED = new FastThreadLocal<>();
private final IntervalIndexAdapter adapter = new IntervalIndexAdapter();
Subtraction()
{
((IntervalBranchBuilder)parent).adapter = adapter;
}
static <K, T extends K> Subtraction<K, T> get(IntervalComparators<K> 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}.
* <p>
* Note that {@code UpdateFunction.noOp} is assumed to indicate a lack of interest in which value survives.
*/
public static <Compare> Object[] subtract(Object[] toUpdate, Object[] subtract, IntervalComparators<Compare> 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}.
* <p>
* Note that {@code UpdateFunction.noOp} is assumed to indicate a lack of interest in which value survives.
*/
public static <Compare, Existing extends Compare, Insert extends Compare> Object[] update(Object[] existing,
Object[] insert,
IntervalComparators<Compare> 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<Compare, Existing, Insert> 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 <V> FastInteralTreeBuilder<V> fastBuilder(Comparator<V> endSorter)
{
TinyThreadLocalPool.TinyPool<FastInteralTreeBuilder<?>> pool = FastInteralTreeBuilder.POOL.get();
FastInteralTreeBuilder<V> builder = (FastInteralTreeBuilder<V>) 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];
}
}

View File

@ -65,6 +65,15 @@ public class LeafBTreeSearchIterator<K, V> implements BTreeSearchIterator<K, V>
return elem;
}
@Override
public V peek()
{
if (!hasNext)
throw new NoSuchElementException();
return (V) keys[nextPos];
}
public boolean hasNext()
{
return hasNext;

View File

@ -234,7 +234,7 @@ class TreeCursor<K> extends NodeCursor<K>
return;
}
int[] sizeMap = getSizeMap(node);
int[] sizeMap = sizeMap(node);
int boundary = Arrays.binarySearch(sizeMap, relativeIndex);
if (boundary >= 0)
{

View File

@ -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<Range> 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<Entry>, Map.Entry<Range, Integer>
{
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<Routing, Range, Integer> tree;
private RangeTreeModel(RangeTree<Routing, Range, Integer> 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<Routing, Integer, Interval<Routing, Integer>> builder = IntervalTree.builder();
IntervalTree<Routing, Integer, Interval<Routing, Integer>> actual = null;
IntervalTree.Builder<Routing, Entry, Interval<Routing, Entry>> builder = IntervalTree.builder();
IntervalTree<Routing, Entry, Interval<Routing, Entry>> actual = null;
@Override
public IntervalTree<Routing, Integer, Interval<Routing, Integer>> actual()
public IntervalTree<Routing, Entry, Interval<Routing, Entry>> 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<Map.Entry<Range, Integer>> 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.Entry<Range, Integer>> map(List<Interval<Routing, Integer>> matches)
private static List<Map.Entry<Range, Integer>> map(List<Interval<Routing, Entry>> 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<Item>
{
static final ItemComparators INSTANCE = new ItemComparators();
@Override
public Comparator<Item> 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<Item> startWithStartComparator()
{
return (a, b) -> a.start.compareTo(b.start);
}
@Override
public Comparator<Item> startWithEndComparator()
{
return (a, b) -> a.start.compareTo(b.end);
}
@Override
public Comparator<Item> endWithStartComparator()
{
return (a, b) -> a.end.compareTo(b.start);
}
@Override
public Comparator<Item> endWithEndComparator()
{
return (a, b) -> a.end.compareTo(b.end);
}
}
static class ItemKeyComparators implements IntervalBTree.IntervalComparators<Object>
{
private static final ItemKeyComparators INSTANCE = new ItemKeyComparators();
@Override public Comparator<Object> totalOrder() { throw new UnsupportedOperationException(); }
@Override
public Comparator<Object> 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<Object> 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<Object> 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<Object> 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<Map.Entry<Range, Integer>> intersectsToken(Routing key)
{
return IntervalBTree.<Object, Object, Object, List<Map.Entry<Range, Integer>>>accumulate(btree, ItemKeyComparators.INSTANCE, key, (i1, i2, item, list) -> { list.add((Item)item); return list; }, null, null, new ArrayList<>());
}
@Override
public List<Map.Entry<Range, Integer>> intersects(Range range)
{
return IntervalBTree.<Item, Object, Object, List<Map.Entry<Range, Integer>>>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()
{
}
}
}

View File

@ -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));
}
}

View File

@ -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<TestInterval>
{
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<TestInterval>
{
static final TestComparators INSTANCE = new TestComparators();
@Override
public Comparator<TestInterval> totalOrder()
{
return TestInterval::compareTo;
}
@Override
public Comparator<TestInterval> startWithStartComparator()
{
return (a, b) -> Integer.compare(a.start, b.start);
}
@Override
public Comparator<TestInterval> startWithEndComparator()
{
return (a, b) -> Integer.compare(a.start, b.end);
}
@Override
public Comparator<TestInterval> endWithStartComparator()
{
return (a, b) -> Integer.compare(a.end, b.start);
}
@Override
public Comparator<TestInterval> 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<TestInterval> 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<TestInterval> 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<TestInterval> 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<TestInterval> 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<TestInterval> collect(List<TestInterval> list, int from, TestInterval intersects)
{
TreeSet<TestInterval> 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<TestInterval> 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;
}
}