From e69e995e6e00a12c94a81cd182e1d3ca7cb09360 Mon Sep 17 00:00:00 2001 From: "S.Vladykin" Date: Wed, 20 Apr 2016 15:51:18 +0300 Subject: [PATCH] ignite-db - wip --- .../cache/database/tree/BPlusTree.java | 340 +++++++++--------- .../cache/database/tree/reuse/ReuseList.java | 13 +- .../cache/database/tree/reuse/ReuseTree.java | 2 +- .../database/BPlusTreeSelfTest.java | 99 ++++- 4 files changed, 261 insertions(+), 193 deletions(-) diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/database/tree/BPlusTree.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/database/tree/BPlusTree.java index c741cc641fc71..c708a2b8e7fd7 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/database/tree/BPlusTree.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/database/tree/BPlusTree.java @@ -318,7 +318,7 @@ public abstract class BPlusTree { r.removed = getRow(io, buf, idx); - doRemove(io, leaf, buf, cnt, idx, r.meta, lvl, false); + r.doRemove(io, leaf, buf, cnt, idx, lvl, false); // We may need to replace inner key or want to merge this leaf with sibling after the remove -> keep lock. if (r.needReplaceInner == TRUE || @@ -434,7 +434,7 @@ public abstract class BPlusTree { assert t.io == io : "must be the same"; // Otherwise may be not compatible. - return mergePages(r.meta, prnt, t, fwd, fwdBuf) ? TRUE : FALSE; + return r.mergePages(prnt, t, fwd, fwdBuf) ? TRUE : FALSE; } }; @@ -849,14 +849,6 @@ public T remove(L row) throws IgniteCheckedException { return remove(row, false); } - /** - * @return Removed row. - * @throws IgniteCheckedException If failed. - */ - public T removeFirst() throws IgniteCheckedException { - return remove(null, false); // TODO - } - /** * @param row Lookup row. * @param ceil If we can remove ceil row when we can not find exact. @@ -890,48 +882,9 @@ public T remove(L row, boolean ceil) throws IgniteCheckedException { finally { r.releaseTail(); r.releaseMeta(); - -// if ("_key_PK".equals(getName()) && row.getValue(0).getInt() < 900) { -// X.println("row= " + row); -// X.println("rmv= " + r.removed); -// X.println("idx= " + getName()); -// X.println(printTree()); -// X.println("======================================"); -// } } } - /** - * @param io IO. - * @param buf Buffer. - * @param cnt Count. - * @param idx Index to remove. - * @param meta Meta page. - * @param lvl Level. - * @param kickLeftChild If we are dropping left child instead of the right one. - * @throws IgniteCheckedException If failed. - */ - private void doRemove(BPlusIO io, Page page, ByteBuffer buf, int cnt, int idx, Page meta, int lvl, - boolean kickLeftChild) throws IgniteCheckedException { - assert cnt > 0; - assert idx >= 0; - assert idx <= cnt; - - if (idx == cnt) { - idx--; // This may happen in case of right turn, we need to remove the rightmost ref and link. - - assert !kickLeftChild: "right child must be dropped here"; - } - - cnt--; - - io.copyItems(buf, buf, idx + 1, idx, cnt - idx, kickLeftChild); - io.setCount(buf, cnt); - - if (cnt == 0 && lvl != 0 && getRootLevel(meta) == lvl) - freePage(page, buf, io, meta, lvl); // Free root. - } - /** * @param r Remove operation. * @param pageId Page ID. @@ -1091,9 +1044,9 @@ private boolean mayMerge(int cnt, int cap) { /** * @param max Max. - * @return Random value from {@code 0} (inclusive) to the given max value exclusive. + * @return Random value from {@code 0} (inclusive) to the given max value (exclusive). */ - public static int randomInt(int max) { + public int randomInt(int max) { return ThreadLocalRandom.current().nextInt(max); } @@ -1116,113 +1069,6 @@ private int countAfterMerge(Tail cur, int fwdCnt) { return -1; } - /** - * @param prnt Parent tail. - * @param cur Current tail. - * @param fwd Forward page. - * @param fwdBuf Forward buffer. - * @throws IgniteCheckedException If failed. - */ - private boolean mergePages(Page meta, Tail prnt, Tail cur, Page fwd, ByteBuffer fwdBuf) - throws IgniteCheckedException { - assert io(fwdBuf) == cur.io; - - int cnt = cur.io.getCount(cur.buf); - int fwdCnt = cur.io.getCount(fwdBuf); - int newCnt = countAfterMerge(cur, fwdCnt); - - if (newCnt == -1) // Not enough space. - return false; - - cur.io.setCount(cur.buf, newCnt); - - int prntCnt = prnt.io.getCount(prnt.buf); - - // Move down split key in inner pages. - if (cur.lvl != 0) { - int prntIdx = prnt.idx; - - if (prntIdx == prntCnt) // It was a right turn. - prntIdx--; - - // We can be sure that we have enough free space to store split key here, - // because we've done remove already and did not release child locks. - inner(cur.io).store(cur.buf, cnt, prnt.io, prnt.buf, prntIdx); - - cnt++; - } - - cur.io.copyItems(fwdBuf, cur.buf, 0, cnt, fwdCnt, true); - cur.io.setForward(cur.buf, cur.io.getForward(fwdBuf)); - - assert prntCnt > 0: prntCnt; - - // Remove split key from parent. If parent is root and becomes empty, it will be freed by doRemove. - doRemove(prnt.io, prnt.page, prnt.buf, prntCnt, prnt.idx, meta, prnt.lvl, false); - - // Forward page is now empty and has no links. - freePage(fwd, fwdBuf, cur.io, meta, cur.lvl); - - return true; - } - - /** - * @param meta Meta. - * @param inner Inner replace page. - * @throws IgniteCheckedException If failed. - */ - private void dropEmptyBranch(Page meta, Tail inner) throws IgniteCheckedException { - int cnt = inner.io.getCount(inner.buf); - - assert cnt > 0: cnt; - assert inner.fwd == null: "if we've found our inner key in this page it can't be a back page"; - - // We need to check if the branch we are going to drop goes to the left or to the right. - boolean kickLeft = inner.down.page.id() == inner(inner.io).getLeft(inner.buf, inner.idx); - - assert kickLeft || inner.down.page.id() == inner(inner.io).getRight(inner.buf, inner.idx); - - // Remove found inner key from inner page. - doRemove(inner.io, inner.page, inner.buf, cnt, inner.idx, meta, inner.lvl, kickLeft); - - // If inner page was root and became empty, it was freed in doRemove. - // Otherwise we can be sure that inner page was not freed, at lead it must become - // an empty routing page. Thus always starting from inner.down here. - for (Tail t = inner.down; t != null; t = t.down) { - if (t.fwd != null) - t = t.fwd; - - assert t.io.getCount(t.buf) == 0; - - freePage(t.page, t.buf, t.io, meta, t.lvl); - } - } - - /** - * @param page Page. - * @param buf Buffer. - * @param io IO. - * @param meta Meta page. - * @param lvl Level. - * @throws IgniteCheckedException If failed. - */ - private void freePage(Page page, ByteBuffer buf, BPlusIO io, Page meta, int lvl) - throws IgniteCheckedException { - if (getLeftmostPageId(meta, lvl) == page.id()) { - // This logic will handle root as well. - long fwdId = io.getForward(buf); - - writePage(meta, updateLeftmost, fwdId, lvl, FALSE); - } - - // Mark removed. - io.setRemoveId(buf, Long.MAX_VALUE); - - // Reuse empty page. - if (reuseList != null) - reuseList.put(this, page.fullId()); - } - /** * @param r Remove. * @param page Page. @@ -1667,7 +1513,7 @@ boolean notFound(BPlusIO io, ByteBuffer buf, int idx, int lvl) throws IgniteC /** * Release meta page. */ - void releaseMeta() { + final void releaseMeta() { if (meta != null) { meta.close(); meta = null; @@ -1687,11 +1533,11 @@ boolean canRelease(Page page, int lvl) { /** * Get a single entry. */ - private class GetOne extends Get { + private final class GetOne extends Get { /** * @param row Row. */ - public GetOne(L row) { + private GetOne(L row) { super(row); } @@ -1712,15 +1558,15 @@ public GetOne(L row) { /** * Get a cursor for range. */ - private class GetCursor extends Get { + private final class GetCursor extends Get { /** */ - private ForwardCursor cursor; + ForwardCursor cursor; /** * @param lower Lower bound. * @param upper Upper bound. */ - public GetCursor(L lower, L upper) { + private GetCursor(L lower, L upper) { super(lower); cursor = new ForwardCursor(upper); @@ -1754,7 +1600,7 @@ public GetCursor(L lower, L upper) { /** * Put operation. */ - private class Put extends Get { + private final class Put extends Get { /** Right child page ID for split row. */ long rightId; @@ -1779,7 +1625,7 @@ private class Put extends Get { /** * @param row Row. */ - Put(T row) { + private Put(T row) { super(row); } @@ -1838,7 +1684,7 @@ private boolean isFinished() { /** * Remove operation. */ - private class Remove extends Get { + private final class Remove extends Get { /** */ boolean ceil; @@ -1864,7 +1710,7 @@ private class Remove extends Get { * @param row Row. * @param ceil If we can remove ceil row when we can not find exact. */ - Remove(L row, boolean ceil) { + private Remove(L row, boolean ceil) { super(row); this.ceil = ceil; @@ -1896,7 +1742,7 @@ private class Remove extends Get { /** * Finish the operation. */ - void finish() { + private void finish() { assert tail == null; row = null; @@ -1909,7 +1755,7 @@ void finish() { * @return {@code false} If failed to finish and we need to lock more pages up. * @throws IgniteCheckedException If failed. */ - boolean finishTail(boolean skipMergeMore) throws IgniteCheckedException { + private boolean finishTail(boolean skipMergeMore) throws IgniteCheckedException { assert !isFinished(); assert needMerge != FALSE || needReplaceInner != FALSE; assert tail != null; @@ -1951,6 +1797,144 @@ else if (needMerge == READY) { return true; } + /** + * @param io IO. + * @param buf Buffer. + * @param cnt Count. + * @param idx Index to remove. + * @param lvl Level. + * @param kickLeftChild If we are dropping left child instead of the right one. + * @throws IgniteCheckedException If failed. + */ + private void doRemove(BPlusIO io, Page page, ByteBuffer buf, int cnt, int idx, int lvl, + boolean kickLeftChild) throws IgniteCheckedException { + assert cnt > 0; + assert idx >= 0; + assert idx <= cnt; + + if (idx == cnt) { + idx--; // This may happen in case of right turn, we need to remove the rightmost ref and link. + + assert !kickLeftChild: "right child must be dropped here"; + } + + cnt--; + + io.copyItems(buf, buf, idx + 1, idx, cnt - idx, kickLeftChild); + io.setCount(buf, cnt); + + if (cnt == 0 && lvl != 0 && getRootLevel(meta) == lvl) + freePage(page, buf, io, lvl); // Free root. + } + + /** + * @param prnt Parent tail. + * @param cur Current tail. + * @param fwd Forward page. + * @param fwdBuf Forward buffer. + * @throws IgniteCheckedException If failed. + */ + private boolean mergePages(Tail prnt, Tail cur, Page fwd, ByteBuffer fwdBuf) + throws IgniteCheckedException { + assert io(fwdBuf) == cur.io; + + int cnt = cur.io.getCount(cur.buf); + int fwdCnt = cur.io.getCount(fwdBuf); + int newCnt = countAfterMerge(cur, fwdCnt); + + if (newCnt == -1) // Not enough space. + return false; + + cur.io.setCount(cur.buf, newCnt); + + int prntCnt = prnt.io.getCount(prnt.buf); + + // Move down split key in inner pages. + if (cur.lvl != 0) { + int prntIdx = prnt.idx; + + if (prntIdx == prntCnt) // It was a right turn. + prntIdx--; + + // We can be sure that we have enough free space to store split key here, + // because we've done remove already and did not release child locks. + inner(cur.io).store(cur.buf, cnt, prnt.io, prnt.buf, prntIdx); + + cnt++; + } + + cur.io.copyItems(fwdBuf, cur.buf, 0, cnt, fwdCnt, true); + cur.io.setForward(cur.buf, cur.io.getForward(fwdBuf)); + + assert prntCnt > 0: prntCnt; + + // Remove split key from parent. If parent is root and becomes empty, it will be freed by doRemove. + doRemove(prnt.io, prnt.page, prnt.buf, prntCnt, prnt.idx, prnt.lvl, false); + + // Forward page is now empty and has no links. + freePage(fwd, fwdBuf, cur.io, cur.lvl); + + return true; + } + + /** + * @param page Page. + * @param buf Buffer. + * @param io IO. + * @param lvl Level. + * @throws IgniteCheckedException If failed. + */ + private void freePage(Page page, ByteBuffer buf, BPlusIO io, int lvl) + throws IgniteCheckedException { + if (getLeftmostPageId(meta, lvl) == page.id()) { + // This logic will handle root as well. + long fwdId = io.getForward(buf); + + writePage(meta, updateLeftmost, fwdId, lvl, FALSE); + } + + // Mark removed. + io.setRemoveId(buf, Long.MAX_VALUE); + + // Reuse empty page. + if (reuseList != null) { +// U.dumpStack("page: " + page.fullId()); + + reuseList.put(BPlusTree.this, page.fullId()); + } + } + + /** + * @param inner Inner replace page. + * @throws IgniteCheckedException If failed. + */ + private void dropEmptyBranch(Tail inner) throws IgniteCheckedException { + int cnt = inner.io.getCount(inner.buf); + + assert cnt > 0: cnt; + assert inner.fwd == null: "if we've found our inner key in this page it can't be a back page"; + + // We need to check if the branch we are going to drop goes to the left or to the right. + boolean kickLeft = inner.down.page.id() == inner(inner.io).getLeft(inner.buf, inner.idx); + + assert kickLeft || inner.down.page.id() == inner(inner.io).getRight(inner.buf, inner.idx); + + // Remove found inner key from inner page. + doRemove(inner.io, inner.page, inner.buf, cnt, inner.idx, inner.lvl, kickLeft); + + // If inner page was root and became empty, it was freed in doRemove. + // Otherwise we can be sure that inner page was not freed, at lead it must become + // an empty routing page. Thus always starting from inner.down here. + for (Tail t = inner.down; t != null; t = t.down) { + if (t.fwd != null) + t = t.fwd; + + assert t.io.getCount(t.buf) == 0; + + freePage(t.page, t.buf, t.io, t.lvl); + } + } + /** * @throws IgniteCheckedException If failed. */ @@ -1969,7 +1953,7 @@ private void doReplaceInner() throws IgniteCheckedException { if (cnt == 0) { // Merge empty leaf page. if (!merge(0, true, false)) { // For leaf pages this can happen only when parent is empty -> drop the whole branch. - dropEmptyBranch(meta, inner); + dropEmptyBranch(inner); return; } @@ -2055,7 +2039,7 @@ private boolean merge(int lvl, boolean skipMayMerge, boolean releaseMerged) thro else { assert cur.io == back.io: "must always be the same"; // Otherwise may be not compatible. - if (mergePages(meta, prnt, back, cur.page, cur.buf)) { + if (mergePages(prnt, back, cur.page, cur.buf)) { assert prnt.down == back; assert back.fwd == cur; @@ -2080,14 +2064,14 @@ private boolean merge(int lvl, boolean skipMayMerge, boolean releaseMerged) thro /** * @return {@code true} If finished. */ - boolean isFinished() { + private boolean isFinished() { return row == null; } /** * Release pages for all locked levels at the tail. */ - void releaseTail() { + private void releaseTail() { Tail t = tail; tail = null; @@ -2112,7 +2096,7 @@ void releaseTail() { * @param lvl Level. * @return {@code true} If the given page is in tail. */ - boolean isTail(long pageId, int lvl) { + private boolean isTail(long pageId, int lvl) { Tail t = tail; while (t != null) { @@ -2139,7 +2123,7 @@ boolean isTail(long pageId, int lvl) { * @param back If the given page is back page for the current tail, otherwise it must be an upper level page. * @param idx Insertion index. */ - void addTail(Page page, ByteBuffer buf, BPlusIO io, int lvl, boolean back, int idx) { + private void addTail(Page page, ByteBuffer buf, BPlusIO io, int lvl, boolean back, int idx) { Tail t = new Tail<>(page, buf, io, lvl, idx); if (back) { @@ -2161,7 +2145,7 @@ void addTail(Page page, ByteBuffer buf, BPlusIO io, int lvl, boolean back, in /** * @return Non-back tail page. */ - public Page nonBackTailPage() { + private Page nonBackTailPage() { assert tail != null; return tail.fwd == null ? tail.page : tail.fwd.page; @@ -2172,7 +2156,7 @@ public Page nonBackTailPage() { * @param back Back page. * @return Tail. */ - public Tail getTail(int lvl, boolean back) { + private Tail getTail(int lvl, boolean back) { assert lvl <= tail.lvl: "level is too high"; Tail t = tail; diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/database/tree/reuse/ReuseList.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/database/tree/reuse/ReuseList.java index 3c5cdbf3c532b..186fee5a80619 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/database/tree/reuse/ReuseList.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/database/tree/reuse/ReuseList.java @@ -19,18 +19,20 @@ import org.apache.ignite.IgniteCheckedException; import org.apache.ignite.internal.pagemem.FullPageId; +import org.apache.ignite.internal.pagemem.PageIdUtils; import org.apache.ignite.internal.pagemem.PageMemory; import org.apache.ignite.internal.processors.cache.database.MetaStore; import org.apache.ignite.internal.processors.cache.database.tree.BPlusTree; import org.apache.ignite.internal.util.typedef.internal.A; import org.apache.ignite.lang.IgniteBiTuple; -import static org.apache.ignite.internal.processors.cache.database.tree.BPlusTree.randomInt; - /** * Reuse list for index pages. */ public class ReuseList { + /** */ + private static final FullPageId MIN = new FullPageId(0,0); + /** */ private final ReuseTree[] trees; @@ -67,7 +69,7 @@ public ReuseList(int cacheId, PageMemory pageMem, int segments, MetaStore metaSt private ReuseTree tree(BPlusTree client) { assert trees.length > 1; - int treeIdx = randomInt(trees.length); + int treeIdx = client.randomInt(trees.length); ReuseTree tree = trees[treeIdx]; @@ -92,7 +94,10 @@ private ReuseTree tree(BPlusTree client) { * @throws IgniteCheckedException If failed. */ public FullPageId take(BPlusTree client) throws IgniteCheckedException { - return ready ? tree(client).removeFirst() : null; + assert PageIdUtils.pageIdx(MIN.pageId()) == 0; + + // Remove and return page at min position. + return ready ? tree(client).removeCeil(MIN) : null; } /** diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/database/tree/reuse/ReuseTree.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/database/tree/reuse/ReuseTree.java index 2a27d585205fd..6206465ad7295 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/database/tree/reuse/ReuseTree.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/database/tree/reuse/ReuseTree.java @@ -75,7 +75,7 @@ public ReuseTree(ReuseList reuseList, int cacheId, PageMemory pageMem, FullPageI throws IgniteCheckedException { long pageIdx = io.isLeaf() ? PageIdUtils.pageIdx(((ReuseLeafIO)io).getPageId(buf, idx)) : - (((ReuseInnerIO)io).getPageIndex(buf, idx) & 0xFFFFFFFFL ); + (((ReuseInnerIO)io).getPageIndex(buf, idx) & 0xFFFFFFFFL); return Long.compare(pageIdx, PageIdUtils.pageIdx(fullPageId.pageId())); } diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/database/BPlusTreeSelfTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/database/BPlusTreeSelfTest.java index 7b9512f29fd6d..4968fe8f5bad7 100644 --- a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/database/BPlusTreeSelfTest.java +++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/database/BPlusTreeSelfTest.java @@ -33,7 +33,6 @@ import org.apache.ignite.internal.processors.cache.database.tree.io.BPlusInnerIO; import org.apache.ignite.internal.processors.cache.database.tree.io.BPlusLeafIO; import org.apache.ignite.internal.processors.cache.database.tree.reuse.ReuseList; -import org.apache.ignite.internal.util.GridRandom; import org.apache.ignite.internal.util.lang.GridCursor; import org.apache.ignite.internal.util.typedef.T2; import org.apache.ignite.internal.util.typedef.X; @@ -69,6 +68,12 @@ public class BPlusTreeSelfTest extends GridCommonAbstractTest { /** {@inheritDoc} */ @Override protected void beforeTest() throws Exception { + long seed = System.nanoTime(); + + X.println("Test seed: " + seed); + + TestTree.rnd = new Random(seed); + pageMem = new PageMemoryImpl(log, new UnsafeMemoryProvider(64 * MB, 32 * MB), null, PAGE_SIZE, CPUS); pageMem.start(); @@ -200,29 +205,33 @@ public void testPutRemoveFindRandom1() throws IgniteCheckedException { */ private void doTestPutRemoveFindRandom(boolean canGetRow) throws IgniteCheckedException { TestTree tree = createTestTree(canGetRow); - Map map = new HashMap<>(); - long seed = System.currentTimeMillis(); - - X.println("seed: " + seed); - - Random rnd = new GridRandom(seed); + Map map = new HashMap<>(); int cnt = 100_000; for (int i = 0, end = 30 * cnt; i < end; i++) { - long x = rnd.nextInt(cnt); +// if (i % 1000 == 0) +// X.println(" -> " + i); - switch(rnd.nextInt(3)) { + long x = tree.randomInt(cnt); + + switch(tree.randomInt(3)) { case 0: +// X.println("Put: " + x); + assertEquals(map.put(x, x), tree.put(x)); case 1: +// X.println("Get: " + x); + assertEquals(map.get(x), tree.findOne(x)); break; case 2: +// X.println("Rmv: " + x); + assertEquals(map.remove(x), tree.remove(x)); break; @@ -239,7 +248,7 @@ private void doTestPutRemoveFindRandom(boolean canGetRow) throws IgniteCheckedEx GridCursor cursor = tree.find(null, null); - while(cursor.next()) { + while (cursor.next()) { Long x = cursor.get(); assert x != null; @@ -250,6 +259,67 @@ private void doTestPutRemoveFindRandom(boolean canGetRow) throws IgniteCheckedEx assertTrue(map.isEmpty()); } + /** + * @throws IgniteCheckedException If failed. + */ + public void testRandomRemove0() throws IgniteCheckedException { + doTestRandomRemove(false); + } + + /** + * @throws IgniteCheckedException If failed. + */ + public void testRandomRemove1() throws IgniteCheckedException { + doTestRandomRemove(true); + } + + /** + * @param canGetRow Can get row from inner page. + * @throws IgniteCheckedException If failed. + */ + private void doTestRandomRemove(boolean canGetRow) throws IgniteCheckedException { + TestTree tree = createTestTree(canGetRow); + + Map map = new HashMap<>(); + + int cnt = 100_000; + + for (long x = 0; x < cnt; x++) + assertEquals(map.put(x,x), tree.put(x)); + + for (;;) { + for (int i = 0; i < 1000 && !map.isEmpty();) { + Long x = (long)tree.randomInt(cnt); + + if (map.remove(x) != null) { + assertEquals(x, tree.remove(x)); + assertNull(tree.remove(x)); + + i++; + } + } + + GridCursor cursor = tree.find(null, null); + + int size = 0; + + while (cursor.next()) { + size++; + + Long x = cursor.get(); + + assert x != null; + + assertEquals(map.get(x), x); + } + + assertEquals(map.size(), size); + + if (size == 0) + break; + } + } + /** * @param canGetRow Can get row from inner page. * @return Test tree instance. @@ -271,6 +341,9 @@ private FullPageId allocatePage() throws IgniteCheckedException { * Test tree. */ private static class TestTree extends BPlusTree { + /** */ + static Random rnd; + /** */ final boolean canGetRow; @@ -290,6 +363,12 @@ public TestTree(ReuseList reuseList, boolean canGetRow, int cacheId, PageMemory initNew(); } + /** {@inheritDoc} */ + @Override public int randomInt(int max) { + // Need to have predictable reproducibility. + return rnd.nextInt(max); + } + /** {@inheritDoc} */ @Override protected BPlusIO io(int type, int ver) { BPlusIO io = io(type);