blob: 8a4c59c93c4e296e745870da5fdd12a58da7c1bc [file] [log] [blame]
/*
* 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.ignite.internal.processors.database;
import java.io.Externalizable;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.configuration.DataRegionConfiguration;
import org.apache.ignite.failure.FailureContext;
import org.apache.ignite.internal.IgniteInternalFuture;
import org.apache.ignite.internal.mem.unsafe.UnsafeMemoryProvider;
import org.apache.ignite.internal.pagemem.FullPageId;
import org.apache.ignite.internal.pagemem.PageIdAllocator;
import org.apache.ignite.internal.pagemem.PageMemory;
import org.apache.ignite.internal.pagemem.PageUtils;
import org.apache.ignite.internal.pagemem.impl.PageMemoryNoStoreImpl;
import org.apache.ignite.internal.processors.cache.persistence.DataRegionMetricsImpl;
import org.apache.ignite.internal.processors.cache.persistence.diagnostic.pagelocktracker.PageLockTrackerManager;
import org.apache.ignite.internal.processors.cache.persistence.tree.BPlusTree;
import org.apache.ignite.internal.processors.cache.persistence.tree.io.BPlusIO;
import org.apache.ignite.internal.processors.cache.persistence.tree.io.BPlusInnerIO;
import org.apache.ignite.internal.processors.cache.persistence.tree.io.BPlusLeafIO;
import org.apache.ignite.internal.processors.cache.persistence.tree.io.IOVersions;
import org.apache.ignite.internal.processors.cache.persistence.tree.io.PageIO;
import org.apache.ignite.internal.processors.failure.FailureProcessor;
import org.apache.ignite.internal.util.typedef.T2;
import org.apache.ignite.testframework.junits.GridTestKernalContext;
import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
import org.junit.Test;
import static org.apache.ignite.internal.util.IgniteUtils.MB;
import static org.apache.ignite.testframework.GridTestUtils.runAsync;
/**
* Test is based on {@link BPlusTreeSelfTest} and has a partial copy of its code.
*/
public class BPlusTreeReplaceRemoveRaceTest extends GridCommonAbstractTest {
/** */
private static final short PAIR_INNER_IO = 30000;
/** */
private static final short PAIR_LEAF_IO = 30001;
/** */
protected static final int PAGE_SIZE = 512;
/** */
private static final int CACHE_ID = 100500;
/** */
protected PageMemory pageMem;
/** Tracking of locks holding. */
private PageLockTrackerManager lockTrackerManager;
/** {@inheritDoc} */
@Override protected void beforeTest() throws Exception {
pageMem = createPageMemory();
lockTrackerManager = new PageLockTrackerManager(log, "testTreeManager");
lockTrackerManager.start();
}
/** {@inheritDoc} */
@Override protected void afterTest() throws Exception {
if (pageMem != null)
pageMem.stop(true);
if (lockTrackerManager != null)
lockTrackerManager.stop();
}
/**
* @return Page memory.
*/
protected PageMemory createPageMemory() {
DataRegionConfiguration plcCfg = new DataRegionConfiguration()
.setInitialSize(1024 * MB)
.setMaxSize(1024 * MB);
PageMemory pageMem = new PageMemoryNoStoreImpl(log,
new UnsafeMemoryProvider(log),
null,
PAGE_SIZE,
plcCfg,
new DataRegionMetricsImpl(plcCfg, new GridTestKernalContext(log())),
true);
pageMem.start();
return pageMem;
}
/**
* @return Allocated meta page ID.
* @throws IgniteCheckedException If failed.
*/
private FullPageId allocateMetaPage() throws IgniteCheckedException {
return new FullPageId(pageMem.allocatePage(CACHE_ID, PageIdAllocator.INDEX_PARTITION, PageIdAllocator.FLAG_IDX), CACHE_ID);
}
/**
* Short for {@code T2<Integer, Integer>}.
*/
protected static class Pair extends T2<Integer, Integer> {
/** Serial version uid. */
private static final long serialVersionUID = 0L;
/**
* @param key Key.
* @param val Value.
*/
public Pair(Integer key, Integer val) {
super(key, val);
}
/**
* No-arg constructor for {@link Externalizable}.
*/
public Pair() {
}
}
/**
* Test tree, maps {@link Integer} to {@code Integer}.
*/
protected static class TestPairTree extends BPlusTree<Pair, Pair> {
/**
* Constructor.
*
* @param cacheId Cache ID.
* @param pageMem Page memory.
* @param metaPageId Meta page ID.
* @throws IgniteCheckedException If failed.
*/
public TestPairTree(
int cacheId,
PageMemory pageMem,
long metaPageId,
PageLockTrackerManager lockTrackerManager
) throws IgniteCheckedException {
super(
"test",
cacheId,
null,
pageMem,
null,
new AtomicLong(),
metaPageId,
null,
new IOVersions<>(new TestPairInnerIO()),
new IOVersions<>(new TestPairLeafIO()),
PageIdAllocator.FLAG_IDX,
new FailureProcessor(new GridTestKernalContext(log)) {
/** {@inheritdoc} */
@Override public boolean process(FailureContext failureCtx) {
return true;
}
},
lockTrackerManager
);
PageIO.registerTest(latestInnerIO(), latestLeafIO());
initTree(true);
}
/** {@inheritDoc} */
@Override protected int compare(BPlusIO<Pair> io, long pageAddr, int idx, Pair n2)
throws IgniteCheckedException {
Pair n1 = io.getLookupRow(this, pageAddr, idx);
return Integer.compare(n1.getKey(), n2.getKey());
}
/** {@inheritDoc} */
@Override public Pair getRow(BPlusIO<Pair> io, long pageAddr, int idx, Object ignore)
throws IgniteCheckedException {
return io.getLookupRow(this, pageAddr, idx);
}
}
/** */
private static final class TestPairInnerIO extends BPlusInnerIO<Pair> {
/** */
TestPairInnerIO() {
super(PAIR_INNER_IO, 1, true, 8);
}
/** {@inheritDoc} */
@Override public int getMaxCount(long buf, int pageSize) {
return 2;
}
/** {@inheritDoc} */
@Override public void store(long dst, int dstIdx, BPlusIO<Pair> srcIo, long src, int srcIdx)
throws IgniteCheckedException {
Pair row = srcIo.getLookupRow(null, src, srcIdx);
store(dst, dstIdx, row, null, false);
}
/** {@inheritDoc} */
@Override public void storeByOffset(long pageAddr, int off, Pair row) {
PageUtils.putInt(pageAddr, off, row.getKey());
PageUtils.putInt(pageAddr, off + 4, row.getValue());
}
/** {@inheritDoc} */
@Override public Pair getLookupRow(BPlusTree<Pair, ?> tree, long pageAddr, int idx) {
int key = PageUtils.getInt(pageAddr, offset(idx));
int val = PageUtils.getInt(pageAddr, offset(idx) + 4);
return new Pair(key, val);
}
}
/** */
private static final class TestPairLeafIO extends BPlusLeafIO<Pair> {
/** */
TestPairLeafIO() {
super(PAIR_LEAF_IO, 1, 8);
}
/** {@inheritDoc} */
@Override public int getMaxCount(long pageAddr, int pageSize) {
return 2;
}
/** {@inheritDoc} */
@Override public void storeByOffset(long pageAddr, int off, Pair row) {
PageUtils.putInt(pageAddr, off, row.getKey());
PageUtils.putInt(pageAddr, off + 4, row.getValue());
}
/** {@inheritDoc} */
@Override public void store(long dst, int dstIdx, BPlusIO<Pair> srcIo, long src, int srcIdx) throws IgniteCheckedException {
Pair row = srcIo.getLookupRow(null, src, srcIdx);
store(dst, dstIdx, row, null, false);
}
/** {@inheritDoc} */
@Override public Pair getLookupRow(BPlusTree<Pair, ?> tree, long pageAddr, int idx) {
int key = PageUtils.getInt(pageAddr, offset(idx));
int val = PageUtils.getInt(pageAddr, offset(idx) + 4);
return new Pair(key, val);
}
}
/**
* Tests a very specific scenario for concurrent replace and remove that used to corrupt the tree before the fix.
* <p/>
*
* Consider the following tree, represented by a {@code tree} variable in test:<br>
* <pre><code>
* [ 5:0 ]
* / \
* [ 2:0 | 4:0 ] [ 6:0 ]
* / | \ / \
* [ 1:0 | 2:0 ] [ 3:0 | 4:0 ] [ 5:0 ] [ 6:0 ] [ 7:0 ]
* </code></pre>
*
* Individual replace {@code 4:0} to {@code 4:8} would take two steps and look like this:
* <pre><code>
* // Inner node goes first.
* [ 5:0 ]
* / \
* [ 2:0 | 4:8 ] [ 6:0 ]
* / | \ / \
* [ 1:0 | 2:0 ] [ 3:0 | 4:0 ] [ 5:0 ] [ 6:0 ] [ 7:0 ]
*
* // Leaf node goes last.
* [ 5:0 ]
* / \
* [ 2:0 | 4:8 ] [ 6:0 ]
* / | \ / \
* [ 1:0 | 2:0 ] [ 3:0 | 4:8 ] [ 5:0 ] [ 6:0 ] [ 7:0 ]
* </code></pre>
*
* Note that inbetween these two updates tree is fully unlocked and available for modifications. So, if one tries
* to remove {@code 5:0} during the replacement, following modifications would happen:
* <pre><code>
* // Inner node update from replacement goes first, as before.
* [ 5:0 ]
* / \
* [ 2:0 | 4:8 ] [ 6:0 ]
* / | \ / \
* [ 1:0 | 2:0 ] [ 3:0 | 4:0 ] [ 5:0 ] [ 6:0 ] [ 7:0 ]
*
* // Removal of 5:0 starts from the leaf.
* [ 5:0 ]
* / \
* [ 2:0 | 4:8 ] [ 6:0 ]
* / | \ / \
* [ 1:0 | 2:0 ] [ 3:0 | 4:0 ] [] [ 6:0 ] [ 7:0 ]
*
* // Merge of empty branch is now required, 4:8 is removed from inner node.
* [ 5:0 ]
* / \
* [ 2:0 ] [ 6:0 ]
* / \ / \
* [ 1:0 | 2:0 ] [ 3:0 | 4:0 ] [ 6:0 ] [ 7:0 ]
*
* // Inner replace is happening in the root. To do that, closest left value is retrieved from the leaf, it's 4:0.
* [ 4:0 ]
* / \
* [ 2:0 ] [ 6:0 ]
* / \ / \
* [ 1:0 | 2:0 ] [ 3:0 | 4:0 ] [ 6:0 ] [ 7:0 ]
*
* // At this point removal is complete. Last replacement step will do the following.
* [ 4:0 ]
* / \
* [ 2:0 ] [ 6:0 ]
* / \ / \
* [ 1:0 | 2:0 ] [ 3:0 | 4:8 ] [ 6:0 ] [ 7:0 ]
* </code></pre>
*
* It is clear that root has an invalid value {@code 4:0}, hence the tree should be considered corrupted.
* This is the exact situation that test is trying to check.
* <p/>
*
* Several iterations are required for this, given that there's no guaranteed way to force a tree to perform page
* modifications in the desired order. Typically, less than {@code 10} attempts have been required to get a
* corrupted tree. Value {@code 100} is arbitrary and has been chosen to be big enough for test to fail in case of
* regression, but not too big so that test won't run for too long.
*
* @throws Exception If failed.
*/
@Test
public void testConcurrentPutRemove() throws Exception {
for (int i = 0; i < 100; i++) {
TestPairTree tree = prepareBPlusTree();
// Exact tree from the description is constructed at this point.
CyclicBarrier barrier = new CyclicBarrier(2);
// This is the replace operation.
IgniteInternalFuture<?> putFut = runAsync(() -> {
barrier.await();
tree.putx(new Pair(4, 999));
});
// This is the remove operation.
IgniteInternalFuture<?> remFut = runAsync(() -> {
barrier.await();
tree.removex(new Pair(5, -1));
});
// Wait for both operations.
try {
putFut.get(1, TimeUnit.SECONDS);
}
finally {
remFut.get(1, TimeUnit.SECONDS);
}
// Just in case.
tree.validateTree();
// Find a value associated with 4. It'll be right in the root page.
Pair pair = tree.findOne(new Pair(4, -1));
// Assert that it is valid.
assertEquals(999, pair.getValue().intValue());
}
}
/**
* Checks that there will be no corrupted B+tree during concurrent update and deletion
* of the same key that is contained in the inner and leaf nodes of the B+tree.
*
* NOTE: Test logic is the same as of {@link #testConcurrentPutRemove},
* the only difference is that it operates (puts and removes) on a single key.
*
* @throws Exception If failed.
*/
@Test
public void testConcurrentPutRemoveSameRow() throws Exception {
for (int i = 0; i < 100; i++) {
TestPairTree tree = prepareBPlusTree();
// Exact tree from the description is constructed at this point.
CyclicBarrier barrier = new CyclicBarrier(2);
// This is the replace operation.
IgniteInternalFuture<?> putFut = runAsync(() -> {
barrier.await();
tree.putx(new Pair(5, 999));
});
// This is the remove operation.
IgniteInternalFuture<?> remFut = runAsync(() -> {
barrier.await();
tree.removex(new Pair(5, 0));
});
// Wait for both operations.
try {
putFut.get(1, TimeUnit.SECONDS);
}
finally {
remFut.get(1, TimeUnit.SECONDS);
}
// Just in case.
tree.validateTree();
}
}
/**
* Creates and fills a tree:
* <pre><code>
* [ 5:0 ]
* / \
* [ 2:0 | 4:0 ] [ 6:0 ]
* / | \ / |
* [ 1:0 | 2:0 ]->[ 3:0 | 4:0 ]->[ 5:0 ]->[ 6:0 ]->[ 7:0 ]
* </code></pre>
*
* @return New B+tree.
* @throws Exception If failed.
*/
private TestPairTree prepareBPlusTree() throws Exception {
TestPairTree tree = new TestPairTree(
CACHE_ID,
pageMem,
allocateMetaPage().pageId(),
lockTrackerManager
);
tree.putx(new Pair(1, 0));
tree.putx(new Pair(2, 0));
tree.putx(new Pair(4, 0));
tree.putx(new Pair(6, 0));
tree.putx(new Pair(7, 0));
// Split root.
tree.putx(new Pair(5, 0));
// Split its left subtree.
tree.putx(new Pair(3, 0));
return tree;
}
}