package org.infinispan.server.test.cs.jdbc; import static org.infinispan.server.test.util.ITestUtils.createMBeans; import static org.infinispan.server.test.util.ITestUtils.createMemcachedClient; import static org.infinispan.server.test.util.ITestUtils.eventually; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.io.IOException; import java.util.Base64; import org.infinispan.arquillian.core.InfinispanResource; import org.infinispan.arquillian.core.RemoteInfinispanServer; import org.infinispan.arquillian.core.RunningServer; import org.infinispan.arquillian.core.WithRunningServer; import org.infinispan.client.hotrod.RemoteCache; import org.infinispan.commons.logging.Log; import org.infinispan.commons.logging.LogFactory; import org.infinispan.server.test.category.CacheStore; import org.infinispan.server.test.client.memcached.MemcachedClient; import org.infinispan.server.test.util.RemoteCacheManagerFactory; import org.infinispan.server.test.util.RemoteInfinispanMBeans; import org.infinispan.server.test.util.jdbc.DBServer; import org.jboss.arquillian.container.test.api.ContainerController; import org.jboss.arquillian.junit.Arquillian; import org.jboss.arquillian.test.api.ArquillianResource; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; /** * These are the tests for various JDBC stores (string, binary, mixed) with a single server. * We test each store with 2 configurations: * 1. passivation = true, preload = false * 2. passivation = false, preload = true * To speed things up, the tests use hotrod client so we can reuse a single server. * Test for the write-behind store uses memcached client. * Mixed store is not fully tested, because DefaultTwoWayKey2StringMapper (which does the decision string/binary) can handle * both string keys (memcached) and byte array keys (hotrod), which means all the keys go into the string store. * * @author <a href="mailto:mgencur@redhat.com">Martin Gencur</a> * @author <a href="mailto:vchepeli@redhat.com">Vitalii Chepeliuk</a> * @author <a href="mailto:jmarkos@redhat.com">Jakub Markos</a> */ @RunWith(Arquillian.class) @Category(CacheStore.class) public class SingleNodeJdbcStoreIT { public static final Log log = LogFactory.getLog(SingleNodeJdbcStoreIT.class); public final String CONTAINER = "jdbc"; @ArquillianResource protected ContainerController controller; @InfinispanResource(CONTAINER) protected RemoteInfinispanServer server; public final String ID_COLUMN_NAME = "id"; public final String DATA_COLUMN_NAME = "datum"; public static RemoteCacheManagerFactory rcmFactory; // WP = without passivation static DBServer stringDB, stringWPDB, stringAsyncDB; static RemoteInfinispanMBeans stringMBeans, stringWPMBeans; static RemoteCache stringCache, stringWPCache; @BeforeClass public static void startup() { rcmFactory = new RemoteCacheManagerFactory(); } @AfterClass public static void cleanup() { /** * We need to drop the tables, because of DB2 SQL Error: SQLCODE=-204, SQLSTATE=42704 */ DBServer[] dbservers = {stringDB, stringWPDB, stringAsyncDB}; for (DBServer dbServer : dbservers) { try { DBServer.TableManipulation bucketTable = dbServer.bucketTable; if (bucketTable != null) { if (bucketTable.getConnectionUrl().contains("db2")) { bucketTable.dropTable(); if (dbServer.stringTable != null) { dbServer.stringTable.dropTable(); } } } } catch (Exception e) { // catching the exception, because the drop is not part of the tests log.trace("Couldn't drop the tables: ", e); } } if (rcmFactory != null) { rcmFactory.stopManagers(); } rcmFactory = null; } @Before public void setUp() throws Exception { if (stringDB == null) { // initialize only once (can't do it in BeforeClass because no server is running at that time) stringMBeans = createMBeans(server, CONTAINER, "stringWithPassivation", "local"); stringCache = createCache(stringMBeans); stringDB = new DBServer(null, "STRING_WITH_PASSIVATION" + "_" + stringMBeans.cacheName, ID_COLUMN_NAME, DATA_COLUMN_NAME); stringWPMBeans = createMBeans(server, CONTAINER, "stringNoPassivation", "local"); stringWPCache = createCache(stringWPMBeans); stringWPDB = new DBServer(null, "STRING_NO_PASSIVATION" + "_" + stringWPMBeans.cacheName, ID_COLUMN_NAME, DATA_COLUMN_NAME); stringAsyncDB = new DBServer(null, "STRING_ASYNC" + "_" + "memcachedCache", ID_COLUMN_NAME, DATA_COLUMN_NAME); } } @Test @WithRunningServer({@RunningServer(name = CONTAINER)}) public void testNormalShutdown() throws Exception { // passivation = true, preload = false, purge = false, eviction.max-entries = 2 (LRU) testRestartStringStoreBefore(); // passivation = false, preload = true, purge = false testRestartStringStoreWPBefore(); controller.stop(CONTAINER); // normal shutdown - should store all entries from cache to store controller.start(CONTAINER); testRestartStringStoreAfter(false); testRestartStringStoreWPAfter(); } @Test @WithRunningServer({@RunningServer(name = CONTAINER)}) public void testForcedShutdown() throws Exception { // passivation = true, preload = false, purge = false, eviction.max-entries = 2 (LRU) testRestartStringStoreBefore(); // passivation = false, preload = true, purge = false testRestartStringStoreWPBefore(); controller.kill(CONTAINER); // (kill -9)-ing the server controller.start(CONTAINER); testRestartStringStoreAfter(true); testRestartStringStoreWPAfter(); } // simple test to see that write-behind store works @Test @WithRunningServer({@RunningServer(name = CONTAINER)}) public void testAsyncStringStore() throws Exception { MemcachedClient mc = createMemcachedClient(server); int numEntries = 1000; for (int i = 0; i != numEntries; i++) { mc.set("key" + i, "value" + i); } eventually(() -> stringAsyncDB.stringTable.exists(), 10000); for (int i = 0; i != numEntries; i++) { assertNotNull("key" + i + " was not found in DB in " + DBServer.TIMEOUT + " ms", stringAsyncDB.stringTable.getValueByKeyAwait("key" + i)); } for (int i = 0; i != numEntries; i++) { mc.delete("key" + i); } eventually(() -> stringAsyncDB.stringTable.getAllRows().isEmpty(), 10000); } public void testRestartStringStoreBefore() throws Exception { assertCleanCacheAndStoreHotrod(stringCache, stringDB.stringTable); stringCache.put("k1", "v1"); stringCache.put("k2", "v2"); boolean tableExists = stringDB.stringTable.exists(); if (tableExists) { assertNull(stringDB.stringTable.getValueByKey(getStoredKey(stringCache, "k1"))); assertNull(stringDB.stringTable.getValueByKey(getStoredKey(stringCache, "k2"))); } stringCache.put("k3", "v3"); //now a key would be evicted and stored in store assertTrue(2 >= server.getCacheManager(stringMBeans.managerName).getCache(stringMBeans.cacheName).getNumberOfEntriesInMemory()); if (tableExists) { assertEquals(1, stringDB.stringTable.getAllKeys().size()); } } public void testRestartStringStoreAfter(boolean killed) throws Exception { assertEquals(0, server.getCacheManager(stringMBeans.managerName).getCache(stringMBeans.cacheName).getNumberOfEntriesInMemory()); assertNotNull(stringDB.stringTable.getValueByKey(getStoredKey(stringCache, "k1"))); if (killed) { assertEquals(1, stringDB.stringTable.getAllRows().size()); assertEquals("v1", stringCache.get("k1")); // removed from store assertNull(stringDB.stringTable.getValueByKey(getStoredKey(stringCache, "k1"))); assertNull(stringCache.get("k2")); assertNull(stringCache.get("k3")); } else { assertEquals(3, stringDB.stringTable.getAllRows().size()); assertEquals("v1", stringCache.get("k1")); assertEquals("v2", stringCache.get("k2")); assertEquals("v3", stringCache.get("k3")); assertNull(stringDB.stringTable.getValueByKey(getStoredKey(stringCache, "k3"))); } } public void testRestartStringStoreWPBefore() throws Exception { assertCleanCacheAndStoreHotrod(stringWPCache, stringWPDB.stringTable); stringWPCache.put("k1", "v1"); stringWPCache.put("k2", "v2"); assertNotNull(stringWPDB.stringTable.getValueByKey(getStoredKey(stringWPCache, "k1"))); assertNotNull(stringWPDB.stringTable.getValueByKey(getStoredKey(stringWPCache, "k2"))); } public void testRestartStringStoreWPAfter() throws Exception { eventually(() -> { return 2 == server.getCacheManager(stringWPMBeans.managerName).getCache(stringWPMBeans.cacheName).getNumberOfEntries(); }, 10000); assertEquals("v1", stringWPCache.get("k1")); assertEquals("v2", stringWPCache.get("k2")); stringWPCache.remove("k1"); assertNull(stringWPDB.stringTable.getValueByKey(getStoredKey(stringWPCache, "k1"))); assertNotNull(stringWPDB.stringTable.getValueByKey(getStoredKey(stringWPCache, "k2"))); } public void assertCleanCacheAndStoreHotrod(RemoteCache cache, final DBServer.TableManipulation table) throws Exception { cache.clear(); if (table.exists() && !table.getAllRows().isEmpty()) { table.deleteAllRows(); eventually(() -> table.getAllRows().isEmpty(), 10000); } } // gets the database representation of the String key stored with hotrod client (when using string store) public String getStoredKey(RemoteCache cache, String key) throws IOException, InterruptedException { // 1. marshall the key // 2. encode it with base64 (that's what DefaultTwoWayKey2StringMapper does) // 3. prefix it with 8 (again, done by DefaultTwoWayKey2StringMapper to mark the key as wrapped byte array type) // 4. prefix it with UTF-16 BOM (that is what DefaultTwoWayKey2StringMapper does for non string values) return '\uFEFF' + "8" + Base64.getEncoder().encodeToString(cache.getRemoteCacheManager().getMarshaller().objectToByteBuffer(key)); } public RemoteCache<Object, Object> createCache(RemoteInfinispanMBeans mbeans) { return rcmFactory.createCache(mbeans); } }