/*-
* See the file LICENSE for redistribution information.
*
* Copyright (c) 2002, 2015 Oracle and/or its affiliates. All rights reserved.
*
*/
package com.sleepycat.persist.test;
import static com.sleepycat.persist.model.Relationship.MANY_TO_ONE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.List;
import java.util.concurrent.TimeUnit;
import com.sleepycat.compat.DbCompat;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import com.sleepycat.db.DatabaseException;
import com.sleepycat.db.DeadlockException;
import com.sleepycat.db.Transaction;
import com.sleepycat.persist.EntityCursor;
import com.sleepycat.persist.EntityIndex;
import com.sleepycat.persist.EntityStore;
import com.sleepycat.persist.PrimaryIndex;
import com.sleepycat.persist.SecondaryIndex;
import com.sleepycat.persist.StoreConfig;
import com.sleepycat.persist.model.Entity;
import com.sleepycat.persist.model.PrimaryKey;
import com.sleepycat.persist.model.SecondaryKey;
import com.sleepycat.util.test.TxnTestCase;
/**
* Tests that getLast restarts work correctly. See RangeCursor.getLast.
*
* This tests getLast via the DPL API simply because it's convenient. It could
* have been tested via the collections API, or directly using RangeCursor.
*
* @author Mark Hayes
*/
@RunWith(Parameterized.class)
public class GetLastRestartTest extends TxnTestCase {
private static final int N_ITERS = 5000;
@Entity
static class MyEntity {
@PrimaryKey
private int priKey;
@SecondaryKey(relate=MANY_TO_ONE)
private Integer secKey;
private MyEntity() {}
MyEntity(final int priKey, final Integer secKey) {
this.priKey = priKey;
this.secKey = secKey;
}
}
private EntityStore store;
private PrimaryIndex<Integer, MyEntity> priIndex;
private SecondaryIndex<Integer, Integer, MyEntity> secIndex;
private volatile Thread insertThread;
private volatile Exception insertException;
@Parameters
public static List<Object[]> genParams() {
/* TXN_NULL in DB doesn't support multi-threading. */
String[] txnTypes = new String[] {
TxnTestCase.TXN_USER,
TxnTestCase.TXN_AUTO,
TxnTestCase.TXN_CDB };
return getTxnParams(txnTypes, false);
}
public GetLastRestartTest(String type)
throws DatabaseException {
initEnvConfig();
DbCompat.enableDeadlockDetection(envConfig, type.equals(TXN_CDB));
/*
* Use large lock timeout because getLast is retrying in a loop while
* it holds a lock, since it doesn't release locks when it restarts the
* operation. This is a disadvantage of implementing getLast on top of
* the Cursor API, rather than in the cursor code. However, the looping
* is more of a problem in this test than it should be in the real
* life, because here we're tightly looping in another thread,
* inserting/deleting a single record.
*/
txnType = type;
isTransactional = (txnType != TXN_NULL);
customName = txnType;
}
private void open()
throws DatabaseException {
StoreConfig config = new StoreConfig();
config.setAllowCreate(envConfig.getAllowCreate());
config.setTransactional(envConfig.getTransactional());
store = new EntityStore(env, "test", config);
priIndex = store.getPrimaryIndex(Integer.class, MyEntity.class);
secIndex = store.getSecondaryIndex(priIndex, Integer.class, "secKey");
}
private void close()
throws DatabaseException {
try {
store.close();
} finally {
store = null;
priIndex = null;
secIndex = null;
}
}
@Override
@After
public void tearDown()
throws Exception {
final boolean stoppedInserts = stopInserts();
try {
if (store != null) {
close();
}
} catch (Throwable e) {
System.out.println("During tearDown: " + e);
}
super.tearDown();
if (!stoppedInserts) {
fail("Could not kill insert thread");
}
if (insertException != null) {
throw insertException;
}
}
/**
* Keys: 1, 2. Range: (-, 3) Expect: getLast == 2.
*
* RangeCursor.getLast calls getSearchKeyRange(3) which returns NOTFOUND.
* It calls getLast, but insertThread has inserted key 3, so getLast lands
* on key 3 which is outside the range. It must restart.
*/
@Test
public void testMainKeyRangeNoDups_GetLast()
throws DatabaseException, InterruptedException {
open();
insert(1);
insert(2);
startInserts(3);
checkRange(
3 /*endKey*/, false /*endInclusive*/,
2 /*expectLastKey*/);
assertTrue(stopInserts());
close();
}
/**
* Keys: 1, 2, 4. Range: (-, 3) Expect: getLast == 2.
*
* RangeCursor.getLast calls getSearchKeyRange(3) which lands on key 4. It
* calls getPrev, but insertThread has inserted key 3, so getPrev lands on
* key 3 which is outside the range. It must restart.
*/
@Test
public void testMainKeyRangeNoDups_GetPrev()
throws DatabaseException, InterruptedException {
open();
insert(1);
insert(2);
insert(4);
startInserts(3);
checkRange(
3 /*endKey*/, false /*endInclusive*/,
2 /*expectLastKey*/);
assertTrue(stopInserts());
close();
}
/**
* Records: 1/1, 2/2, 3/2. SecRange: [2, 2] Expect: getLast == 3/2.
*
* RangeCursor.getLast calls getSearchKeyRange(2) which returns 2/2. It
* calls getNextNoDup which returns NOTFOUND. It calls getLast, but
* insertThread has inserted key 4/3, so getLast lands on key 4/3 which is
* outside the range. It must restart.
*/
@Test
public void testMainKeyRangeWithDups_GetLast()
throws DatabaseException, InterruptedException {
open();
insert(1, 1);
insert(2, 2);
insert(3, 2);
startInserts(4, 3);
checkSecRange(
2 /*secKey*/,
3 /*expectLastPKey*/, 2 /*expectLastSecKey*/);
assertTrue(stopInserts());
close();
}
/**
* Records: 1/1, 2/2, 3/2, 4/4. SecRange: [2, 2] Expect: getLast == 3/2.
*
* RangeCursor.getLast calls getSearchKeyRange(2) which returns 2/2. It
* calls getNextNoDup which returns 4/4. It calls getPrev, but insertThread
* has inserted key 5/3, so getPrev lands on key 5/3 which is outside the
* range. It must restart.
*/
@Test
public void testMainKeyRangeWithDups_GetPrev()
throws DatabaseException, InterruptedException {
open();
insert(1, 1);
insert(2, 2);
insert(3, 2);
insert(4, 4);
startInserts(5, 3);
checkSecRange(
2 /*secKey*/,
3 /*expectLastPKey*/, 2 /*expectLastSecKey*/);
assertTrue(stopInserts());
close();
}
/**
* Records: 1/1, 2/2, 3/2. SecRange: [2, 2] PKeyRange: [2, -)
* Expect: getLast == 3/2.
*
* RangeCursor.getLast calls getSearchKey(2) which returns 2/2. It calls
* getNextNoDup which returns NOTFOUND. It calls getLast, but insertThread
* has inserted key 4/3, so getLast lands on key 4/3 which is outside the
* range. It must restart.
*/
@Test
public void testDupRangeNoEndKey_GetLast()
throws DatabaseException, InterruptedException {
open();
insert(1, 1);
insert(2, 2);
insert(3, 2);
startInserts(4, 3);
checkPKeyRange(
2 /*secKey*/,
2 /*beginPKey*/, true /*beginInclusive*/,
null /*endPKey*/, false /*endInclusive*/,
3 /*expectLastPKey*/, 2 /*expectLastSecKey*/);
assertTrue(stopInserts());
close();
}
/**
* Records: 1/1, 2/2, 3/2, 4/4. SecRange: [2, 2] PKeyRange: [2, -)
* Expect: getLast == 3/2.
*
* RangeCursor.getLast calls getSearchKey(2) which returns 2/2. It calls
* getNextNoDup which returns 4/4. It calls getPrev, but insertThread has
* inserted key 5/3, so getPrev lands on key 5/3 which is outside the
* range. It must restart.
*/
@Test
public void testDupRangeNoEndKey_GetPrev()
throws DatabaseException, InterruptedException {
open();
insert(1, 1);
insert(2, 2);
insert(3, 2);
insert(4, 4);
startInserts(5, 3);
checkPKeyRange(
2 /*secKey*/,
2 /*beginPKey*/, true /*beginInclusive*/,
null /*endPKey*/, false /*endInclusive*/,
3 /*expectLastPKey*/, 2 /*expectLastSecKey*/);
assertTrue(stopInserts());
close();
}
/**
* Records: 1/1, 2/2, 3/2, 5/2. SecRange: [2, 2] PKeyRange: (-, 4)
* Expect: getLast == 3/2.
*
* RangeCursor.getLast calls getSearchBothRange(2, 4) which returns 5/2. It
* calls getPrevDup, but insertThread has inserted key 4/2, so getPrevDup
* lands on key 4/2 which is outside the range. It must restart.
*/
@Test
public void testDupRangeWithEndKey()
throws DatabaseException, InterruptedException {
open();
insert(1, 1);
insert(2, 2);
insert(3, 2);
insert(5, 2);
startInserts(4, 2);
checkPKeyRange(
2 /*secKey*/,
null /*beginPKey*/, false /*beginInclusive*/,
4 /*endPKey*/, false /*endInclusive*/,
3 /*expectLastPKey*/, 2 /*expectLastSecKey*/);
assertTrue(stopInserts());
close();
}
private void checkRange(int endKey,
boolean endInclusive,
int expectLastKey)
throws DatabaseException {
for (int i = 0; i < N_ITERS; i += 1) {
final Transaction txn = txnBeginCursor();
final EntityCursor<MyEntity> c = priIndex.entities(
txn, null, false, endKey, endInclusive, null);
try {
final MyEntity e = c.last();
assertNotNull(e);
assertEquals(expectLastKey, e.priKey);
} finally {
c.close();
txnCommit(txn);
}
}
}
private void checkSecRange(int secKey,
int expectLastPKey,
Integer expectLastSecKey)
throws DatabaseException {
final EntityIndex<Integer, MyEntity> subIndex =
secIndex.subIndex(secKey);
for (int i = 0; i < N_ITERS; i += 1) {
final Transaction txn = txnBeginCursor();
final EntityCursor<MyEntity> c = subIndex.entities(txn, null);
try {
final MyEntity e = c.last();
assertNotNull(e);
assertEquals(expectLastPKey, e.priKey);
assertEquals(expectLastSecKey, e.secKey);
} finally {
c.close();
txnCommit(txn);
}
}
}
private void checkPKeyRange(int secKey,
Integer beginPKey,
boolean beginInclusive,
Integer endPKey,
boolean endInclusive,
int expectLastPKey,
Integer expectLastSecKey)
throws DatabaseException {
final EntityIndex<Integer, MyEntity> subIndex =
secIndex.subIndex(secKey);
for (int i = 0; i < N_ITERS; i += 1) {
final Transaction txn = txnBeginCursor();
final EntityCursor<MyEntity> c = subIndex.entities(
txn, beginPKey, beginInclusive, endPKey, endInclusive, null);
try {
final MyEntity e = c.last();
assertNotNull(e);
assertEquals(expectLastPKey, e.priKey);
assertEquals(expectLastSecKey, e.secKey);
} finally {
c.close();
txnCommit(txn);
}
}
}
private void startInserts(int priKey)
throws DatabaseException {
startInserts(priKey, null);
}
private void startInserts(final int priKey, final Integer secKey)
throws DatabaseException {
final EntityIndex<Integer, MyEntity> subIndex =
(secKey != null) ?
secIndex.subIndex(secKey) :
null;
insertThread = new Thread() {
@Override
public void run() {
try {
while (insertThread != null) {
try {
if (secKey != null) {
insert(priKey, secKey);
subIndex.delete(priKey);
} else {
insert(priKey);
priIndex.delete(priKey);
}
} catch (DeadlockException e) {
// ignore on DB, but not JE.
}
}
} catch (Exception e) {
insertException = e;
}
}
};
insertThread.start();
}
private boolean stopInserts()
throws InterruptedException {
if (insertThread == null) {
return true;
}
final Thread t = insertThread;
insertThread = null;
/*
* First try stopping without interrupts, since they will invalidate
* the Environment.
*/
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < 10 * 1000) {
if (!t.isAlive()) {
return true;
}
Thread.sleep(10);
}
/* Try thread interrupts as a last resort. */
start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < 10 * 1000) {
t.interrupt();
Thread.sleep(10);
if (!t.isAlive()) {
return true;
}
}
return false;
}
private void insert(int priKey)
throws DatabaseException {
insert(priKey, null);
}
private void insert(int priKey, Integer secKey)
throws DatabaseException {
priIndex.put(new MyEntity(priKey, secKey));
}
}