package org.infinispan.statetransfer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import org.infinispan.Cache; import org.infinispan.configuration.cache.CacheMode; import org.infinispan.configuration.cache.ConfigurationBuilder; import org.infinispan.remoting.transport.Address; import org.infinispan.test.MultipleCacheManagersTest; import org.infinispan.test.TestingUtil; import org.infinispan.test.fwk.CleanupAfterMethod; import org.infinispan.util.BaseControlledConsistentHashFactory; import org.testng.AssertJUnit; import org.testng.annotations.Test; /** * Tests the read when a node loses the ownership of a key. * * @author Pedro Ruivo * @since 6.0 */ @Test(groups = "functional", testName = "statetransfer.ReadAfterLosingOwnershipTest") @CleanupAfterMethod public class ReadAfterLosingOwnershipTest extends MultipleCacheManagersTest { private boolean l1 = false; @Override public Object[] factory() { return new Object[] { new ReadAfterLosingOwnershipTest().transactional(true), new ReadAfterLosingOwnershipTest().transactional(false), new ReadAfterLosingOwnershipTest().l1(true).transactional(true), new ReadAfterLosingOwnershipTest().l1(true).transactional(false), }; } public ReadAfterLosingOwnershipTest l1(boolean l1) { this.l1 = l1; return this; } @Override protected String parameters() { return "[tx=" + transactional + ", l1=" + l1 + "]"; } public void testOwnershipLostWithPut() throws Exception { doOwnershipLostTest(Operation.PUT, false); } public void testOwnershipLostWithRemove() throws Exception { doOwnershipLostTest(Operation.REMOVE, false); } public void testOwnershipLostWithPutOnOwner() throws Exception { doOwnershipLostTest(Operation.PUT, true); } public void testOwnershipLostWithRemoveOnOwner() throws Exception { doOwnershipLostTest(Operation.REMOVE, true); } @Override protected void createCacheManagers() throws Throwable { createClusteredCaches(2, createConfigurationBuilder()); } protected final ConfigurationBuilder createConfigurationBuilder() { ConfigurationBuilder builder = getDefaultClusteredCacheConfig(CacheMode.DIST_SYNC, transactional); builder.clustering() .hash().numOwners(2).consistentHashFactory(new SingleKeyConsistentHashFactory()).numSegments(1) .l1().enabled(l1) .stateTransfer().fetchInMemoryState(true); return builder; } private void doOwnershipLostTest(Operation operation, boolean onOwner) throws ExecutionException, InterruptedException { log.debug("Initialize cache"); cache(0).put("key", "value0"); assertCachesKeyValue("key", "value0"); StateConsumerImpl stateConsumer = (StateConsumerImpl) TestingUtil.extractComponent(cache(1), StateConsumer.class); Listener listener = new Listener(); stateConsumer.setKeyInvalidationListener(listener); log.debug("Add a 3rd node"); addClusterEnabledCacheManager(createConfigurationBuilder()); Future<Void> join = fork(new Callable<Void>() { @Override public Void call() throws Exception { waitForClusterToForm(); log.debug("3rd has join"); return null; } }); log.debug("Waiting for command to block"); listener.notifier.await(); log.debug("Set a new value"); //we change the value in the old owner if onOwner is false operation.update(onOwner ? cache(0) : cache(1)); //we check the value in the primary owner and old owner (cache(2) has not started yet) assertCachesKeyValue("key", operation.finalValue(), cache(0), cache(1)); listener.wait.countDown(); log.debug("Waiting for the 3rd node to join"); join.get(); assertCachesKeyValue("key", operation.finalValue()); } private void assertCachesKeyValue(Object key, Object value) { assertCachesKeyValue(key, value, caches()); } private void assertCachesKeyValue(Object key, Object value, Cache<Object, Object>... caches) { assertCachesKeyValue(key, value, Arrays.asList(caches)); } private void assertCachesKeyValue(Object key, Object value, Collection<Cache<Object, Object>> caches) { for (Cache<Object, Object> cache : caches) { AssertJUnit.assertEquals("Wrong key value for " + address(cache), value, cache.get(key)); } } private enum Operation { //only PUT and REMOVE is needed because one updates the key (i.e. the value is not null) and the other removes //it (i.e. the value is null) PUT, REMOVE; public void update(Cache<Object, Object> cache) { if (this == PUT) { cache.put("key", "value1"); } else { cache.remove("key"); } } public Object finalValue() { return this == PUT ? "value1" : null; } } public static class SingleKeyConsistentHashFactory extends BaseControlledConsistentHashFactory { public SingleKeyConsistentHashFactory() { super(1); } protected final List<Address> createOwnersCollection(List<Address> members, int numberOfOwners, int segmentIndex) { //the owners will be the first member and the last (numberOfOwners - 1)-th members List<Address> owners = new ArrayList<Address>(numberOfOwners); owners.add(members.get(0)); for (int i = members.size() - 1; i > 0; --i) { if (owners.size() >= numberOfOwners) { break; } owners.add(members.get(i)); } return owners; } } public class Listener implements StateConsumerImpl.KeyInvalidationListener { public final CountDownLatch notifier = new CountDownLatch(1); final CountDownLatch wait = new CountDownLatch(1); @Override public void beforeInvalidation(Set<Integer> removedSegments, Set<Integer> staleL1Segments) { log.debugf("Before invalidation: removedSegments=%s, staleL1Segments=%s", removedSegments, staleL1Segments); if (!removedSegments.contains(0)) { //it only matters when it looses the segment 0 and the key is moved to the new owner return; } notifier.countDown(); try { wait.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } }