Java tutorial
/* * Copyright 2013 Stanley Shyiko * * 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 com.github.shyiko.rook.it.h4com; import com.github.shyiko.mysql.binlog.BinaryLogClient; import com.github.shyiko.mysql.binlog.event.Event; import com.github.shyiko.mysql.binlog.event.EventType; import com.github.shyiko.mysql.binlog.event.QueryEventData; import com.github.shyiko.rook.api.ReplicationEventListener; import com.github.shyiko.rook.api.event.DeleteRowsReplicationEvent; import com.github.shyiko.rook.api.event.InsertRowsReplicationEvent; import com.github.shyiko.rook.api.event.ReplicationEvent; import com.github.shyiko.rook.api.event.UpdateRowsReplicationEvent; import com.github.shyiko.rook.it.h4com.model.IgnoredEntity; import com.github.shyiko.rook.it.h4com.model.OneToManyEntity; import com.github.shyiko.rook.it.h4com.model.OneToOneEntity; import com.github.shyiko.rook.it.h4com.model.RootEntity; import com.github.shyiko.rook.it.h4com.model.CompositeKeyEntity; import com.github.shyiko.rook.source.mysql.MySQLReplicationStream; import com.github.shyiko.rook.target.hibernate4.cache.HibernateCacheSynchronizer; import com.github.shyiko.rook.target.hibernate4.cache.QueryCacheSynchronizer; import com.github.shyiko.rook.target.hibernate4.cache.SecondLevelCacheSynchronizer; import com.github.shyiko.rook.target.hibernate4.cache.SynchronizationContext; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.cfg.Configuration; import org.hibernate.service.ServiceRegistry; import org.hibernate.service.ServiceRegistryBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.annotations.*; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.net.URI; import java.sql.Connection; import java.sql.DriverManager; import java.sql.Statement; import java.util.*; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; /** * @author <a href="mailto:stanley.shyiko@gmail.com">Stanley Shyiko</a> */ public class IntegrationTest { private static final long DEFAULT_TIMEOUT = TimeUnit.SECONDS.toMillis(3); private final Logger logger = LoggerFactory.getLogger(IntegrationTest.class); private MySQLReplicationStream replicationStream; @BeforeClass public void setUp() throws Exception { ResourceBundle bundle = ResourceBundle.getBundle("slave"); String dsURL = bundle.getString("hibernate.connection.url"); URI uri = new URI("schema" + dsURL.substring(dsURL.indexOf("://"))); final CountDownLatch latchForCreateDatabase = new CountDownLatch(2); final AtomicReference<BinaryLogClient> binaryLogClientRef = new AtomicReference<BinaryLogClient>(); final BinaryLogClient.EventListener eventListener = new BinaryLogClient.EventListener() { @Override public void onEvent(Event event) { if (event.getHeader().getEventType() == EventType.QUERY && ((QueryEventData) event.getData()).getSql().toLowerCase().contains("create database")) { latchForCreateDatabase.countDown(); } } }; replicationStream = new MySQLReplicationStream(uri.getHost(), uri.getPort(), bundle.getString("hibernate.connection.username"), bundle.getString("hibernate.connection.password")) { @Override protected void configureBinaryLogClient(final BinaryLogClient binaryLogClient) { binaryLogClientRef.set(binaryLogClient); binaryLogClient.registerEventListener(eventListener); } }; replicationStream.connect(DEFAULT_TIMEOUT); Class.forName("com.mysql.jdbc.Driver"); recreateDatabaseForProfiles("master", "master-sdb"); assertTrue(latchForCreateDatabase.await(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS)); binaryLogClientRef.get().unregisterEventListener(eventListener); } private void recreateDatabaseForProfiles(String... profiles) throws Exception { for (String profile : profiles) { ResourceBundle bundle = ResourceBundle.getBundle(profile); recreateDatabase(bundle.getString("hibernate.connection.url"), bundle.getString("hibernate.connection.username"), bundle.getString("hibernate.connection.password")); } } private void recreateDatabase(String connectionURI, String username, String password) throws Exception { int delimiter = connectionURI.lastIndexOf("/"); Connection connection = DriverManager.getConnection(connectionURI.substring(0, delimiter), username, password); try { Statement statement = connection.createStatement(); try { String databaseName = connectionURI.substring(delimiter + 1); statement.execute("drop database if exists " + databaseName); statement.execute("create database " + databaseName); } finally { statement.close(); } } finally { connection.close(); } } @BeforeMethod public void beforeTest() { replicationStream.registerListener(new ReplicationEventListener() { @Override public void onEvent(ReplicationEvent event) { if (logger.isDebugEnabled()) { logger.debug("Received " + event); } } }); } @Test public void testQueryCacheIsNotEvictedWithoutQCS() throws Exception { testQueryCacheEviction(false, null, 3); } @Test public void testQueryCacheIsEvictedByQCS() throws Exception { testQueryCacheEviction(true, null, 3); } @Test public void testQueryCacheIsNotEvictedWithoutQCSWithIgnoredTable() throws Exception { testQueryCacheEviction(false, IgnoredEntity.class.getSimpleName(), 2); } @Test public void testQueryCacheIsEvictedByQCSWithIgnoredTable() throws Exception { testQueryCacheEviction(true, IgnoredEntity.class.getSimpleName(), 2); } private void testQueryCacheEviction(final boolean enableQCS, final String ignoredTable, int expectedEvents) throws Exception { ExecutionContext masterContext = ExecutionContextHolder.get("master"); ExecutionContext slaveContext = ExecutionContextHolder.get("slave"); HashSet<String> ignoredTables = new HashSet<String>() { { add(ignoredTable); } }; replicationStream.setIgnoredTables(ignoredTables); if (enableQCS) { replicationStream.registerListener(new QueryCacheSynchronizer( new SynchronizationContext(slaveContext.getConfiguration(), slaveContext.getSessionFactory()))); } CountDownReplicationListener countDownReplicationListener = new CountDownReplicationListener(); replicationStream.registerListener(countDownReplicationListener); slaveContext.execute(new Callback<Session>() { @Override public void execute(Session session) { assertEquals(session.createQuery("from RootEntity").setCacheable(true).list().size(), 0); assertEquals(session.createQuery("from IgnoredEntity").setCacheable(true).list().size(), 0); assertEquals(session.createQuery("from CompositeKeyEntity").setCacheable(true).list().size(), 0); } }); masterContext.execute(new Callback<Session>() { @Override public void execute(Session session) { final RootEntity rootEntity = new RootEntity("Slytherin"); session.persist(rootEntity); final IgnoredEntity ignoredEntity = new IgnoredEntity("Gryffindor"); session.persist(ignoredEntity); session.persist(new CompositeKeyEntity(rootEntity.getId(), ignoredEntity.getId())); } }); countDownReplicationListener.waitFor(InsertRowsReplicationEvent.class, expectedEvents, DEFAULT_TIMEOUT); slaveContext.execute(new Callback<Session>() { @Override public void execute(Session session) { assertEquals(session.createQuery("from RootEntity").setCacheable(true).list().size(), enableQCS ? 1 : 0); assertEquals(session.createQuery("from CompositeKeyEntity").setCacheable(true).list().size(), enableQCS ? 1 : 0); assertEquals(session.createQuery("from IgnoredEntity").setCacheable(true).list().size(), (enableQCS && (ignoredTable == null)) ? 1 : 0); } }); } @Test public void testSecondLevelCacheIsNotEvictedWithoutSLCS() throws Exception { testSecondLevelCacheEviction(false); } @Test public void testSecondLevelCacheIsEvictedBySLCS() throws Exception { testSecondLevelCacheEviction(true); } private void testSecondLevelCacheEviction(final boolean enableSLCS) throws Exception { ExecutionContext masterContext = ExecutionContextHolder.get("master"); ExecutionContext slaveContext = ExecutionContextHolder.get("slave"); if (enableSLCS) { replicationStream.registerListener(new SecondLevelCacheSynchronizer( new SynchronizationContext(slaveContext.getConfiguration(), slaveContext.getSessionFactory()))); } CountDownReplicationListener countDownReplicationListener = new CountDownReplicationListener(); replicationStream.registerListener(countDownReplicationListener); final AtomicReference<Serializable> rootEntityId = new AtomicReference<Serializable>(), ignoredEntityId = new AtomicReference<Serializable>(); masterContext.execute(new Callback<Session>() { @Override public void execute(Session session) { rootEntityId.set(session.save(new RootEntity("Slytherin", new OneToOneEntity("Severus Snape"), new HashSet<OneToManyEntity>(Arrays.asList(new OneToManyEntity("Draco Malfoy"), new OneToManyEntity("Vincent Crabbe"), new OneToManyEntity("Gregory Goyle")))))); ignoredEntityId.set(session.save(new IgnoredEntity("Hufflepuff"))); session.persist(new CompositeKeyEntity((Long) rootEntityId.get(), (Long) ignoredEntityId.get())); } }); slaveContext.execute(new Callback<Session>() { @Override public void execute(Session session) { RootEntity rootEntity = (RootEntity) session.get(RootEntity.class, rootEntityId.get()); assertEquals(rootEntity.getName(), "Slytherin"); assertEquals(rootEntity.getCompositeRelations().size(), 1); } }); masterContext.execute(new Callback<Session>() { @Override public void execute(Session session) { RootEntity rootEntity = (RootEntity) session.get(RootEntity.class, rootEntityId.get()); rootEntity.setName("Slytherin House"); rootEntity.getCompositeRelations().clear(); session.merge(rootEntity); } }); countDownReplicationListener.waitFor(UpdateRowsReplicationEvent.class, 1, DEFAULT_TIMEOUT); countDownReplicationListener.waitFor(DeleteRowsReplicationEvent.class, 1, DEFAULT_TIMEOUT); slaveContext.execute(new Callback<Session>() { @Override public void execute(Session session) { RootEntity rootEntity = (RootEntity) session.get(RootEntity.class, rootEntityId.get()); assertEquals(rootEntity.getName(), enableSLCS ? "Slytherin House" : "Slytherin"); assertEquals(rootEntity.getCompositeRelations().size(), enableSLCS ? 0 : 1); } }); masterContext.execute(new Callback<Session>() { @Override public void execute(Session session) { session.delete(session.get(RootEntity.class, rootEntityId.get())); } }); countDownReplicationListener.waitFor(DeleteRowsReplicationEvent.class, 2, DEFAULT_TIMEOUT); slaveContext.execute(new Callback<Session>() { @Override public void execute(Session session) { RootEntity rootEntity = (RootEntity) session.get(RootEntity.class, rootEntityId.get()); assertTrue(enableSLCS == (rootEntity == null)); } }); } @Test public void testOnlyWiredInDatabaseIsAffected() throws Exception { class ReplicationContext { private ExecutionContext master, slave; ReplicationContext(ExecutionContext master, ExecutionContext slave) { this.master = master; this.slave = slave; } } final ReplicationContext primaryContext = new ReplicationContext(ExecutionContextHolder.get("master"), ExecutionContextHolder.get("slave")), separateContext = new ReplicationContext(ExecutionContextHolder.get("master-sdb"), ExecutionContextHolder.get("slave-sdb")); replicationStream.registerListener(new HibernateCacheSynchronizer(primaryContext.slave.getConfiguration(), primaryContext.slave.getSessionFactory())); CountDownReplicationListener countDownReplicationListener = new CountDownReplicationListener(); replicationStream.registerListener(countDownReplicationListener); for (ReplicationContext context : new ReplicationContext[] { primaryContext, separateContext }) { context.slave.execute(new Callback<Session>() { @Override public void execute(Session session) { assertEquals(session.createQuery("from RootEntity").setCacheable(true).list().size(), 0); } }); } for (final ReplicationContext group : new ReplicationContext[] { primaryContext, separateContext }) { group.master.execute(new Callback<Session>() { @Override public void execute(Session session) { session.save(new RootEntity("Slytherin")); } }); } countDownReplicationListener.waitFor(InsertRowsReplicationEvent.class, 2, DEFAULT_TIMEOUT); primaryContext.slave.execute(new Callback<Session>() { @Override public void execute(Session session) { assertEquals(session.createQuery("from RootEntity").setCacheable(true).list().size(), 1); } }); separateContext.slave.execute(new Callback<Session>() { @Override public void execute(Session session) { assertEquals(session.createQuery("from RootEntity").setCacheable(true).list().size(), 0); } }); } @Test public void testReplicationEventsComeGroupedByStatement() throws Exception { CountDownReplicationListener countDownReplicationListener = new CountDownReplicationListener(); replicationStream.registerListener(countDownReplicationListener); ExecutionContext masterContext = ExecutionContextHolder.get("master"); masterContext.execute(new Callback<Session>() { @Override public void execute(Session session) { session.persist(new RootEntity("Slytherin")); session.persist(new RootEntity("Hufflepuff")); session.persist(new RootEntity("Ravenclaw")); } }); countDownReplicationListener.waitFor(InsertRowsReplicationEvent.class, 3, DEFAULT_TIMEOUT); masterContext.execute(new Callback<Session>() { @Override public void execute(Session session) { session.createQuery("update RootEntity set name = '~'").executeUpdate(); } }); masterContext.execute(new Callback<Session>() { @Override public void execute(Session session) { session.createQuery("delete RootEntity where name = 'Ravenclaw'").executeUpdate(); } }); countDownReplicationListener.waitFor(UpdateRowsReplicationEvent.class, 1, DEFAULT_TIMEOUT); } @AfterMethod(alwaysRun = true) public void afterTest() throws Exception { replicationStream.unregisterListener(ReplicationEventListener.class); for (ExecutionContext executionContext : ExecutionContextHolder.flush()) { executionContext.close(); } } @AfterClass(alwaysRun = true) public void tearDown() throws Exception { replicationStream.disconnect(); } private static class ExecutionContextHolder { private static Map<String, ExecutionContext> map = new HashMap<String, ExecutionContext>(); public static ExecutionContext get(String profile) { ExecutionContext executionContext = map.get(profile); if (executionContext == null) { map.put(profile, executionContext = new ExecutionContext(profile)); } return executionContext; } public static Collection<ExecutionContext> flush() { Collection<ExecutionContext> result = new ArrayList<ExecutionContext>(map.values()); map.clear(); return result; } } private static final class ExecutionContext { private final Logger logger = LoggerFactory.getLogger(ExecutionContext.class); private Configuration configuration; private SessionFactory sessionFactory; private String profile; private ExecutionContext(String profile) { configuration = new Configuration().configure("hibernate.cfg.xml"); try { configuration.addProperties(getProperties(profile)); } catch (IOException e) { throw new IllegalStateException("Failed to load properties for " + profile + " profile"); } ServiceRegistry serviceRegistry = new ServiceRegistryBuilder() .applySettings(configuration.getProperties()).buildServiceRegistry(); sessionFactory = configuration.buildSessionFactory(serviceRegistry); this.profile = profile; } private Properties getProperties(String profile) throws IOException { InputStream inputStream = getClass().getResource("/" + profile + ".properties").openStream(); try { Properties properties = new Properties(); properties.load(inputStream); return properties; } finally { inputStream.close(); } } public Configuration getConfiguration() { return configuration; } public SessionFactory getSessionFactory() { return sessionFactory; } public void execute(Callback<Session> callback) { if (logger.isDebugEnabled()) { logger.debug("Executing callback on " + profile); } Session session = sessionFactory.openSession(); try { session.beginTransaction(); callback.execute(session); session.getTransaction().commit(); } finally { session.close(); } } public void close() { sessionFactory.close(); } } private interface Callback<T> { void execute(T obj); } }