/* * Copyright 2008-2009 LinkedIn, Inc * * Licensed 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 voldemort.store.bdb; import java.io.File; import java.io.FilenameFilter; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Random; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.io.FileDeleteStrategy; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import voldemort.TestUtils; import voldemort.server.protocol.admin.AsyncOperationStatus; import voldemort.server.storage.KeyLockHandle; import voldemort.store.AbstractStorageEngineTest; import voldemort.store.PersistenceFailureException; import voldemort.store.StorageEngine; import voldemort.utils.ByteArray; import voldemort.utils.ClosableIterator; import voldemort.utils.Pair; import voldemort.versioning.ObsoleteVersionException; import voldemort.versioning.VectorClock; import voldemort.versioning.Versioned; import com.google.common.collect.Lists; import com.sleepycat.je.Database; import com.sleepycat.je.DatabaseConfig; import com.sleepycat.je.Durability; import com.sleepycat.je.Environment; import com.sleepycat.je.EnvironmentConfig; import com.sleepycat.je.LockMode; import com.sleepycat.je.LockTimeoutException; /** * Tests the BDB storage engine. Note that this class uses junit4 style test * methods, though the base class extends TestCase junit 3 style * */ @RunWith(Parameterized.class) public class BdbStorageEngineTest extends AbstractStorageEngineTest { private static final LockMode LOCK_MODE = LockMode.DEFAULT; private Environment environment; private EnvironmentConfig envConfig; private Database database; private File tempDir; private BdbStorageEngine store; private DatabaseConfig databaseConfig; private BdbRuntimeConfig runtimeConfig; private boolean prefixPartitionId; public BdbStorageEngineTest(boolean prefixPartitionId) { this.prefixPartitionId = prefixPartitionId; } @Parameters public static Collection<Object[]> modes() { Object[][] data = new Object[][] { { true }, { false } }; return Arrays.asList(data); } @Override @Before public void setUp() throws Exception { super.setUp(); this.envConfig = new EnvironmentConfig(); this.envConfig.setDurability(Durability.COMMIT_NO_SYNC); this.envConfig.setAllowCreate(true); this.envConfig.setTransactional(true); this.tempDir = TestUtils.createTempDir(); this.environment = new Environment(this.tempDir, envConfig); this.databaseConfig = new DatabaseConfig(); databaseConfig.setAllowCreate(true); databaseConfig.setTransactional(true); databaseConfig.setSortedDuplicates(false); this.database = environment.openDatabase(null, "test", databaseConfig); this.runtimeConfig = new BdbRuntimeConfig(); runtimeConfig.setLockMode(LOCK_MODE); this.store = makeBdbStorageEngine("test", this.environment, this.database, runtimeConfig, this.prefixPartitionId); } protected static BdbStorageEngine makeBdbStorageEngine(String name, Environment environment, Database database, BdbRuntimeConfig config, boolean prefixPartitionId) { if(prefixPartitionId) { return new PartitionPrefixedBdbStorageEngine(name, environment, database, config, TestUtils.makeSingleNodeRoutingStrategy()); } else { return new BdbStorageEngine(name, environment, database, config); } } @Override @After public void tearDown() throws Exception { super.tearDown(); try { store.close(); environment.close(); } finally { FileDeleteStrategy.FORCE.delete(tempDir); } } @Override public StorageEngine<ByteArray, byte[], byte[]> getStorageEngine() { return store; } @Test public void testPersistence() throws Exception { this.store.put(new ByteArray("abc".getBytes()), new Versioned<byte[]>("cdef".getBytes()), null); this.store.close(); this.environment.close(); this.environment = new Environment(this.tempDir, envConfig); this.database = environment.openDatabase(null, "test", databaseConfig); this.store = makeBdbStorageEngine("test", this.environment, this.database, runtimeConfig, this.prefixPartitionId); List<Versioned<byte[]>> vals = store.get(new ByteArray("abc".getBytes()), null); assertEquals(1, vals.size()); TestUtils.bytesEqual("cdef".getBytes(), vals.get(0).getValue()); } @Test public void testEquals() { String name = "someName"; assertEquals(makeBdbStorageEngine(name, environment, database, runtimeConfig, this.prefixPartitionId), makeBdbStorageEngine(name, environment, database, runtimeConfig, this.prefixPartitionId)); } @Test public void testNullConstructorParameters() { try { makeBdbStorageEngine(null, environment, database, runtimeConfig, this.prefixPartitionId); } catch(IllegalArgumentException e) { return; } fail("No exception thrown for null name."); try { makeBdbStorageEngine("name", null, database, runtimeConfig, this.prefixPartitionId); } catch(IllegalArgumentException e) { return; } fail("No exception thrown for null environment."); try { makeBdbStorageEngine("name", environment, null, runtimeConfig, this.prefixPartitionId); } catch(IllegalArgumentException e) { return; } fail("No exception thrown for null database."); } @Test public void testConcurrentReadAndPut() throws Exception { ExecutorService executor = Executors.newFixedThreadPool(10); final CountDownLatch latch = new CountDownLatch(10); final AtomicBoolean returnedEmpty = new AtomicBoolean(false); final byte[] keyBytes = "foo".getBytes(); final byte[] valueBytes = "bar".getBytes(); store.put(new ByteArray(keyBytes), new Versioned<byte[]>(valueBytes), null); for(int i = 0; i < 10; i++) { executor.submit(new Runnable() { public void run() { try { for(int j = 0; j < 1000 && !returnedEmpty.get(); j++) { List<Versioned<byte[]>> vals = store.get(new ByteArray(keyBytes), null); if(vals.size() == 0 && j > 1) returnedEmpty.set(true); else { VectorClock v = (VectorClock) vals.get(0).getVersion(); v.incrementVersion(0, System.currentTimeMillis()); try { store.put(new ByteArray(keyBytes), new Versioned<byte[]>(valueBytes, v), null); } catch(ObsoleteVersionException e) { // Ignore these } } } } finally { latch.countDown(); } } }); } latch.await(); assertFalse("Should not have seen any empty results", returnedEmpty.get()); } @Test(timeout = 30000) public void testGetAndLock() throws Exception { final ByteArray key = new ByteArray("getAndLock".getBytes()); final byte[] valueBytes = "bar".getBytes(); store.put(key, new Versioned<byte[]>(valueBytes), null); KeyLockHandle<byte[]> handle = store.getAndLock(key); // get will block and timeout try { store.get(key, null); fail("get(..) should have blocked and timedout"); } catch(PersistenceFailureException pfe) { // expected assertTrue("Should have had a LockTimeoutException", pfe.getCause() instanceof LockTimeoutException); } // let go of the key lock store.releaseLock(handle); // get should not block, since the lock has been released List<Versioned<byte[]>> vals = store.get(key, null); assertEquals("Should read back the version previously written", 1, vals.size()); assertEquals("Should read back the version previously written", new ByteArray(valueBytes), new ByteArray(vals.get(0).getValue())); } @Test(timeout = 30000) public void testPutAndLock() throws Exception { final ByteArray key = new ByteArray("putAndLock".getBytes()); final byte[] valueBytes = "Lion".getBytes(); store.put(key, new Versioned<byte[]>(valueBytes), null); // begin the read-modify-write cycle KeyLockHandle<byte[]> handle = store.getAndLock(key); // put will block and timeout try { store.put(key, new Versioned<byte[]>("Mountain Lion".getBytes()), null); fail("put(..) should have blocked and timedout"); } catch(PersistenceFailureException pfe) { // expected assertTrue("Should have had a LockTimeoutException", pfe.getCause() instanceof LockTimeoutException); } // end the read-modify-write cycle handle.setValues(Lists.newArrayList(new Versioned<byte[]>("Mavericks".getBytes()))); store.putAndUnlock(key, handle); // get should not block, and read out Mavericks List<Versioned<byte[]>> vals = store.get(key, null); assertEquals("Exactly one version", 1, vals.size()); assertEquals("Should read back the version written by putAndUnlock", "Mavericks", new String(vals.get(0).getValue())); } @Test public void testSimultaneousIterationAndModification() throws Exception { // start a thread to do modifications ExecutorService executor = Executors.newFixedThreadPool(2); final Random rand = new Random(); final AtomicInteger count = new AtomicInteger(0); final AtomicBoolean keepRunning = new AtomicBoolean(true); executor.execute(new Runnable() { public void run() { while(keepRunning.get()) { byte[] bytes = Integer.toString(count.getAndIncrement()).getBytes(); store.put(new ByteArray(bytes), Versioned.value(bytes), null); count.incrementAndGet(); } } }); executor.execute(new Runnable() { public void run() { while(keepRunning.get()) { byte[] bytes = Integer.toString(rand.nextInt(count.get())).getBytes(); store.delete(new ByteArray(bytes), new VectorClock()); count.incrementAndGet(); } } }); // wait a bit while(count.get() < 300) continue; // now simultaneously do iteration ClosableIterator<Pair<ByteArray, Versioned<byte[]>>> iter = this.store.entries(); while(iter.hasNext()) iter.next(); iter.close(); keepRunning.set(false); executor.shutdown(); assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); } @Test public void testNativeBackup() throws Exception { File backupToDir = File.createTempFile("bdb-storage", "bkp"); backupToDir.delete(); backupToDir.mkdir(); try { store.nativeBackup(backupToDir, false, false, new AsyncOperationStatus(0, "dummy")); // Check that one file was copied assertArrayEquals(backupToDir.list(), new String[] { "00000000.jdb" }); long backupFileModified = backupToDir.listFiles()[0].lastModified(); store.nativeBackup(backupToDir, false, false, new AsyncOperationStatus(0, "dummy")); // Check that there are now two files, and the first one hasn't // changed String[] backedUp = backupToDir.list(); Arrays.sort(backedUp); assertArrayEquals(backedUp, new String[] { "00000000.jdb", "00000001.jdb" }); FilenameFilter filter = new FilenameFilter() { @Override public boolean accept(File dir, String name) { if(name.equals("00000000.jdb")) return true; return false; } }; assertEquals(backupFileModified, backupToDir.listFiles(filter)[0].lastModified()); } finally { deleteDir(backupToDir); } } private static void assertArrayEquals(Object[] expected, Object[] actual) { String error = Arrays.toString(expected) + " does not equal " + Arrays.toString(actual); assertEquals(error, expected.length, actual.length); for(int i = 0; i < expected.length; i++) { assertEquals(error, expected[i], actual[i]); } } private boolean deleteDir(File dir) { if(dir.isDirectory()) { for(File file: dir.listFiles()) { deleteDir(file); } } return dir.delete(); } }