/** * 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 com.persistit.TransactionIndex.tss2vh; import static com.persistit.TransactionStatus.UNCOMMITTED; import static com.persistit.unit.ConcurrentUtil.assertSuccess; import static com.persistit.unit.ConcurrentUtil.createThread; import static com.persistit.unit.ConcurrentUtil.join; import static com.persistit.unit.ConcurrentUtil.start; import static com.persistit.util.Util.NS_PER_S; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Random; import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicInteger; import org.junit.Test; import com.persistit.Version.PrunableVersion; import com.persistit.Version.VersionCreator; import com.persistit.exception.PersistitException; import com.persistit.exception.RollbackException; import com.persistit.unit.ConcurrentUtil.ThrowingRunnable; import com.persistit.unit.ConcurrentUtil.UncaughtExceptionHandler; import com.persistit.util.Util; public class TimelyResourceTest extends PersistitUnitTestCase { private int _idCounter; static class TestVersion implements PrunableVersion { final int _id; final TimelyResourceTest _test; final AtomicInteger _pruned = new AtomicInteger(); TestVersion(final int id, final TimelyResourceTest test) { _id = id; _test = test; } @Override public boolean prune() { _pruned.incrementAndGet(); return true; } @Override public void vacate() { System.out.println("No more versions"); } @Override public String toString() { return String.format("<%,d:%,d>", _id, _pruned.get()); } TimelyResourceTest getContainer() { return _test; } } @Test public void testAddAndPruneResources() throws Exception { testAddAndPruneResources1(false); testAddAndPruneResources1(true); } private void testAddAndPruneResources1(final boolean withTransactions) throws Exception { final Transaction txn = _persistit.getTransaction(); final TimelyResource<TestVersion> tr = new TimelyResource<TestVersion>(_persistit); final long[] history = new long[5]; final TestVersion[] resources = new TestVersion[5]; for (int i = 0; i < 5; i++) { if (withTransactions) { txn.begin(); } final TestVersion resource = new TestVersion(i, this); resources[i] = resource; if (!tr.isEmpty()) { tr.delete(); } tr.addVersion(resource, txn); if (withTransactions) { txn.commit(); txn.end(); } history[i] = _persistit.getTimestampAllocator().updateTimestamp(); } assertEquals("Incorrect version count " + withTransactions, 9, tr.getVersionCount()); for (int i = 0; i < 5; i++) { final TestVersion t = tr.getVersion(tss2vh(history[i], 0)); assertTrue("Missing version " + withTransactions, t != null); assertEquals("Wrong version " + withTransactions, i, t._id); } _persistit.getTransactionIndex().updateActiveTransactionCache(); tr.prune(); assertEquals("Should have one version left " + withTransactions, 1, tr.getVersionCount()); assertEquals("Wrong version " + withTransactions, 4, tr.getVersion(tss2vh(UNCOMMITTED, 0))._id); tr.delete(); assertEquals("Should have two versions left " + withTransactions, 2, tr.getVersionCount()); _persistit.getTransactionIndex().updateActiveTransactionCache(); tr.prune(); assertEquals("Should have no versions left " + withTransactions, 0, tr.getVersionCount()); for (int i = 0; i < 5; i++) { assertEquals("Should have been pruned " + withTransactions, 1, resources[i]._pruned.get()); } } @Test public void concurrentAddAndPruneResources() throws Exception { final TimelyResource<TestVersion> tr = new TimelyResource<TestVersion>(_persistit); final Random random = new Random(1); final long expires = System.nanoTime() + 10 * NS_PER_S; final AtomicInteger sequence = new AtomicInteger(); final AtomicInteger rollbackCount = new AtomicInteger(); final List<Thread> threads = new ArrayList<Thread>(); final UncaughtExceptionHandler handler = new UncaughtExceptionHandler(); int threadCounter = 0; while (System.nanoTime() < expires) { for (final Iterator<Thread> iter = threads.iterator(); iter.hasNext();) { if (!iter.next().isAlive()) { iter.remove(); } } while (threads.size() < 20) { final Thread t = createThread(String.format("Thread_%06d", ++threadCounter), new ThrowingRunnable() { @Override public void run() throws Exception { doConcurrentTransaction(tr, random, sequence, rollbackCount); } }); threads.add(t); start(handler, t); } Util.sleep(10); tr.prune(); } join(Long.MAX_VALUE, handler.getThrowableMap(), threads.toArray(new Thread[threads.size()])); assertSuccess(handler.getThrowableMap()); assertTrue("Every transaction rolled back", rollbackCount.get() < sequence.get()); System.out.printf("%,d entries, %,d rollbacks\n", sequence.get(), rollbackCount.get()); } private void doConcurrentTransaction(final TimelyResource<TestVersion> tr, final Random random, final AtomicInteger sequence, final AtomicInteger rollbackCount) throws PersistitException { try { final Transaction txn = _persistit.getTransaction(); for (int i = 0; i < 25; i++) { txn.begin(); try { final int id = sequence.incrementAndGet(); tr.addVersion(new TestVersion(id, this), txn); final int delay = (1 << random.nextInt(3)); // Up to 7/1000 of a second Util.sleep(delay); final TestVersion mine = tr.getVersion(); assertEquals("Should not have been pruned yet", 0, mine._pruned.get()); assertEquals("Wrong resource", id, mine._id); if (random.nextInt(10) == 0) { txn.rollback(); } else { txn.commit(); } } catch (final RollbackException e) { txn.rollback(); rollbackCount.incrementAndGet(); } finally { txn.end(); } } } catch (final RollbackException e) { rollbackCount.incrementAndGet(); } } @Test public void deleteResource() throws Exception { final TimelyResource<TestVersion> tr = new TimelyResource<TestVersion>(_persistit); _idCounter = 0; final VersionCreator<TestVersion> creator = new VersionCreator<TestVersion>() { @Override public TestVersion createVersion(final TimelyResource<? extends TestVersion> resource) throws PersistitException { return new TestVersion(++_idCounter, TimelyResourceTest.this); } }; final Transaction txn1 = _persistit.getTransaction(); _persistit.setSessionId(new SessionId()); final Transaction txn2 = _persistit.getTransaction(); TestVersion v1; txn1.begin(); v1 = tr.getVersion(creator); assertTrue("Version ID mismatch", v1._id == _idCounter); txn2.begin(); txn1.incrementStep(); tr.delete(); tr.prune(); assertEquals("Should still be two versions", 2, tr.getVersionCount()); txn2.commit(); txn1.commit(); _persistit.getTransactionIndex().updateActiveTransactionCache(); tr.prune(); assertEquals("Should now have no versions", 0, tr.getVersionCount()); txn1.end(); txn2.end(); } @Test public void versions() throws Exception { final TimelyResource<TestVersion> tr = new TimelyResource<TestVersion>(_persistit); _idCounter = 0; final VersionCreator<TestVersion> creator = new VersionCreator<TestVersion>() { @Override public TestVersion createVersion(final TimelyResource<? extends TestVersion> resource) throws PersistitException { return new TestVersion(++_idCounter, TimelyResourceTest.this); } }; final Semaphore semaphore1 = new Semaphore(0); final Transaction txn = _persistit.getTransaction(); final Thread[] threads = new Thread[10]; for (int i = 0; i < 10; i++) { try { txn.begin(); tr.addVersion(creator.createVersion(tr), txn); txn.commit(); } finally { txn.end(); } final Semaphore semaphore2 = new Semaphore(0); threads[i] = new Thread(new Runnable() { @Override public void run() { final Transaction txn = _persistit.getTransaction(); try { txn.begin(); final TestVersion t = tr.getVersion(); assertEquals(t._id, _idCounter); semaphore2.release(); semaphore1.acquire(); txn.commit(); } catch (final Exception e) { e.printStackTrace(); } finally { txn.end(); } } }); threads[i].start(); semaphore2.acquire(); } _persistit.getTransactionIndex().updateActiveTransactionCache(); tr.prune(); assertEquals(10, tr.getVersionCount()); semaphore1.release(10); for (final Thread thread : threads) { thread.join(); } _persistit.getTransactionIndex().updateActiveTransactionCache(); tr.prune(); assertEquals(1, tr.getVersionCount()); assertEquals("Surviving primordial version should be last one committed", 10, tr.getVersion(null)._id); } }