/** * Copyright 2011-2012 Akiban Technologies, Inc. * * 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.persistit; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.io.InputStream; import java.io.Serializable; import java.lang.reflect.Field; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.junit.Test; import com.persistit.Transaction.CommitPolicy; import com.persistit.util.Util; public class ClassIndexTest extends PersistitUnitTestCase { /** * Arbitrary constant chosen to be found exactly once in the * MockSerailizableObject class file. */ final static long SUID_CONSTANT = 0xF3E1D4C1B5A99286L; private final static String MOCK_SERIALIZABLE_CLASS_NAME = "MockSerializableObject"; private int _maxHandle = 0; final Map<Integer, ClassInfo> map = new ConcurrentHashMap<Integer, ClassInfo>(); @SuppressWarnings("serial") public static class A implements Serializable { } @SuppressWarnings("serial") public static class B implements Serializable { } public static class C { } @Override public void tearDown() throws Exception { map.clear(); super.tearDown(); } @Test public void oneClassInfo() throws Exception { final ClassIndex cx = _persistit.getClassIndex(); cx.registerClass(this.getClass()); final ClassInfo ci = cx.lookupByClass(this.getClass()); assertEquals(this.getClass().getName(), ci.getName()); assertTrue(ci.getHandle() > 0); } @Test public void manyClassInfo() throws Exception { _maxHandle = 0; final ClassIndex cx = _persistit.getClassIndex(); final Class<?> clazz = Persistit.class; test2a(cx, clazz); assertEquals("Cache misses should match map size", map.size(), cx.getCacheMisses() - cx.getDiscardedDuplicates()); for (int handle = 0; handle < _maxHandle + 10; handle++) { assertTrue(equals(map.get(handle), cx.lookupByHandle(handle))); } final ClassIndex cy = new ClassIndex(_persistit); for (int handle = 0; handle < _maxHandle + 10; handle++) { assertTrue(equals(map.get(handle), cy.lookupByHandle(handle))); } for (int handle = 0; handle < _maxHandle + 10; handle++) { final ClassInfo ci = map.get(handle); if (ci != null) { assertEquals(ci, cy.lookupByClass(ci.getDescribedClass())); } } final ClassIndex cz = new ClassIndex(_persistit); for (int handle = 0; handle < _maxHandle + 10; handle++) { final ClassInfo ci = map.get(handle); if (ci != null) { assertEquals(ci, cz.lookupByClass(ci.getDescribedClass())); } } System.out.println(cx.size() + " classes"); } @Test public void multiThreaded() throws Exception { final int threadCount = 50; final Thread[] threads = new Thread[threadCount]; for (int i = 0; i < threadCount; i++) { final int index = i; threads[i] = new Thread(new Runnable() { @Override public void run() { final Transaction transaction = _persistit.getTransaction(); for (int k = 0; k < 10; k++) { try { transaction.begin(); try { test2a(_persistit.getClassIndex(), Persistit.class); if ((index % 3) == 0) { transaction.rollback(); } else { transaction.commit(); } } finally { transaction.end(); } } catch (final Exception e) { e.printStackTrace(); } } } }); } for (int i = 0; i < threadCount; i++) { threads[i].start(); } for (int i = 0; i < threadCount; i++) { threads[i].join(); } final ClassIndex cx = _persistit.getClassIndex(); /* * Verify that almost all lookups were satisfied from cache. There * should be one "cache miss" for every class in the map. There may be * more: these are caused by concurrent execution and are explicitly * permitted - they will happen only rarely and only when multiple * threads are contending to register a new class. Such instances are * safely discarded and the following adjusts the cache miss count by * the number so discarded. */ assertEquals("Cache misses should match map size", map.size(), cx.getCacheMisses() - cx.getDiscardedDuplicates()); for (int handle = 0; handle < _maxHandle + 10; handle++) { assertTrue(equals(map.get(handle), cx.lookupByHandle(handle))); } } @Test public void rollback() throws Exception { Exchange ex = _persistit.getExchange("persistit", "ClassIndexTest", true); final Transaction txn = ex.getTransaction(); txn.begin(); try { ex.getValue().put(new A()); ex.to("A").store(); ex.getValue().put(new B()); ex.to("B").store(); txn.rollback(); } finally { txn.end(); } txn.begin(); try { ex.getValue().put(new B()); ex.to("B").store(); ex.getValue().put(new A()); ex.to("A").store(); txn.commit(CommitPolicy.HARD); } finally { txn.end(); } _persistit.crash(); _persistit = new Persistit(_config); ex = _persistit.getExchange("persistit", "ClassIndexTest", false); final Object b = ex.to("B").fetch().getValue().get(); final Object a = ex.to("A").fetch().getValue().get(); assertTrue("Incorrect class", a instanceof A); assertTrue("Incorrect class", b instanceof B); } @Test public void knownNull() throws Exception { final ClassIndex cx = _persistit.getClassIndex(); final ClassInfo ci = cx.lookupByHandle(12345); assertEquals("Should return cached known null", ci, cx.lookupByHandle(12345)); } @Test public void multipleVersions() throws Exception { final ClassIndex cx = _persistit.getClassIndex(); cx.clearAllEntries(); final int count = 10; final Class<?>[] classes = new Class<?>[count]; for (int i = 0; i < count; i++) { classes[i] = makeAClass(i); assertNotNull("this test requires tools.jar on the classpath", classes[i]); cx.lookupByClass(classes[i]); } assertEquals("Each version should be unique", count, cx.getCacheMisses()); final Set<Integer> handles = new HashSet<Integer>(); for (int i = 0; i < count; i++) { final ClassInfo ci = cx.lookupByClass(classes[i]); final int handle = ci.getHandle(); assertTrue("Handles must be unique", handles.add(handle)); } assertEquals("Each version should be in cache", count, cx.getCacheMisses()); for (final Integer handle : handles) { final ClassInfo ci = cx.lookupByHandle(handle); assertEquals("Lookup by handle and class must match", ci, cx.lookupByClass(ci.getDescribedClass())); } } private boolean equals(final ClassInfo a, final ClassInfo b) { if (a == b) { return true; } if (a == null) { return b.getDescribedClass() == null; } return a.equals(b); } private void test2a(final ClassIndex cx, final Class<?> clazz) throws Exception { if (clazz.isPrimitive()) { return; } final ClassInfo ci = cx.lookupByClass(clazz); final ClassInfo copy = map.get(ci.getHandle()); if (ci.getHandle() > _maxHandle) { assertNull(copy); map.put(ci.getHandle(), ci); _maxHandle = ci.getHandle(); final Field[] fields = clazz.getDeclaredFields(); for (final Field field : fields) { if (cx.size() < 1000) { test2a(cx, field.getType()); } } } else { assert (copy.equals(ci)); } } private static Class<?> makeAClass(final long suid) throws Exception { final InputStream is = ClassIndexTest.class.getClassLoader().getResourceAsStream( "com/persistit/" + MOCK_SERIALIZABLE_CLASS_NAME + "_classBytes"); final byte[] bytes = new byte[10000]; final int length = is.read(bytes); replaceSuid(bytes, suid); final ClassLoader cl = new ClassLoader(ClassIndexTest.class.getClassLoader()) { @Override public Class<?> loadClass(final String name) { if (name.contains(MOCK_SERIALIZABLE_CLASS_NAME)) { return defineClass(name, bytes, 0, length); } else { try { return ClassIndexTest.class.getClassLoader().loadClass(name); } catch (final Exception e) { throw new RuntimeException(e); } } } }; return cl.loadClass("com.persistit." + MOCK_SERIALIZABLE_CLASS_NAME); } private final static void replaceSuid(final byte[] bytes, final long suid) { boolean replaced = false; for (int i = 0; i < bytes.length; i++) { boolean found = true; for (int j = 0; found && j < 8; j++) { found &= ((bytes[i + j] & 0xFF) == ((SUID_CONSTANT >>> (56 - j * 8)) & 0xFF)); } if (found) { assertTrue("Found multiple instances of SUID_CONSTANT", !replaced); Util.putLong(bytes, i, suid); replaced = true; } } assertTrue("Did not find SUID_CONSTANT", replaced); } }