/* * 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.decorators; import org.infinispan.CacheException; import org.infinispan.container.entries.InternalCacheEntry; import org.infinispan.test.fwk.TestCacheManagerFactory; import org.infinispan.test.fwk.TestInternalCacheEntryFactory; import org.infinispan.loaders.CacheLoaderException; import org.infinispan.loaders.CacheStore; import org.infinispan.loaders.dummy.DummyInMemoryCacheStore; 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.test.AbstractInfinispanTest; import org.infinispan.test.TestingUtil; import org.infinispan.transaction.xa.GlobalTransaction; import org.infinispan.transaction.xa.TransactionFactory; import org.infinispan.util.logging.Log; import org.infinispan.util.logging.LogFactory; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import static org.infinispan.test.TestingUtil.k; import static org.infinispan.test.TestingUtil.v; @Test(groups = "unit", testName = "loaders.decorators.AsyncTest", sequential=true) public class AsyncTest extends AbstractInfinispanTest { private static final Log log = LogFactory.getLog(AsyncTest.class); AsyncStore store; ExecutorService asyncExecutor; DummyInMemoryCacheStore underlying; AsyncStoreConfig asyncConfig; DummyInMemoryCacheStore.Cfg dummyCfg; @BeforeMethod public void setUp() throws CacheLoaderException { underlying = new DummyInMemoryCacheStore(); asyncConfig = new AsyncStoreConfig().threadPoolSize(10); store = new AsyncStore(underlying, asyncConfig); dummyCfg = new DummyInMemoryCacheStore.Cfg().storeName(AsyncTest.class.getName()); store.init(dummyCfg, null, null); store.start(); asyncExecutor = (ExecutorService) TestingUtil.extractField(store, "executor"); } @AfterMethod public void tearDown() throws CacheLoaderException { if (store != null) store.stop(); } @Test(timeOut=10000) public void testPutRemove() throws Exception { TestCacheManagerFactory.backgroundTestStarted(this); final int number = 1000; String key = "testPutRemove-k-"; String value = "testPutRemove-v-"; doTestPut(number, key, value); doTestRemove(number, key); } @Test(timeOut=10000) public void testPutClearPut() throws Exception { TestCacheManagerFactory.backgroundTestStarted(this); final int number = 1000; String key = "testPutClearPut-k-"; String value = "testPutClearPut-v-"; doTestPut(number, key, value); doTestClear(number, key); value = "testPutClearPut-v[2]-"; doTestPut(number, key, value); doTestRemove(number, key); } @Test(timeOut=10000) public void testMultiplePutsOnSameKey() throws Exception { TestCacheManagerFactory.backgroundTestStarted(this); final int number = 1000; String key = "testMultiplePutsOnSameKey-k"; String value = "testMultiplePutsOnSameKey-v-"; doTestSameKeyPut(number, key, value); doTestSameKeyRemove(key); } @Test(timeOut=10000) public void testRestrictionOnAddingToAsyncQueue() throws Exception { TestCacheManagerFactory.backgroundTestStarted(this); store.remove("blah"); final int number = 10; String key = "testRestrictionOnAddingToAsyncQueue-k"; String value = "testRestrictionOnAddingToAsyncQueue-v-"; doTestPut(number, key, value); // stop the cache store store.stop(); try { store.remove("blah"); assert false : "Should have restricted this entry from being made"; } catch (CacheException expected) { } // clean up store.start(); doTestRemove(number, key); } public void testThreadSafetyWritingDiffValuesForKey(Method m) throws Exception { try { final String key = "k1"; final CountDownLatch v1Latch = new CountDownLatch(1); final CountDownLatch v2Latch = new CountDownLatch(1); final CountDownLatch endLatch = new CountDownLatch(1); DummyInMemoryCacheStore underlying = new DummyInMemoryCacheStore(); store = new MockAsyncStore(key, v1Latch, v2Latch, endLatch, underlying, asyncConfig); dummyCfg = new DummyInMemoryCacheStore.Cfg(); dummyCfg.setStoreName(m.getName()); store.init(dummyCfg, null, null); store.start(); store.store(TestInternalCacheEntryFactory.create(key, "v1")); v2Latch.await(); store.store(TestInternalCacheEntryFactory.create(key, "v2")); endLatch.await(); assert store.load(key).getValue().equals("v2"); } finally { store.delegate.clear(); store.stop(); store = null; } } public void testTransactionalModificationsHappenInDiffThread(Method m) throws Exception { try { final TransactionFactory gtf = new TransactionFactory(); gtf.init(false, false, true); final String k1 = k(m, 1), k2 = k(m, 2), v1 = v(m, 1), v2 = v(m, 2); final ConcurrentMap<Object, Modification> localMods = new ConcurrentHashMap<Object, Modification>(); final CyclicBarrier barrier = new CyclicBarrier(2); DummyInMemoryCacheStore underlying = new DummyInMemoryCacheStore(); store = new AsyncStore(underlying, asyncConfig) { @Override protected void applyModificationsSync(ConcurrentMap<Object, Modification> mods) throws CacheLoaderException { for (Map.Entry<Object, Modification> entry : mods.entrySet()) { localMods.put(entry.getKey(), entry.getValue()); } super.applyModificationsSync(mods); try { barrier.await(5, TimeUnit.SECONDS); } catch (TimeoutException e) { assert false : "Timed out applying for modifications"; } catch (Exception e) { throw new CacheLoaderException("Barrier failed", e); } } }; dummyCfg = new DummyInMemoryCacheStore.Cfg(); dummyCfg.setStoreName(m.getName()); store.init(dummyCfg, null, null); store.start(); 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)); GlobalTransaction tx = gtf.newGlobalTransaction(null, false); store.prepare(mods, tx, false); assert 0 == localMods.size(); assert !store.containsKey(k1); assert !store.containsKey(k2); store.commit(tx); barrier.await(5, TimeUnit.SECONDS); assert store.load(k2).getValue().equals(v2); assert !store.containsKey(k1); assert 2 == localMods.size(); assert new Remove(k1).equals(localMods.get(k1)); } finally { store.delegate.clear(); store.stop(); store = null; } } public void testTransactionalModificationsAreCoalesced(Method m) throws Exception { try { final TransactionFactory gtf = new TransactionFactory(); gtf.init(false, false, true); final String k1 = k(m, 1), k2 = k(m, 2), k3 = k(m, 3), v1 = v(m, 1), v2 = v(m, 2), v3 = v(m, 3); final AtomicInteger storeCount = new AtomicInteger(); final AtomicInteger removeCount = new AtomicInteger(); final AtomicInteger clearCount = new AtomicInteger(); final CyclicBarrier barrier = new CyclicBarrier(2); DummyInMemoryCacheStore underlying = new DummyInMemoryCacheStore() { @Override public void store(InternalCacheEntry ed) { super.store(ed); storeCount.getAndIncrement(); } @Override public boolean remove(Object key) { boolean ret = super.remove(key); removeCount.getAndIncrement(); return ret; } @Override public void clear() { super.clear(); clearCount.getAndIncrement(); } }; store = new AsyncStore(underlying, asyncConfig) { @Override protected void applyModificationsSync(ConcurrentMap<Object, Modification> mods) throws CacheLoaderException { super.applyModificationsSync(mods); try { barrier.await(5, TimeUnit.SECONDS); } catch (TimeoutException e) { assert false : "Timed out applying for modifications"; } catch (Exception e) { throw new CacheLoaderException("Barrier failed", e); } } }; dummyCfg = new DummyInMemoryCacheStore.Cfg(); dummyCfg.setStoreName(m.getName()); store.init(dummyCfg, null, null); store.start(); List<Modification> mods = new ArrayList<Modification>(); mods.add(new Store(TestInternalCacheEntryFactory.create(k1, v1))); mods.add(new Store(TestInternalCacheEntryFactory.create(k1, v2))); mods.add(new Store(TestInternalCacheEntryFactory.create(k2, v1))); mods.add(new Store(TestInternalCacheEntryFactory.create(k2, v2))); mods.add(new Remove(k1)); GlobalTransaction tx = gtf.newGlobalTransaction(null, false); store.prepare(mods, tx, false); Thread.sleep(200); //verify that work is not performed until commit assert 0 == storeCount.get(); assert 0 == removeCount.get(); assert 0 == clearCount.get(); store.commit(tx); barrier.await(5, TimeUnit.SECONDS); //modifications applied all at once assert 1 == storeCount.get() : "Store count was " + storeCount.get(); assert 1 == removeCount.get(); assert 0 == clearCount.get(); storeCount.set(0); removeCount.set(0); clearCount.set(0); mods = new ArrayList<Modification>(); mods.add(new Store(TestInternalCacheEntryFactory.create(k1, v1))); mods.add(new Remove(k1)); mods.add(new Clear()); mods.add(new Store(TestInternalCacheEntryFactory.create(k2, v2))); mods.add(new Remove(k2)); tx = gtf.newGlobalTransaction(null, false); store.prepare(mods, tx, false); Thread.sleep(200); //verify that work is not performed until commit assert 0 == storeCount.get(); assert 0 == removeCount.get(); assert 0 == clearCount.get(); store.commit(tx); barrier.await(5, TimeUnit.SECONDS); assert 0 == storeCount.get() : "Store count was " + storeCount.get(); assert 1 == removeCount.get(); assert 1 == clearCount.get(); storeCount.set(0); removeCount.set(0); clearCount.set(0); mods = new ArrayList<Modification>(); mods.add(new Store(TestInternalCacheEntryFactory.create(k1, v1))); mods.add(new Remove(k1)); mods.add(new Store(TestInternalCacheEntryFactory.create(k2, v2))); mods.add(new Remove(k2)); mods.add(new Store(TestInternalCacheEntryFactory.create(k3, v3))); tx = gtf.newGlobalTransaction(null, false); store.prepare(mods, tx, false); Thread.sleep(200); assert 0 == storeCount.get(); assert 0 == removeCount.get(); assert 0 == clearCount.get(); store.commit(tx); barrier.await(5, TimeUnit.SECONDS); assert 1 == storeCount.get() : "Store count was " + storeCount.get(); assert 2 == removeCount.get(); assert 0 == clearCount.get(); storeCount.set(0); removeCount.set(0); clearCount.set(0); mods = new ArrayList<Modification>(); mods.add(new Clear()); mods.add(new Remove(k1)); tx = gtf.newGlobalTransaction(null, false); store.prepare(mods, tx, false); Thread.sleep(200); assert 0 == storeCount.get(); assert 0 == removeCount.get(); assert 0 == clearCount.get(); store.commit(tx); barrier.await(5, TimeUnit.SECONDS); assert 0 == storeCount.get() : "Store count was " + storeCount.get(); assert 1 == removeCount.get(); assert 1 == clearCount.get(); storeCount.set(0); removeCount.set(0); clearCount.set(0); mods = new ArrayList<Modification>(); mods.add(new Clear()); mods.add(new Store(TestInternalCacheEntryFactory.create(k1, v1))); tx = gtf.newGlobalTransaction(null, false); store.prepare(mods, tx, false); Thread.sleep(200); assert 0 == storeCount.get(); assert 0 == removeCount.get(); assert 0 == clearCount.get(); store.commit(tx); barrier.await(5, TimeUnit.SECONDS); assert 1 == storeCount.get() : "Store count was " + storeCount.get(); assert 0 == removeCount.get(); assert 1 == clearCount.get(); } finally { store.delegate.clear(); store.stop(); store = null; } } private void doTestPut(int number, String key, String value) throws Exception { for (int i = 0; i < number; i++) { InternalCacheEntry cacheEntry = TestInternalCacheEntryFactory.create(key + i, value + i); store.store(cacheEntry); } store.stop(); store.start(); InternalCacheEntry[] entries = new InternalCacheEntry[number]; for (int i = 0; i < number; i++) { entries[i] = store.load(key + i); } for (int i = 0; i < number; i++) { InternalCacheEntry entry = entries[i]; if (entry != null) { assert entry.getValue().equals(value + i); } else { while (entry == null) { entry = store.load(key + i); if (entry != null) { assert entry.getValue().equals(value + i); } else { TestingUtil.sleepThreadInt(20, "still waiting for key to appear: " + key + i); } } } } } private void doTestSameKeyPut(int number, String key, String value) throws Exception { for (int i = 0; i < number; i++) { store.store(TestInternalCacheEntryFactory.create(key, value + i)); } store.stop(); store.start(); InternalCacheEntry entry; boolean success = false; for (int i = 0; i < 120; i++) { TestingUtil.sleepThreadInt(20, null); entry = store.load(key); success = entry.getValue().equals(value + (number - 1)); if (success) break; } assert success; } private void doTestRemove(int number, String key) throws Exception { for (int i = 0; i < number; i++) store.remove(key + i); store.stop();//makes sure the store is flushed store.start(); InternalCacheEntry[] entries = new InternalCacheEntry[number]; for (int i = 0; i < number; i++) { entries[i] = store.load(key + i); } for (int i = 0; i < number; i++) { InternalCacheEntry entry = entries[i]; while (entry != null) { TestingUtil.sleepThreadInt(20, "still waiting for key to be removed: " + key + i); entry = store.load(key + i); } } } private void doTestSameKeyRemove(String key) throws Exception { store.remove(key); InternalCacheEntry entry; do { TestingUtil.sleepThreadInt(20, "still waiting for key to be removed: " + key); entry = store.load(key); } while (entry != null); } private void doTestClear(int number, String key) throws Exception { store.clear(); store.stop(); store.start(); InternalCacheEntry[] entries = new InternalCacheEntry[number]; for (int i = 0; i < number; i++) { entries[i] = store.load(key + i); } for (int i = 0; i < number; i++) { InternalCacheEntry entry = entries[i]; while (entry != null) { TestingUtil.sleepThreadInt(20, "still waiting for key to be removed: " + key + i); entry = store.load(key + i); } } } static class MockAsyncStore extends AsyncStore { volatile boolean block = true; final CountDownLatch v1Latch; final CountDownLatch v2Latch; final CountDownLatch endLatch; final Object key; MockAsyncStore(Object key, CountDownLatch v1Latch, CountDownLatch v2Latch, CountDownLatch endLatch, CacheStore delegate, AsyncStoreConfig asyncStoreConfig) { super(delegate, asyncStoreConfig); this.v1Latch = v1Latch; this.v2Latch = v2Latch; this.endLatch = endLatch; this.key = key; } @Override protected void applyModificationsSync(ConcurrentMap<Object, Modification> mods) throws CacheLoaderException { if (mods.get(key) != null && block) { log.trace("Wait for v1 latch"); try { v2Latch.countDown(); block = false; v1Latch.await(2, TimeUnit.SECONDS); } catch (InterruptedException e) { } super.applyModificationsSync(mods); } else if (mods.get(key) != null && !block) { log.trace("Do v2 modification and unleash v1 latch"); super.applyModificationsSync(mods); v1Latch.countDown(); endLatch.countDown(); } } } }