/* * 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.api.tree; import org.infinispan.api.mvcc.LockAssert; import org.infinispan.config.Configuration; import org.infinispan.container.DataContainer; import org.infinispan.container.entries.CacheEntry; import org.infinispan.context.InvocationContextContainer; import org.infinispan.factories.ComponentRegistry; import org.infinispan.manager.EmbeddedCacheManager; import org.infinispan.test.SingleCacheManagerTest; import org.infinispan.test.TestingUtil; import org.infinispan.test.fwk.TestCacheManagerFactory; import org.infinispan.transaction.LocalTransaction; import org.infinispan.transaction.TransactionTable; import org.infinispan.tree.Fqn; import org.infinispan.tree.Node; import org.infinispan.tree.NodeKey; import org.infinispan.tree.TreeCacheImpl; import org.infinispan.tree.TreeStructureSupport; import org.infinispan.util.Util; import org.infinispan.util.concurrent.locks.LockManager; import org.infinispan.util.logging.Log; import org.infinispan.util.logging.LogFactory; import org.testng.annotations.Test; import javax.transaction.SystemException; import javax.transaction.TransactionManager; import java.util.Random; import java.util.concurrent.CountDownLatch; import static org.testng.AssertJUnit.*; /** * Excercises and tests the new move() api * * @author <a href="mailto:manik AT jboss DOT org">Manik Surtani</a> */ @Test(groups = "functional", testName = "api.tree.NodeMoveAPITest") public class NodeMoveAPITest extends SingleCacheManagerTest { protected final Log log = LogFactory.getLog(getClass()); protected static final Fqn A = Fqn.fromString("/a"), B = Fqn.fromString("/b"), C = Fqn.fromString("/c"), D = Fqn.fromString("/d"), E = Fqn.fromString("/e"); static final Fqn A_B = Fqn.fromRelativeFqn(A, B); static final Fqn A_B_C = Fqn.fromRelativeFqn(A_B, C); static final Fqn A_B_C_E = Fqn.fromRelativeFqn(A_B_C, E); static final Fqn A_B_D = Fqn.fromRelativeFqn(A_B, D); static final Fqn C_E = Fqn.fromRelativeFqn(C, E); static final Fqn D_B = Fqn.fromRelativeFqn(D, B); static final Fqn D_B_C = Fqn.fromRelativeFqn(D_B, C); protected static final Object k = "key", vA = "valueA", vB = "valueB", vC = "valueC", vD = "valueD", vE = "valueE"; TreeCacheImpl<Object, Object> treeCache; TransactionManager tm; DataContainer dc; protected EmbeddedCacheManager createCacheManager() throws Exception { EmbeddedCacheManager cm = TestCacheManagerFactory.createLocalCacheManager(false); Configuration c = new Configuration().fluent() .stateRetrieval().fetchInMemoryState(false) .invocationBatching() .locking().lockAcquisitionTimeout(1000L) .build(); cm.defineConfiguration("test", c); cache = cm.getCache("test"); tm = TestingUtil.extractComponent(cache, TransactionManager.class); treeCache = new TreeCacheImpl(cache); dc = TestingUtil.extractComponent(cache, DataContainer.class); return cm; } public void testBasicMove() { Node<Object, Object> rootNode = treeCache.getRoot(); Node<Object, Object> nodeA = rootNode.addChild(A); nodeA.put(k, vA); Node<Object, Object> nodeB = rootNode.addChild(B); nodeB.put(k, vB); Node<Object, Object> nodeC = nodeA.addChild(C); nodeC.put(k, vC); /* /a/c /b */ assertTrue(rootNode.hasChild(A)); assertTrue(rootNode.hasChild(B)); assertFalse(rootNode.hasChild(C)); assertTrue(nodeA.hasChild(C)); // test data assertEquals("" + nodeA, vA, nodeA.get(k)); assertEquals(vB, nodeB.get(k)); assertEquals(vC, nodeC.get(k)); // parentage assertEquals(nodeA, nodeC.getParent()); log.info("BEFORE MOVE " + treeCache); // move treeCache.move(nodeC.getFqn(), nodeB.getFqn()); // re-fetch nodeC nodeC = treeCache.getNode(Fqn.fromRelativeFqn(nodeB.getFqn(), C)); log.info("POST MOVE " + treeCache); log.info("HC " + nodeC + " " + Util.hexIdHashCode(nodeC)); Node x = treeCache.getRoot().getChild(Fqn.fromString("b/c")); log.info("HC " + x + " " + Util.hexIdHashCode(x)); /* /a /b/c */ assertEquals("NODE C " + nodeC, "/b/c", nodeC.getFqn().toString()); assertTrue(rootNode.hasChild(A)); assertTrue(rootNode.hasChild(B)); assertFalse(rootNode.hasChild(C)); assertFalse(nodeA.hasChild(C)); assertTrue(nodeB.hasChild(C)); // test data assertEquals(vA, nodeA.get(k)); assertEquals(vB, nodeB.get(k)); assertEquals(vC, nodeC.get(k)); // parentage assertEquals("B is parent of C: " + nodeB, nodeB, nodeC.getParent()); } @SuppressWarnings("unchecked") private Node<Object, Object> genericize(Node node) { return (Node<Object, Object>) node; } public void testMoveWithChildren() { Node<Object, Object> rootNode = treeCache.getRoot(); Node<Object, Object> nodeA = rootNode.addChild(A); nodeA.put(k, vA); Node<Object, Object> nodeB = rootNode.addChild(B); nodeB.put(k, vB); Node<Object, Object> nodeC = nodeA.addChild(C); nodeC.put(k, vC); Node<Object, Object> nodeD = nodeC.addChild(D); nodeD.put(k, vD); Node<Object, Object> nodeE = nodeD.addChild(E); nodeE.put(k, vE); assertTrue(rootNode.hasChild(A)); assertTrue(rootNode.hasChild(B)); assertFalse(rootNode.hasChild(C)); assertTrue(nodeA.hasChild(C)); assertTrue(nodeC.hasChild(D)); assertTrue(nodeD.hasChild(E)); // test data assertEquals(vA, nodeA.get(k)); assertEquals(vB, nodeB.get(k)); assertEquals(vC, nodeC.get(k)); assertEquals(vD, nodeD.get(k)); assertEquals(vE, nodeE.get(k)); // parentage assertEquals(rootNode, nodeA.getParent()); assertEquals(rootNode, nodeB.getParent()); assertEquals(nodeA, nodeC.getParent()); assertEquals(nodeC, nodeD.getParent()); assertEquals(nodeD, nodeE.getParent()); // move log.info("move " + nodeC + " to " + nodeB); treeCache.move(nodeC.getFqn(), nodeB.getFqn()); //System.out.println("nodeB " + nodeB); //System.out.println("nodeC " + nodeC); // child nodes will need refreshing, since existing pointers will be stale. nodeC = nodeB.getChild(C); nodeD = nodeC.getChild(D); nodeE = nodeD.getChild(E); assertTrue(rootNode.hasChild(A)); assertTrue(rootNode.hasChild(B)); assertFalse(rootNode.hasChild(C)); assertFalse(nodeA.hasChild(C)); assertTrue(nodeB.hasChild(C)); assertTrue(nodeC.hasChild(D)); assertTrue(nodeD.hasChild(E)); // test data assertEquals(vA, nodeA.get(k)); assertEquals(vB, nodeB.get(k)); assertEquals(vC, nodeC.get(k)); assertEquals(vD, nodeD.get(k)); assertEquals(vE, nodeE.get(k)); // parentage assertEquals(rootNode, nodeA.getParent()); assertEquals(rootNode, nodeB.getParent()); assertEquals(nodeB, nodeC.getParent()); assertEquals(nodeC, nodeD.getParent()); assertEquals(nodeD, nodeE.getParent()); } public void testTxCommit() throws Exception { Node<Object, Object> rootNode = treeCache.getRoot(); Node<Object, Object> nodeA = rootNode.addChild(A); Node<Object, Object> nodeB = nodeA.addChild(B); assertEquals(rootNode, nodeA.getParent()); assertEquals(nodeA, nodeB.getParent()); assertEquals(nodeA, rootNode.getChildren().iterator().next()); assertEquals(nodeB, nodeA.getChildren().iterator().next()); tm.begin(); System.out.println("Before: " + TreeStructureSupport.printTree(treeCache, true)); // move node B up to hang off the root treeCache.move(nodeB.getFqn(), Fqn.ROOT); System.out.println("After: " + TreeStructureSupport.printTree(treeCache, true)); tm.commit(); System.out.println("Committed: " + TreeStructureSupport.printTree(treeCache, true)); nodeB = rootNode.getChild(B); assertEquals(rootNode, nodeA.getParent()); assertEquals(rootNode, nodeB.getParent()); assertTrue(rootNode.getChildren().contains(nodeA)); assertTrue(rootNode.getChildren().contains(nodeB)); assertTrue(nodeA.getChildren().isEmpty()); } public void testTxRollback() throws Exception { Node<Object, Object> rootNode = treeCache.getRoot(); Node<Object, Object> nodeA = rootNode.addChild(A); Node<Object, Object> nodeB = nodeA.addChild(B); assertEquals(rootNode, nodeA.getParent()); assertEquals(nodeA, nodeB.getParent()); assertEquals(nodeA, rootNode.getChildren().iterator().next()); assertEquals(nodeB, nodeA.getChildren().iterator().next()); tm.begin(); // move node B up to hang off the root System.out.println("Before: " + TreeStructureSupport.printTree(treeCache, true)); treeCache.move(nodeB.getFqn(), Fqn.ROOT); System.out.println("After: " + TreeStructureSupport.printTree(treeCache, true)); tm.rollback(); System.out.println("Rolled back: " + TreeStructureSupport.printTree(treeCache, true)); nodeA = rootNode.getChild(A); nodeB = nodeA.getChild(B); // should revert assertEquals(rootNode, nodeA.getParent()); assertEquals(nodeA, nodeB.getParent()); assertEquals(nodeA, rootNode.getChildren().iterator().next()); assertEquals(nodeB, nodeA.getChildren().iterator().next()); } public void testLocksDeepMove() throws Exception { Node<Object, Object> rootNode = treeCache.getRoot(); Node<Object, Object> nodeA = rootNode.addChild(A); Node<Object, Object> nodeB = nodeA.addChild(B); Node<Object, Object> nodeD = nodeB.addChild(D); Node<Object, Object> nodeC = rootNode.addChild(C); Node<Object, Object> nodeE = nodeC.addChild(E); assertNoLocks(); tm.begin(); treeCache.move(nodeC.getFqn(), nodeB.getFqn()); checkLocksDeep(); tm.commit(); assertNoLocks(); } public void testLocks() throws Exception { Node<Object, Object> rootNode = treeCache.getRoot(); Node<Object, Object> nodeA = rootNode.addChild(A); Node<Object, Object> nodeB = nodeA.addChild(B); Node<Object, Object> nodeC = rootNode.addChild(C); assertNoLocks(); tm.begin(); treeCache.move(nodeC.getFqn(), nodeB.getFqn()); checkLocks(); tm.commit(); assertNoLocks(); } public void testConcurrency() throws InterruptedException { Node<Object, Object> rootNode = treeCache.getRoot(); final int N = 3;// number of threads final int loops = 1 << 6;// number of loops // tests a tree structure as such: // /a // /b // /c // /d // /e // /x // /y // N threads constantly move /x and /y around to hang off either /a ~ /e randomly. final Fqn FQN_A = A, FQN_B = B, FQN_C = C, FQN_D = D, FQN_E = E, FQN_X = Fqn.fromString("/x"), FQN_Y = Fqn.fromString("/y"); // set up the initial structure. final Node[] NODES = { rootNode.addChild(FQN_A), rootNode.addChild(FQN_B), rootNode.addChild(FQN_C), rootNode.addChild(FQN_D), rootNode.addChild(FQN_E) }; final Node<Object, Object> NODE_X = genericize(NODES[0]).addChild(FQN_X); final Node<Object, Object> NODE_Y = genericize(NODES[1]).addChild(FQN_Y); Thread[] movers = new Thread[N]; final CountDownLatch latch = new CountDownLatch(1); final Random rnd = new Random(); for (int i = 0; i < N; i++) { movers[i] = new Thread("Mover-" + i) { public void run() { try { latch.await(); } catch (InterruptedException e) { } for (int counter = 0; counter < loops; counter++) { treeCache.move(NODE_X.getFqn(), NODES[rnd.nextInt(NODES.length)].getFqn()); TestingUtil.sleepRandom(250); treeCache.move(NODE_Y.getFqn(), NODES[rnd.nextInt(NODES.length)].getFqn()); TestingUtil.sleepRandom(250); } } }; movers[i].start(); } latch.countDown(); for (Thread t : movers) { t.join(); } assertNoLocks(); boolean found_x = false, found_x_again = false; for (Node erased : NODES) { Node<Object, Object> n = genericize(erased); if (!found_x) { found_x = n.hasChild(FQN_X); } else { found_x_again = found_x_again || n.hasChild(FQN_X); } } boolean found_y = false, found_y_again = false; for (Node erased : NODES) { Node<Object, Object> n = genericize(erased); if (!found_y) { found_y = n.hasChild(FQN_Y); } else { found_y_again = found_y_again || n.hasChild(FQN_Y); } } log.fatal("Tree: " + TreeStructureSupport.printTree(treeCache, true)); assertTrue("Should have found x", found_x); assertTrue("Should have found y", found_y); assertFalse("Should have only found x once", found_x_again); assertFalse("Should have only found y once", found_y_again); } public void testMoveInSamePlace() { Node<Object, Object> rootNode = treeCache.getRoot(); final Fqn FQN_X = Fqn.fromString("/x"); // set up the initial structure. Node aNode = rootNode.addChild(A); Node xNode = aNode.addChild(FQN_X); assertEquals(aNode.getChildren().size(), 1); System.out.println("Before: " + TreeStructureSupport.printTree(treeCache, true)); treeCache.move(xNode.getFqn(), aNode.getFqn()); System.out.println("After: " + TreeStructureSupport.printTree(treeCache, true)); assertEquals(aNode.getChildren().size(), 1); assertNoLocks(); } /** * Looks up the CacheEntry stored in transactional context corresponding to this AtomicMap. If this AtomicMap * has yet to be touched by the current transaction, this method will return a null. * @return */ protected CacheEntry lookupEntryFromCurrentTransaction(TransactionTable transactionTable, TransactionManager transactionManager, Object key) { // Prior to 5.1, this used to happen by grabbing any InvocationContext in ThreadLocal. Since ThreadLocals // can no longer be relied upon in 5.1, we need to grab the TransactionTable and check if an ongoing // transaction exists, peeking into transactional state instead. try { LocalTransaction localTransaction = transactionTable.getLocalTransaction(transactionManager.getTransaction()); // The stored localTransaction could be null, if this is the first call in a transaction. In which case // we know that there is no transactional state to refer to - i.e., no entries have been looked up as yet. return localTransaction == null ? null : localTransaction.lookupEntry(key); } catch (SystemException e) { return null; } } protected boolean isNodeLocked(Fqn fqn) { TransactionManager tm = cache.getAdvancedCache().getTransactionManager(); TransactionTable tt = cache.getAdvancedCache().getComponentRegistry().getComponent(TransactionTable.class); CacheEntry structure = lookupEntryFromCurrentTransaction(tt, tm, new NodeKey(fqn, NodeKey.Type.STRUCTURE)); CacheEntry data = lookupEntryFromCurrentTransaction(tt, tm, new NodeKey(fqn, NodeKey.Type.DATA)); return structure != null && data != null && structure.isChanged() && data.isChanged(); } protected void checkLocks() { assert isNodeLocked(C) : "Node " + C + " is not locked!"; assert isNodeLocked(A_B_C) : "Node " + A_B_C + " is not locked!"; } protected void checkLocksDeep() { // /a/b, /c, /c/e, /a/b/c and /a/b/c/e should all be locked. assert isNodeLocked(C); assert isNodeLocked(C_E); assert isNodeLocked(A_B_C); assert isNodeLocked(A_B_C_E); } protected void assertNoLocks() { ComponentRegistry cr = TestingUtil.extractComponentRegistry(cache); LockManager lm = cr.getComponent(LockManager.class); InvocationContextContainer icc = cr.getComponent(InvocationContextContainer.class); LockAssert.assertNoLocks(lm, icc); } public void testNonexistentSource() { treeCache.put(A_B_C, "k", "v"); assert "v".equals(treeCache.get(A_B_C, "k")); assert 1 == treeCache.getNode(A_B).getChildren().size(); assert treeCache.getNode(A_B).getChildrenNames().contains(C.getLastElement()); assert !treeCache.getNode(A_B).getChildrenNames().contains(D.getLastElement()); treeCache.move(D, A_B); assert "v".equals(treeCache.get(A_B_C, "k")); assert 1 == treeCache.getNode(A_B).getChildren().size(); assert treeCache.getNode(A_B).getChildrenNames().contains(C.getLastElement()); assert !treeCache.getNode(A_B).getChildrenNames().contains(D.getLastElement()); } public void testNonexistentTarget() { treeCache.put(A_B_C, "k", "v"); assert "v".equals(treeCache.get(A_B_C, "k")); assert 1 == treeCache.getNode(A_B).getChildren().size(); assert treeCache.getNode(A_B).getChildrenNames().contains(C.getLastElement()); assert null == treeCache.getNode(D); System.out.println(TreeStructureSupport.printTree(treeCache, true)); treeCache.move(A_B, D); System.out.println(TreeStructureSupport.printTree(treeCache, true)); assert null == treeCache.getNode(A_B_C); assert null == treeCache.getNode(A_B); assert null != treeCache.getNode(D); assert null != treeCache.getNode(D_B); assert null != treeCache.getNode(D_B_C); assert "v".equals(treeCache.get(D_B_C, "k")); } }