/* * 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.test; import org.infinispan.Cache; import org.infinispan.commands.VisitableCommand; import org.infinispan.commands.tx.CommitCommand; import org.infinispan.commands.tx.PrepareCommand; import org.infinispan.commands.write.WriteCommand; import org.infinispan.context.InvocationContext; import org.infinispan.context.impl.TxInvocationContext; import org.infinispan.interceptors.base.CommandInterceptor; import org.infinispan.manager.EmbeddedCacheManager; import org.infinispan.util.logging.Log; import org.infinispan.util.logging.LogFactory; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * A listener that listens for replication events on a cache it is watching. Typical usage: <code> ReplListener r = * attachReplicationListener(cache); r.expect(RemoveCommand.class); // ... r.waitForRPC(); </code> */ public class ReplListener { Cache<?, ?> c; volatile List<Class<? extends VisitableCommand>> expectedCommands; List<Class<? extends VisitableCommand>> eagerCommands = new LinkedList<Class<? extends VisitableCommand>>(); boolean recordCommandsEagerly; boolean watchLocal; final Lock expectationSetupLock = new ReentrantLock(); CountDownLatch latch = new CountDownLatch(1); volatile boolean sawAtLeastOneInvocation = false; boolean expectAny = false; private Log log = LogFactory.getLog(ReplListener.class); /** * This listener atatches itself to a cache and when {@link #expect(Class[])} is invoked, will start checking for * invocations of the command on the cache, waiting for all expected commands to be received in {@link * #waitForRpc()}. * * @param c cache on which to attach listener */ public ReplListener(Cache<?, ?> c) { this(c, false); } /** * As {@link #ReplListener(org.infinispan.Cache)} except that you can optionally configure whether command recording * is eager (false by default). * <p/> * If <tt>recordCommandsEagerly</tt> is true, then commands are recorded from the moment the listener is attached to * the cache, even before {@link #expect(Class[])} is invoked. As such, when {@link #expect(Class[])} is called, the * list of commands to wait for will take into account commands already seen thanks to eager recording. * * @param c cache on which to attach listener * @param recordCommandsEagerly whether to record commands eagerly */ public ReplListener(Cache<?, ?> c, boolean recordCommandsEagerly) { this(c, recordCommandsEagerly, false); } /** * Same as {@link #ReplListener(org.infinispan.Cache, boolean)} except that this constructor allows you to set the * watchLocal parameter. If true, even local events are recorded (not just ones that originate remotely). * * @param c cache on which to attach listener * @param recordCommandsEagerly whether to record commands eagerly * @param watchLocal if true, local events are watched for as well */ public ReplListener(Cache<?, ?> c, boolean recordCommandsEagerly, boolean watchLocal) { this.c = c; this.recordCommandsEagerly = recordCommandsEagerly; this.watchLocal = watchLocal; this.c.getAdvancedCache().addInterceptor(new ReplListenerInterceptor(), 1); } /** * Expects any commands. The moment a single command is detected, the {@link #waitForRpc()} command will be * unblocked. */ public void expectAny() { expectAny = true; expect(); } /** * Expects a specific set of commands, within transactional scope (i.e., as a payload to a PrepareCommand). If the * cache mode is synchronous, a CommitCommand is expected as well. * * @param commands commands to expect (not counting transaction boundary commands like PrepareCommand and * CommitCommand) */ @SuppressWarnings("unchecked") public void expectWithTx(Class<? extends VisitableCommand>... commands) { List<Class<? extends VisitableCommand>> cmdsToExpect = new ArrayList<Class<? extends VisitableCommand>>(); cmdsToExpect.add(PrepareCommand.class); if (commands != null) cmdsToExpect.addAll(Arrays.asList(commands)); //this is because for async replication we have an 1pc transaction if (c.getConfiguration().getCacheMode().isSynchronous()) cmdsToExpect.add(CommitCommand.class); expect(cmdsToExpect.toArray(new Class[cmdsToExpect.size()])); } /** * Expects any commands, within transactional scope (i.e., as a payload to a PrepareCommand). If the cache mode is * synchronous, a CommitCommand is expected as well. */ @SuppressWarnings("unchecked") public void expectAnyWithTx() { List<Class<? extends VisitableCommand>> cmdsToExpect = new ArrayList<Class<? extends VisitableCommand>>(2); cmdsToExpect.add(PrepareCommand.class); //this is because for async replication we have an 1pc transaction if (c.getConfiguration().getCacheMode().isSynchronous()) cmdsToExpect.add(CommitCommand.class); expect(cmdsToExpect.toArray(new Class[cmdsToExpect.size()])); } /** * Expects a specific set of commands. {@link #waitForRpc()} will block until all of these commands are detected. * * @param expectedCommands commands to expect */ public void expect(Class<? extends VisitableCommand>... expectedCommands) { expectationSetupLock.lock(); try { if (this.expectedCommands == null) { this.expectedCommands = new CopyOnWriteArrayList<Class<? extends VisitableCommand>>(); } this.expectedCommands.addAll(Arrays.asList(expectedCommands)); info("Setting expected commands to " + this.expectedCommands); info("Record eagerly is " + recordCommandsEagerly + ", and eager commands are " + eagerCommands); if (recordCommandsEagerly) { this.expectedCommands.removeAll(eagerCommands); if (!eagerCommands.isEmpty()) sawAtLeastOneInvocation = true; eagerCommands.clear(); } } finally { expectationSetupLock.unlock(); } } private void info(String str) { log.info(" [" + c + "] " + str); } /** * Blocks for a predefined amount of time (120 Seconds) until commands defined in any of the expect*() methods have * been detected. If the commands have not been detected by this time, an exception is thrown. */ public void waitForRpc() { waitForRpc(30, TimeUnit.SECONDS); } /** * The same as {@link #waitForRpc()} except that you are allowed to specify the max wait time. */ public void waitForRpc(long time, TimeUnit unit) { assert expectedCommands != null : "there are no replication expectations; please use ReplListener.expect() before calling this method"; try { boolean successful = (expectAny && sawAtLeastOneInvocation) || (!expectAny && expectedCommands.isEmpty()); info("Expect Any is " + expectAny + ", saw at least one? " + sawAtLeastOneInvocation + " Successful? " + successful + " Expected commands " + expectedCommands); if (!successful && !latch.await(time, unit)) { EmbeddedCacheManager cacheManager = c.getCacheManager(); assert false : "Waiting for more than " + time + " " + unit + " and following commands did not replicate: " + expectedCommands + " on cache [" + cacheManager.getAddress() + "]"; } else { info("Exiting wait for rpc with expected commands " + expectedCommands); } } catch (InterruptedException e) { throw new IllegalStateException("unexpected", e); } finally { expectationSetupLock.lock(); expectedCommands = null; expectationSetupLock.unlock(); expectAny = false; sawAtLeastOneInvocation = false; latch = new CountDownLatch(1); eagerCommands.clear(); } } public Cache<?, ?> getCache() { return c; } public void resetEager() { eagerCommands.clear(); } public void reconfigureListener(boolean recordCommandsEagerly, boolean watchLocal) { this.recordCommandsEagerly = recordCommandsEagerly; this.watchLocal = watchLocal; } protected class ReplListenerInterceptor extends CommandInterceptor { @Override protected Object handleDefault(InvocationContext ctx, VisitableCommand cmd) throws Throwable { // first pass up chain Object o; try { o = invokeNextInterceptor(ctx, cmd); } finally {//make sure we do mark this command as received even in the case of exceptions(e.g. timeouts) info("Checking whether command " + cmd.getClass().getSimpleName() + " should be marked as local with watch local set to " + watchLocal); if (!ctx.isOriginLocal() || watchLocal) markAsVisited(cmd); } return o; } @Override public Object visitPrepareCommand(TxInvocationContext ctx, PrepareCommand cmd) throws Throwable { // first pass up chain Object o = invokeNextInterceptor(ctx, cmd); if (!ctx.isOriginLocal() || watchLocal) { markAsVisited(cmd); for (WriteCommand mod : cmd.getModifications()) markAsVisited(mod); } return o; } private void markAsVisited(VisitableCommand cmd) { expectationSetupLock.lock(); try { info("ReplListener saw command " + cmd); if (expectedCommands != null) { if (expectedCommands.remove(cmd.getClass())) { info("Successfully removed command: " + cmd.getClass()); } else { if (recordCommandsEagerly) eagerCommands.add(cmd.getClass()); } sawAtLeastOneInvocation = true; if (expectedCommands.isEmpty()) { info("Nothing to wait for, releasing latch"); latch.countDown(); } } else { if (recordCommandsEagerly) eagerCommands.add(cmd.getClass()); } } finally { expectationSetupLock.unlock(); } } } }