/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.cassandra.utils.concurrent; import org.junit.Test; import junit.framework.Assert; import java.io.File; import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicReference; import org.apache.cassandra.utils.ObjectSizes; import org.apache.cassandra.utils.Pair; import org.apache.cassandra.utils.concurrent.Ref.Visitor; @SuppressWarnings({"unused", "unchecked", "rawtypes"}) public class RefCountedTest { static { if (Ref.STRONG_LEAK_DETECTOR != null) Ref.STRONG_LEAK_DETECTOR.submit(() -> { Thread.sleep(Integer.MAX_VALUE); return null; }); } private static final class Tidier implements RefCounted.Tidy { boolean tidied; public void tidy() { tidied = true; } public String name() { return "test tidy"; } } @Test public void testLeak() throws InterruptedException { Tidier tidier = new Tidier(); Ref<?> obj = new Ref(null, tidier); obj.tryRef(); obj.release(); System.gc(); System.gc(); Thread.sleep(1000); Assert.assertTrue(tidier.tidied); } @Test public void testSeriousLeak() throws InterruptedException { Tidier tidier = new Tidier(); new Ref(null, tidier); System.gc(); System.gc(); System.gc(); System.gc(); Thread.sleep(1000); Assert.assertTrue(tidier.tidied); } @Test public void testDoubleRelease() throws InterruptedException { Tidier tidier = null; try { tidier = new Tidier(); Ref<?> obj = new Ref(null, tidier); obj.release(); obj.release(); Assert.assertTrue(false); } catch (Exception e) { } } @Test public void testMemoryLeak() { Tidier tidier = new Tidier(); Ref<Object> ref = new Ref(null, tidier); long initialSize = ObjectSizes.measureDeep(ref); for (int i = 0 ; i < 1000 ; i++) ref.ref().release(); long finalSize = ObjectSizes.measureDeep(ref); if (finalSize > initialSize * 2) throw new AssertionError(); ref.release(); } static final int entryCount = 1000000; static final int fudgeFactor = 20; @Test public void testLinkedList() { final List<Object> iterable = new LinkedList<Object>(); Pair<Object, Object> p = Pair.create(iterable, iterable); RefCounted.Tidy tidier = new RefCounted.Tidy() { Object ref = iterable; @Override public void tidy() throws Exception { } @Override public String name() { return "42"; } }; Ref<Object> ref = new Ref(new AtomicReference<List<Object>>(iterable), tidier); for (int i = 0; i < entryCount; i++) { iterable.add(p); } Visitor visitor = new Visitor(); visitor.run(); ref.close(); System.out.println("LinkedList visited " + visitor.lastVisitedCount + " iterations " + visitor.iterations); //Should visit a lot of list nodes, but no more since there is only one object stored in the list Assert.assertTrue(visitor.lastVisitedCount > entryCount && visitor.lastVisitedCount < entryCount + fudgeFactor); //Should have a lot of iterations to walk the list, but linear to the number of entries Assert.assertTrue(visitor.iterations > (entryCount * 3) && visitor.iterations < (entryCount * 3) + fudgeFactor); } /* * There was a traversal error terminating traversal for an object upon encountering a null * field. Test for the bug here using CLQ. */ @Test public void testCLQBug() { Ref.concurrentIterables.remove(ConcurrentLinkedQueue.class); try { testConcurrentLinkedQueueImpl(true); } finally { Ref.concurrentIterables.add(ConcurrentLinkedQueue.class); } } private void testConcurrentLinkedQueueImpl(boolean bugTest) { final Queue<Object> iterable = new ConcurrentLinkedQueue<Object>(); Pair<Object, Object> p = Pair.create(iterable, iterable); RefCounted.Tidy tidier = new RefCounted.Tidy() { Object ref = iterable; @Override public void tidy() throws Exception { } @Override public String name() { return "42"; } }; Ref<Object> ref = new Ref(new AtomicReference<Queue<Object>>(iterable), tidier); for (int i = 0; i < entryCount; i++) { iterable.add(p); } Visitor visitor = new Visitor(); visitor.run(); ref.close(); System.out.println("ConcurrentLinkedQueue visited " + visitor.lastVisitedCount + " iterations " + visitor.iterations + " bug test " + bugTest); if (bugTest) { //Should have to visit a lot of queue nodes Assert.assertTrue(visitor.lastVisitedCount > entryCount && visitor.lastVisitedCount < entryCount + fudgeFactor); //Should have a lot of iterations to walk the queue, but linear to the number of entries Assert.assertTrue(visitor.iterations > (entryCount * 2) && visitor.iterations < (entryCount * 2) + fudgeFactor); } else { //There are almost no objects in this linked list once it's iterated as a collection so visited count //should be small Assert.assertTrue(visitor.lastVisitedCount < 10); //Should have a lot of iterations to walk the collection, but linear to the number of entries Assert.assertTrue(visitor.iterations > entryCount && visitor.iterations < entryCount + fudgeFactor); } } @Test public void testConcurrentLinkedQueue() { testConcurrentLinkedQueueImpl(false); } @Test public void testBlockingQueue() { final BlockingQueue<Object> iterable = new LinkedBlockingQueue<Object>(); Pair<Object, Object> p = Pair.create(iterable, iterable); RefCounted.Tidy tidier = new RefCounted.Tidy() { Object ref = iterable; @Override public void tidy() throws Exception { } @Override public String name() { return "42"; } }; Ref<Object> ref = new Ref(new AtomicReference<BlockingQueue<Object>>(iterable), tidier); for (int i = 0; i < entryCount; i++) { iterable.add(p); } Visitor visitor = new Visitor(); visitor.run(); ref.close(); System.out.println("BlockingQueue visited " + visitor.lastVisitedCount + " iterations " + visitor.iterations); //There are almost no objects in this queue once it's iterated as a collection so visited count //should be small Assert.assertTrue(visitor.lastVisitedCount < 10); //Should have a lot of iterations to walk the collection, but linear to the number of entries Assert.assertTrue(visitor.iterations > entryCount && visitor.iterations < entryCount + fudgeFactor); } @Test public void testConcurrentMap() { final Map<Object, Object> map = new ConcurrentHashMap<Object, Object>(); RefCounted.Tidy tidier = new RefCounted.Tidy() { Object ref = map; @Override public void tidy() throws Exception { } @Override public String name() { return "42"; } }; Ref<Object> ref = new Ref(new AtomicReference<Map<Object, Object>>(map), tidier); Object o = new Object(); for (int i = 0; i < entryCount; i++) { map.put(new Object(), o); } Visitor visitor = new Visitor(); visitor.run(); ref.close(); System.out.println("ConcurrentHashMap visited " + visitor.lastVisitedCount + " iterations " + visitor.iterations); //Should visit roughly the same number of objects as entries because the value object is constant //Map.Entry objects shouldn't be counted since it is iterated as a collection Assert.assertTrue(visitor.lastVisitedCount > entryCount && visitor.lastVisitedCount < entryCount + fudgeFactor); //Should visit 2x the number of entries since we have to traverse the key and value separately Assert.assertTrue(visitor.iterations > entryCount * 2 && visitor.iterations < entryCount * 2 + fudgeFactor); } @Test public void testHashMap() { final Map<Object, Object> map = new HashMap<Object, Object>(); RefCounted.Tidy tidier = new RefCounted.Tidy() { Object ref = map; @Override public void tidy() throws Exception { } @Override public String name() { return "42"; } }; Ref<Object> ref = new Ref(new AtomicReference<Map<Object, Object>>(map), tidier); Object o = new Object(); for (int i = 0; i < entryCount; i++) { map.put(new Object(), o); } Visitor visitor = new Visitor(); visitor.run(); ref.close(); System.out.println("HashMap visited " + visitor.lastVisitedCount + " iterations " + visitor.iterations); //Should visit 2x the number of entries because of the wrapper Map.Entry objects Assert.assertTrue(visitor.lastVisitedCount > (entryCount * 2) && visitor.lastVisitedCount < (entryCount * 2) + fudgeFactor); //Should iterate 3x the number of entries since we have to traverse the key and value separately Assert.assertTrue(visitor.iterations > (entryCount * 3) && visitor.iterations < (entryCount * 3) + fudgeFactor); } @Test public void testArray() throws Exception { final Object objects[] = new Object[entryCount]; for (int i = 0; i < entryCount; i += 2) objects[i] = new Object(); File f = File.createTempFile("foo", "bar"); RefCounted.Tidy tidier = new RefCounted.Tidy() { Object ref = objects; //Checking we don't get an infinite loop out of traversing file refs File fileRef = f; @Override public void tidy() throws Exception { } @Override public String name() { return "42"; } }; Ref<Object> ref = new Ref(new AtomicReference<Object[]>(objects), tidier); Visitor visitor = new Visitor(); visitor.run(); ref.close(); System.out.println("Array visited " + visitor.lastVisitedCount + " iterations " + visitor.iterations); //Should iterate the elements in the array and get a unique object from every other one Assert.assertTrue(visitor.lastVisitedCount > (entryCount / 2) && visitor.lastVisitedCount < (entryCount / 2) + fudgeFactor); //Should iterate over the array touching roughly the same number of objects as entries Assert.assertTrue(visitor.iterations > (entryCount / 2) && visitor.iterations < (entryCount / 2) + fudgeFactor); } //Make sure a weak ref is ignored by the visitor looking for strong ref leaks @Test public void testWeakRef() throws Exception { AtomicReference dontRefMe = new AtomicReference(); WeakReference<Object> weakRef = new WeakReference(dontRefMe); RefCounted.Tidy tidier = new RefCounted.Tidy() { WeakReference<Object> ref = weakRef; @Override public void tidy() throws Exception { } @Override public String name() { return "42"; } }; Ref<Object> ref = new Ref(dontRefMe, tidier); dontRefMe.set(ref); Visitor visitor = new Visitor(); visitor.haveLoops = new HashSet<>(); visitor.run(); ref.close(); Assert.assertTrue(visitor.haveLoops.isEmpty()); } }