/* * JBoss, Home of Professional Open Source * Copyright 2009 Red Hat Inc. and/or its affiliates and other * contributors as indicated by the @author tags. All rights reserved. * See the copyright.txt in the distribution for a full listing of * individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.infinispan.loaders.bdbje; import com.sleepycat.bind.EntryBinding; import com.sleepycat.bind.serial.SerialBinding; import com.sleepycat.bind.serial.StoredClassCatalog; import com.sleepycat.collections.CurrentTransaction; import com.sleepycat.collections.StoredMap; import com.sleepycat.collections.TransactionRunner; import com.sleepycat.collections.TransactionWorker; import com.sleepycat.je.Cursor; import com.sleepycat.je.Database; import com.sleepycat.je.DatabaseConfig; import com.sleepycat.je.DatabaseEntry; import com.sleepycat.je.DatabaseException; import com.sleepycat.je.Environment; import com.sleepycat.je.EnvironmentConfig; import com.sleepycat.je.OperationStatus; import org.infinispan.container.entries.InternalCacheEntry; import org.infinispan.test.fwk.TestInternalCacheEntryFactory; import org.infinispan.loaders.CacheLoaderException; import org.infinispan.loaders.modifications.Clear; import org.infinispan.loaders.modifications.Modification; import org.infinispan.loaders.modifications.Remove; import org.infinispan.loaders.modifications.Store; import org.infinispan.marshall.TestObjectStreamMarshaller; import org.infinispan.test.AbstractInfinispanTest; import org.infinispan.test.TestingUtil; import org.infinispan.util.logging.Log; import org.infinispan.util.logging.LogFactory; import org.testng.annotations.AfterMethod; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeMethod; import org.testng.annotations.BeforeTest; import org.testng.annotations.Optional; import org.testng.annotations.Parameters; import org.testng.annotations.Test; import javax.transaction.Transaction; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; import static org.mockito.Mockito.*; /** * Learning tests for SleepyCat JE. Behaviour here is used in BdbjeCacheLoader. When there are upgrades to bdbje, this * test may warrant updating. * * @author Adrian Cole * @since 4.0 */ @Test(groups = "unit", enabled = true, testName = "loaders.bdbje.BdbjeLearningTest") public class BdbjeLearningTest extends AbstractInfinispanTest { Environment env; private static final String CLASS_CATALOG = "java_class_catalog"; private StoredClassCatalog javaCatalog; private static final String STORED_ENTRIES = "storedEntriesDb"; private Database storedEntriesDb; private StoredMap<Object, InternalCacheEntry> cacheMap; private String tmpDirectory; @BeforeTest @Parameters("basedir") protected void setUpTempDir(@Optional("/tmp") String basedir) { tmpDirectory = TestingUtil.tmpDirectory(basedir, this); } @AfterTest protected void clearTempDir() { TestingUtil.recursiveFileRemove(tmpDirectory); new File(tmpDirectory).mkdirs(); } @BeforeMethod public void setUp() throws Exception { new File(tmpDirectory).mkdirs(); System.out.println("Opening environment in: " + tmpDirectory); EnvironmentConfig envConfig = new EnvironmentConfig(); envConfig.setTransactional(true); envConfig.setAllowCreate(true); env = new Environment(new File(tmpDirectory), envConfig); DatabaseConfig dbConfig = new DatabaseConfig(); dbConfig.setTransactional(true); dbConfig.setAllowCreate(true); Database catalogDb = env.openDatabase(null, CLASS_CATALOG, dbConfig); javaCatalog = new StoredClassCatalog(catalogDb); EntryBinding storedEntryKeyBinding = new SerialBinding(javaCatalog, Object.class); EntryBinding storedEntryValueBinding = new InternalCacheEntryBinding(new TestObjectStreamMarshaller()); storedEntriesDb = env.openDatabase(null, STORED_ENTRIES, dbConfig); cacheMap = new StoredMap<Object, InternalCacheEntry>(storedEntriesDb, storedEntryKeyBinding, storedEntryValueBinding, true); } public void testTransactionWorker() throws Exception { TransactionRunner runner = new TransactionRunner(env); runner.run(new PopulateDatabase()); runner.run(new PrintDatabase()); } private class PopulateDatabase implements TransactionWorker { @Override public void doWork() throws Exception { } } private class PrintDatabase implements TransactionWorker { @Override public void doWork() throws Exception { } } @AfterMethod public void tearDown() throws Exception { storedEntriesDb.close(); javaCatalog.close(); env.close(); TestingUtil.recursiveFileRemove(tmpDirectory); } private void store(InternalCacheEntry se) { cacheMap.put(se.getKey(), se); } private InternalCacheEntry load(Object key) { InternalCacheEntry s = cacheMap.get(key); if (s == null) return null; if (!s.isExpired()) return s; else cacheMap.remove(key); return null; } private Set loadAll() { return new HashSet(cacheMap.values()); } private void purgeExpired() { Iterator<Map.Entry<Object, InternalCacheEntry>> i = cacheMap.entrySet().iterator(); while (i.hasNext()) { if (i.next().getValue().isExpired()) i.remove(); } } private static final Log log = LogFactory.getLog(BdbjeLearningTest.class); private void toStream(OutputStream outputStream) throws CacheLoaderException { ObjectOutputStream oos = null; Cursor cursor = null; try { oos = (outputStream instanceof ObjectOutputStream) ? (ObjectOutputStream) outputStream : new ObjectOutputStream(outputStream); long recordCount = storedEntriesDb.count(); log.tracef("writing %s records to stream", recordCount); oos.writeLong(recordCount); cursor = storedEntriesDb.openCursor(null, null); DatabaseEntry key = new DatabaseEntry(); DatabaseEntry data = new DatabaseEntry(); while (cursor.getNext(key, data, null) == OperationStatus.SUCCESS) { oos.writeObject(key.getData()); oos.writeObject(data.getData()); } } catch (IOException e) { throw new CacheLoaderException("Error writing to object stream", e); } catch (DatabaseException e) { throw new CacheLoaderException("Error accessing database", e); } finally { if (cursor != null) try { cursor.close(); } catch (DatabaseException e) { throw new CacheLoaderException("Error closing cursor", e); } } } private void fromStream(InputStream inputStream) throws CacheLoaderException { ObjectInputStream ois = null; try { ois = (inputStream instanceof ObjectInputStream) ? (ObjectInputStream) inputStream : new ObjectInputStream(inputStream); long recordCount = ois.readLong(); log.infof("reading %s records from stream", recordCount); log.info("clearing all records"); cacheMap.clear(); Cursor cursor = null; com.sleepycat.je.Transaction txn = env.beginTransaction(null, null); try { cursor = storedEntriesDb.openCursor(txn, null); for (int i = 0; i < recordCount; i++) { byte[] keyBytes = (byte[]) ois.readObject(); byte[] dataBytes = (byte[]) ois.readObject(); DatabaseEntry key = new DatabaseEntry(keyBytes); DatabaseEntry data = new DatabaseEntry(dataBytes); cursor.put(key, data); } cursor.close(); cursor = null; txn.commit(); } finally { if (cursor != null) cursor.close(); } } catch (Exception e) { throw (e instanceof CacheLoaderException) ? (CacheLoaderException) e : new CacheLoaderException("Problems reading from stream", e); } } class StoreTransactionWorker implements TransactionWorker { StoreTransactionWorker(InternalCacheEntry entry) { this.entry = entry; } private InternalCacheEntry entry; @Override public void doWork() throws Exception { store(entry); } } class ClearTransactionWorker implements TransactionWorker { @Override public void doWork() throws Exception { cacheMap.clear(); } } class RemoveTransactionWorker implements TransactionWorker { RemoveTransactionWorker(Object key) { this.key = key; } Object key; @Override public void doWork() throws Exception { cacheMap.remove(key); } } class PurgeExpiredTransactionWorker implements TransactionWorker { @Override public void doWork() throws Exception { purgeExpired(); } } class ModificationsTransactionWorker implements TransactionWorker { private List<? extends Modification> mods; ModificationsTransactionWorker(List<? extends Modification> mods) { this.mods = mods; } @Override public void doWork() throws Exception { for (Modification modification : mods) switch (modification.getType()) { case STORE: Store s = (Store) modification; store(s.getStoredEntry()); break; case CLEAR: cacheMap.clear(); break; case REMOVE: Remove r = (Remove) modification; cacheMap.remove(r.getKey()); break; case PURGE_EXPIRED: purgeExpired(); break; default: throw new IllegalArgumentException("Unknown modification type " + modification.getType()); } } } private void prepare(List<Modification> mods, Transaction tx, boolean isOnePhase) throws CacheLoaderException { if (isOnePhase) { TransactionRunner runner = new TransactionRunner(env); try { runner.run(new ModificationsTransactionWorker(mods)); } catch (Exception e) { e.printStackTrace(); } } else { PreparableTransactionRunner runner = new PreparableTransactionRunner(env); com.sleepycat.je.Transaction txn = null; try { runner.prepare(new ModificationsTransactionWorker(mods)); txn = CurrentTransaction.getInstance(env).getTransaction(); txnMap.put(tx, txn); } catch (Exception e) { e.printStackTrace(); } } } Map<Transaction, com.sleepycat.je.Transaction> txnMap = new HashMap<Transaction, com.sleepycat.je.Transaction>(); private void commit(Transaction tx) { com.sleepycat.je.Transaction txn = txnMap.remove(tx); CurrentTransaction currentTransaction = CurrentTransaction.getInstance(env); if (txn != null) { if (currentTransaction.getTransaction() == txn) { try { currentTransaction.commitTransaction(); } catch (DatabaseException e) { e.printStackTrace(); } } else { log.error("Transactions must be committed on the same thread"); } } } private void rollback(Transaction tx) { com.sleepycat.je.Transaction txn = txnMap.remove(tx); CurrentTransaction currentTransaction = CurrentTransaction.getInstance(env); if (txn != null) { if (currentTransaction.getTransaction() == txn) { try { currentTransaction.abortTransaction(); } catch (DatabaseException e) { e.printStackTrace(); } } else { log.error("Transactions must be committed on the same thread"); } } } public void testLoadAndStore() throws InterruptedException, CacheLoaderException { assert !cacheMap.containsKey("k"); InternalCacheEntry se = TestInternalCacheEntryFactory.create("k", "v"); store(se); assert load("k").getValue().equals("v"); assert load("k").getLifespan() == -1; assert !load("k").isExpired(); assert cacheMap.containsKey("k"); long lifespan = 120000; se = TestInternalCacheEntryFactory.create("k", "v", lifespan); store(se); assert load("k").getValue().equals("v"); assert load("k").getLifespan() == lifespan; assert !load("k").isExpired(); assert cacheMap.containsKey("k"); lifespan = 1; se = TestInternalCacheEntryFactory.create("k", "v", lifespan); store(se); Thread.sleep(100); assert se.isExpired(); assert load("k") == null; assert !cacheMap.containsKey("k"); } public void testOnePhaseCommit() throws CacheLoaderException { List<Modification> mods = new ArrayList<Modification>(); mods.add(new Store(TestInternalCacheEntryFactory.create("k1", "v1"))); mods.add(new Store(TestInternalCacheEntryFactory.create("k2", "v2"))); mods.add(new Remove("k1")); Transaction tx = mock(Transaction.class); prepare(mods, tx, true); Set s = loadAll(); assert load("k2").getValue().equals("v2"); assert !cacheMap.containsKey("k1"); cacheMap.clear(); mods = new ArrayList<Modification>(); mods.add(new Store(TestInternalCacheEntryFactory.create("k1", "v1"))); mods.add(new Store(TestInternalCacheEntryFactory.create("k2", "v2"))); mods.add(new Clear()); mods.add(new Store(TestInternalCacheEntryFactory.create("k3", "v3"))); prepare(mods, tx, true); assert !cacheMap.containsKey("k1"); assert !cacheMap.containsKey("k2"); assert cacheMap.containsKey("k3"); } public void testTwoPhaseCommit() throws Throwable { final List<Throwable> throwables = new ArrayList<Throwable>(); List<Modification> mods = new ArrayList<Modification>(); mods.add(new Store(TestInternalCacheEntryFactory.create("k1", "v1"))); mods.add(new Store(TestInternalCacheEntryFactory.create("k2", "v2"))); mods.add(new Remove("k1")); Transaction tx = mock(Transaction.class); prepare(mods, tx, false); Thread gets1 = new Thread( new Runnable() { @Override public void run() { try { assert load("k2").getValue().equals("v2"); assert !cacheMap.containsKey("k1"); } catch (Throwable e) { throwables.add(e); } } } ); gets1.start(); commit(tx); gets1.join(); if (!throwables.isEmpty()) throw throwables.get(0); cacheMap.clear(); mods = new ArrayList<Modification>(); mods.add(new Store(TestInternalCacheEntryFactory.create("k1", "v1"))); mods.add(new Store(TestInternalCacheEntryFactory.create("k2", "v2"))); mods.add(new Clear()); mods.add(new Store(TestInternalCacheEntryFactory.create("k3", "v3"))); prepare(mods, tx, false); Thread gets2 = new Thread( new Runnable() { @Override public void run() { try { assert !cacheMap.containsKey("k1"); assert !cacheMap.containsKey("k2"); assert cacheMap.containsKey("k3"); } catch (Throwable e) { throwables.add(e); } } } ); gets2.start(); commit(tx); gets2.join(); if (!throwables.isEmpty()) throw throwables.get(0); assert !cacheMap.containsKey("k1"); assert !cacheMap.containsKey("k2"); assert cacheMap.containsKey("k3"); } public void testRollback() throws Throwable { store(TestInternalCacheEntryFactory.create("old", "old")); List<Modification> mods = new ArrayList<Modification>(); mods.add(new Store(TestInternalCacheEntryFactory.create("k1", "v1"))); mods.add(new Store(TestInternalCacheEntryFactory.create("k2", "v2"))); mods.add(new Remove("k1")); mods.add(new Remove("old")); Transaction tx = mock(Transaction.class); prepare(mods, tx, false); rollback(tx); assert !cacheMap.containsKey("k1"); assert !cacheMap.containsKey("k2"); assert cacheMap.containsKey("old"); mods = new ArrayList<Modification>(); mods.add(new Store(TestInternalCacheEntryFactory.create("k1", "v1"))); mods.add(new Store(TestInternalCacheEntryFactory.create("k2", "v2"))); mods.add(new Clear()); mods.add(new Store(TestInternalCacheEntryFactory.create("k3", "v3"))); prepare(mods, tx, false); rollback(tx); assert !cacheMap.containsKey("k1"); assert !cacheMap.containsKey("k2"); assert !cacheMap.containsKey("k3"); assert cacheMap.containsKey("old"); } public void testCommitAndRollbackWithoutPrepare() throws CacheLoaderException { store(TestInternalCacheEntryFactory.create("old", "old")); Transaction tx = mock(Transaction.class); commit(tx); store(TestInternalCacheEntryFactory.create("old", "old")); rollback(tx); assert cacheMap.containsKey("old"); } public void testPreload() throws CacheLoaderException { store(TestInternalCacheEntryFactory.create("k1", "v1")); store(TestInternalCacheEntryFactory.create("k2", "v2")); store(TestInternalCacheEntryFactory.create("k3", "v3")); Set<InternalCacheEntry> set = loadAll(); assert set.size() == 3; Set expected = new HashSet(); expected.add("k1"); expected.add("k2"); expected.add("k3"); for (InternalCacheEntry se : set) assert expected.remove(se.getKey()); assert expected.isEmpty(); } public void testPurgeExpired() throws Exception { long now = System.currentTimeMillis(); long lifespan = 1000; store(TestInternalCacheEntryFactory.create("k1", "v1", lifespan)); store(TestInternalCacheEntryFactory.create("k2", "v2", lifespan)); store(TestInternalCacheEntryFactory.create("k3", "v3", lifespan)); assert cacheMap.containsKey("k1"); assert cacheMap.containsKey("k2"); assert cacheMap.containsKey("k3"); Thread.sleep(lifespan + 100); purgeExpired(); assert !cacheMap.containsKey("k1"); assert !cacheMap.containsKey("k2"); assert !cacheMap.containsKey("k3"); } public void testStreamingAPI() throws IOException, ClassNotFoundException, CacheLoaderException { store(TestInternalCacheEntryFactory.create("k1", "v1")); store(TestInternalCacheEntryFactory.create("k2", "v2")); store(TestInternalCacheEntryFactory.create("k3", "v3")); ByteArrayOutputStream out = new ByteArrayOutputStream(); toStream(out); out.close(); cacheMap.clear(); fromStream(new ByteArrayInputStream(out.toByteArray())); Set<InternalCacheEntry> set = loadAll(); assert set.size() == 3; Set expected = new HashSet(); expected.add("k1"); expected.add("k2"); expected.add("k3"); for (InternalCacheEntry se : set) assert expected.remove(se.getKey()); assert expected.isEmpty(); } public void testStreamingAPIReusingStreams() throws IOException, ClassNotFoundException, CacheLoaderException { store(TestInternalCacheEntryFactory.create("k1", "v1")); store(TestInternalCacheEntryFactory.create("k2", "v2")); store(TestInternalCacheEntryFactory.create("k3", "v3")); ByteArrayOutputStream out = new ByteArrayOutputStream(); byte[] dummyStartBytes = {1, 2, 3, 4, 5, 6, 7, 8}; byte[] dummyEndBytes = {8, 7, 6, 5, 4, 3, 2, 1}; out.write(dummyStartBytes); toStream(out); out.write(dummyEndBytes); out.close(); cacheMap.clear(); // first pop the start bytes byte[] dummy = new byte[8]; ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); int bytesRead = in.read(dummy, 0, 8); assert bytesRead == 8; for (int i = 1; i < 9; i++) assert dummy[i - 1] == i : "Start byte stream corrupted!"; fromStream(in); bytesRead = in.read(dummy, 0, 8); assert bytesRead == 8; for (int i = 8; i > 0; i--) assert dummy[8 - i] == i : "Start byte stream corrupted!"; Set<InternalCacheEntry> set = loadAll(); assert set.size() == 3; Set expected = new HashSet(); expected.add("k1"); expected.add("k2"); expected.add("k3"); for (InternalCacheEntry se : set) assert expected.remove(se.getKey()); assert expected.isEmpty(); } @Test(enabled = false) public void testConcurrency() throws Throwable { int numThreads = 3; final int loops = 500; final String[] keys = new String[10]; final String[] values = new String[10]; for (int i = 0; i < 10; i++) keys[i] = "k" + i; for (int i = 0; i < 10; i++) values[i] = "v" + i; final Random r = new Random(); final List<Throwable> throwables = new LinkedList<Throwable>(); final Runnable store = new Runnable() { @Override public void run() { try { int randomInt = r.nextInt(10); store(TestInternalCacheEntryFactory.create(keys[randomInt], values[randomInt])); } catch (Throwable e) { throwables.add(e); } } }; final Runnable remove = new Runnable() { @Override public void run() { try { cacheMap.remove(keys[r.nextInt(10)]); } catch (Throwable e) { throwables.add(e); } } }; final Runnable get = new Runnable() { @Override public void run() { try { int randomInt = r.nextInt(10); InternalCacheEntry se = load(keys[randomInt]); assert se == null || se.getValue().equals(values[randomInt]); loadAll(); } catch (Throwable e) { throwables.add(e); } } }; Thread[] threads = new Thread[numThreads]; for (int i = 0; i < numThreads; i++) { threads[i] = new Thread(getClass().getSimpleName() + "-" + i) { @Override public void run() { for (int i = 0; i < loops; i++) { store.run(); remove.run(); get.run(); } } }; } for (Thread t : threads) t.start(); for (Thread t : threads) t.join(); if (!throwables.isEmpty()) throw throwables.get(0); } }