package edu.brown.hstore; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Random; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; import org.voltdb.TransactionIdManager; import org.voltdb.VoltProcedure; import org.voltdb.catalog.Procedure; import org.voltdb.catalog.Site; import edu.brown.BaseTestCase; import edu.brown.benchmark.tm1.procedures.DeleteCallForwarding; import edu.brown.hstore.PartitionLockQueue.QueueState; import edu.brown.hstore.conf.HStoreConf; import edu.brown.hstore.txns.AbstractTransaction; import edu.brown.hstore.txns.LocalTransaction; import edu.brown.utils.CollectionUtil; import edu.brown.utils.PartitionSet; import edu.brown.utils.ProjectType; import edu.brown.utils.StringUtil; import edu.brown.utils.ThreadUtil; public class TestPartitionLockQueue extends BaseTestCase { private static final int NUM_TXNS = 10; private static final int TXN_DELAY = 500; private static final Class<? extends VoltProcedure> TARGET_PROCEDURE = DeleteCallForwarding.class; private static final Random random = new Random(0); HStoreSite hstore_site; HStoreConf hstore_conf; TransactionIdManager idManager; TransactionQueueManager queueManager; PartitionLockQueue queue; PartitionLockQueue.Debug queueDbg; Procedure catalog_proc; @Override protected void setUp() throws Exception { super.setUp(ProjectType.TM1); this.hstore_conf = HStoreConf.singleton(); this.hstore_conf.site.txn_incoming_delay = TXN_DELAY; Site catalog_site = CollectionUtil.first(catalogContext.sites); assertNotNull(catalog_site); this.hstore_site = new MockHStoreSite(catalog_site.getId(), catalogContext, HStoreConf.singleton()); this.idManager = hstore_site.getTransactionIdManager(0); this.queueManager = this.hstore_site.getTransactionQueueManager(); this.queue = this.queueManager.getLockQueue(0); this.queueDbg = this.queue.getDebugContext(); assertTrue(this.queue.isEmpty()); this.catalog_proc = this.getProcedure(TARGET_PROCEDURE); } // -------------------------------------------------------------------------------------------- // UTILITY METHODS // -------------------------------------------------------------------------------------------- private class BlockingTakeThread extends Thread { final AtomicReference<AbstractTransaction> result = new AtomicReference<AbstractTransaction>(); final CountDownLatch latch = new CountDownLatch(1); { this.setDaemon(true); } public void run() { try { AbstractTransaction ts = queue.take(); System.err.println("AWOKEN: " + ts); result.set(ts); } catch (InterruptedException ex) { ex.printStackTrace(); } finally { latch.countDown(); } } } private Collection<AbstractTransaction> loadQueue(int num_txns) throws InterruptedException { Collection<AbstractTransaction> added = new TreeSet<AbstractTransaction>(); for (long i = 0; i < num_txns; i++) { LocalTransaction txn = new LocalTransaction(this.hstore_site); Long txnId = this.idManager.getNextUniqueTransactionId(); txn.testInit(txnId, 0, new PartitionSet(1), this.catalog_proc); // I think that we need to do this... this.queue.noteTransactionRecievedAndReturnLastSafeTxnId(txn.getTransactionId()); boolean ret = this.queue.offer(txn, false); assert(ret); added.add(txn); } // FOR return (added); } // -------------------------------------------------------------------------------------------- // TEST CASES // -------------------------------------------------------------------------------------------- /** * testBlockUntilReady */ @Test public void testBlockUntilReady() throws Exception { // We want to check that we can have one thread block on the queue // even though there is a txn in the queue, but whose block time hasn't passed // yet. We want to check to see that the thread can be awoke on its own // without needing to call checkQueueState() // Load one txn in the queue. Collection<AbstractTransaction> added = this.loadQueue(1); assertEquals(1, added.size()); AbstractTransaction expected = CollectionUtil.first(added); assertNotNull(expected); BlockingTakeThread t = new BlockingTakeThread(); t.start(); // Sleep for a little bit to avoid a race condition in our tests // The thread should not have finished ThreadUtil.sleep(TXN_DELAY); // Ok now we'll just move time forward. The thread should // haven been woken up on its own boolean result = t.latch.await(TXN_DELAY, TimeUnit.MILLISECONDS); assertTrue(result); assertEquals(expected, t.result.get()); } /** * testBlockOnEmpty */ @Test public void testBlockOnEmpty() throws Exception { // We want to check that we can have one thread block on the queue when it // is empty and then be awoken when it's ready to run BlockingTakeThread t = new BlockingTakeThread(); t.start(); // Sleep for a little bit to avoid a race condition in our tests ThreadUtil.sleep(TXN_DELAY); assertNull(t.result.get()); assertEquals(1, t.latch.getCount()); assertTrue(t.isAlive()); // Load one txn in the queue. // The thread will not be awoken because we have not moved time forward Collection<AbstractTransaction> added = this.loadQueue(1); AbstractTransaction expected = CollectionUtil.first(added); assertNull(t.result.get()); assertEquals(1, added.size()); // Now sleep and then update the time // The thread still won't be woken up boolean result = t.latch.await(TXN_DELAY*2, TimeUnit.MILLISECONDS); assertTrue(result); assertEquals(expected, t.result.get()); } /** * testThrottling */ @Test public void testThrottling() throws Exception { // Always start off as empty QueueState state = this.queueDbg.checkQueueState(); assertEquals(QueueState.BLOCKED_EMPTY, state); int max = NUM_TXNS / 2; this.queue.setAllowDecrease(false); this.queue.setAllowIncrease(false); this.queue.setThrottleThreshold(max); // Add in the first half of the txns. These should // all get inserted with out get blocked Collection<AbstractTransaction> added = this.loadQueue(max); assertEquals(max, added.size()); assertTrue(this.queue.isThrottled()); // Now when we add in the rest, we should immediately get blocked for (int i = 0; i < NUM_TXNS; i++) { Collection<AbstractTransaction> afterThrottle = null; try { afterThrottle = this.loadQueue(1); } catch (AssertionError ex) { // IGNORE } assertNull(afterThrottle); } // FOR assertEquals(max, added.size()); assertTrue(this.queue.isThrottled()); // Make sure that we get unthrottled after we release // enough txns int release = this.queue.getThrottleRelease(); for (int i = 0, cnt = (max - release); i < cnt; i++) { ThreadUtil.sleep(TXN_DELAY); // EstTimeUpdater.update(System.currentTimeMillis()); this.queueDbg.checkQueueState(); AbstractTransaction ts = this.queue.poll(); assertNotNull("i="+i, ts); } // FOR System.err.println(this.queue.debug()); assertFalse(this.queue.toString(), this.queue.isThrottled()); } /** * testQueueState */ @Test public void testQueueState() throws Exception { // Always start off as empty QueueState state = this.queueDbg.checkQueueState(); assertEquals(QueueState.BLOCKED_EMPTY, state); // Insert a bunch of txns that all have the same initiating timestamp Collection<AbstractTransaction> added = this.loadQueue(NUM_TXNS); assertEquals(added.size(), this.queue.size()); System.err.println(StringUtil.join("\n", added)); // Because we haven't moved the current time up, we know that none of // the txns should be released now state = this.queueDbg.checkQueueState(); // assertEquals(QueueState.BLOCKED_SAFETY, state); // Sleep for a little bit to make the current time move forward ThreadUtil.sleep(TXN_DELAY); // EstTimeUpdater.update(System.currentTimeMillis()); Iterator<AbstractTransaction> it = added.iterator(); for (int i = 0; i < NUM_TXNS; i++) { // No matter how many times that we call checkQueueState, our // blocked timestamp should not change since we haven't released // a transaction Long lastBlockTime = null; long nextBlockTime; for (int j = 0; j < 10; j++) { // if (j != 0) PartitionLockQueue.LOG.info(StringUtil.SINGLE_LINE.trim()); String debug = String.format("i=%d / j=%d", i, j); state = this.queueDbg.checkQueueState(); nextBlockTime = this.queueDbg.getBlockedTimestamp(); // System.err.printf("%s => state=%s / lastBlock=%d\n", debug, state, lastBlockTime); assertEquals(debug, QueueState.UNBLOCKED, state); if (lastBlockTime != null) { assertEquals(debug, lastBlockTime.longValue(), nextBlockTime); } lastBlockTime = nextBlockTime; } // FOR assertEquals("i="+i, it.next(), this.queue.poll()); // PartitionLockQueue.LOG.info(StringUtil.DOUBLE_LINE.trim()); } // FOR } /** * testQueueStateAfterRemove */ @Test public void testQueueStateAfterRemove() throws Exception { // Always start off as empty QueueState state = this.queueDbg.checkQueueState(); assertEquals(QueueState.BLOCKED_EMPTY, state); // Insert a bunch of txns that all have the same initiating timestamp Collection<AbstractTransaction> added = this.loadQueue(NUM_TXNS); assertEquals(added.size(), this.queue.size()); System.err.println(StringUtil.join("\n", added)); // Because we haven't moved the current time up, we know that none of // the txns should be released now state = this.queueDbg.checkQueueState(); // assertEquals(QueueState.BLOCKED_SAFETY, state); // Sleep for a little bit to make the current time move forward ThreadUtil.sleep(TXN_DELAY); // EstTimeUpdater.update(System.currentTimeMillis()); PartitionLockQueue.LOG.info(StringUtil.DOUBLE_LINE.trim()); Iterator<AbstractTransaction> it = added.iterator(); for (int i = 0; i < NUM_TXNS; i++) { String debug = String.format("i=%d", i); // Ok so what we're going to do here is peek into the // queue and make sure that that our expected txn // is the next one that's suppose to pop out AbstractTransaction to_remove = it.next(); state = this.queueDbg.checkQueueState(); assertEquals(debug, QueueState.UNBLOCKED, state); assertEquals(debug, to_remove, this.queue.peek()); // Then we're going to delete it and make sure that the next txn // queued up is not the one that we just removed boolean result = this.queue.remove(to_remove); assertTrue(debug, result); if (i + 1 < NUM_TXNS) { assertFalse(debug, this.queue.isEmpty()); assertEquals(debug, QueueState.UNBLOCKED, this.queue.getQueueState()); } else { assertTrue(debug, this.queue.isEmpty()); } PartitionLockQueue.LOG.info(StringUtil.DOUBLE_LINE.trim()); } // FOR } /** * testOutOfOrderInsertion */ @Test public void testOutOfOrderInsertion() throws Exception { // Create a bunch of txns and then insert them in the wrong order // We should be able to get them back in the right order Collection<AbstractTransaction> added = new TreeSet<AbstractTransaction>(); for (long i = 0; i < NUM_TXNS; i++) { LocalTransaction txn = new LocalTransaction(this.hstore_site); Long txnId = this.idManager.getNextUniqueTransactionId(); txn.testInit(txnId, 0, new PartitionSet(1), this.catalog_proc); added.add(txn); } // FOR List<AbstractTransaction> shuffled = new ArrayList<AbstractTransaction>(added); Collections.shuffle(shuffled, random); System.err.println(StringUtil.columns( "Expected Order:\n" + StringUtil.join("\n", added), "Insertion Order:\n" + StringUtil.join("\n", shuffled) )); System.err.flush(); for (AbstractTransaction txn : shuffled) { this.queue.noteTransactionRecievedAndReturnLastSafeTxnId(txn.getTransactionId()); boolean ret = this.queue.offer(txn, false); assert(ret); // assertNull(this.queue.poll()); } // FOR assertEquals(added.size(), this.queue.size()); assertEquals(QueueState.BLOCKED_SAFETY, this.queueDbg.checkQueueState()); // Now we should be able to remove the first of these mofos Iterator<AbstractTransaction> it = added.iterator(); for (int i = 0; i < NUM_TXNS; i++) { ThreadUtil.sleep(TXN_DELAY); // EstTimeUpdater.update(System.currentTimeMillis()); AbstractTransaction expected = it.next(); assertNotNull(expected); if (i == 0) this.queueDbg.checkQueueState(); assertEquals("i="+i, expected, this.queue.poll()); } // FOR } /** * testOutOfOrderRemoval */ @Test public void testOutOfOrderRemoval() throws Exception { Collection<AbstractTransaction> added = this.loadQueue(NUM_TXNS); assertEquals(added.size(), this.queue.size()); // Now grab the last one and pop it out AbstractTransaction last = CollectionUtil.last(added); assertTrue(this.queue.remove(last)); assertFalse(this.queue.contains(last)); // Now we should be able to remove the first of these mofos Iterator<AbstractTransaction> it = added.iterator(); for (int i = 0; i < NUM_TXNS-1; i++) { ThreadUtil.sleep(TXN_DELAY); // EstTimeUpdater.update(System.currentTimeMillis()); if (i == 0) this.queueDbg.checkQueueState(); assertEquals(it.next(), this.queue.poll()); } // FOR assertTrue(this.queue.isEmpty()); } /** * testRemove */ @Test public void testRemove() throws Exception { Collection<AbstractTransaction> added = this.loadQueue(1); assertEquals(added.size(), this.queue.size()); // Remove the first. Make sure that poll() doesn't return it ThreadUtil.sleep(TXN_DELAY*4); // System.err.println(StringUtil.repeat("-", 100)); this.loadQueue(1); ThreadUtil.sleep(TXN_DELAY*2); // EstTimeUpdater.update(System.currentTimeMillis()); // System.err.println(StringUtil.repeat("-", 100)); this.queueDbg.checkQueueState(); AbstractTransaction first = CollectionUtil.first(added); assertEquals(first, this.queue.peek()); assertTrue(first.toString(), this.queue.remove(first)); assertFalse(first.toString(), this.queue.contains(first)); AbstractTransaction poll = this.queue.poll(); assertNotSame(first, poll); } /** * testRemoveIterator */ @Test public void testRemoveIterator() throws Exception { List<AbstractTransaction> added = new ArrayList<AbstractTransaction>(this.loadQueue(10)); assertEquals(added.size(), this.queue.size()); Collections.shuffle(added); // Remove them one by one and make sure that the iterator // never returns an id that we removed Set<AbstractTransaction> removed = new HashSet<AbstractTransaction>(); for (int i = 0, cnt = added.size(); i < cnt; i++) { AbstractTransaction next = added.get(i); assertFalse(next.toString(), removed.contains(next)); assertTrue(next.toString(), this.queue.contains(next)); assertTrue(next.toString(),this.queue.remove(next)); removed.add(next); int it_ctr = 0; for (AbstractTransaction txn : this.queue) { assertNotNull(txn); assertFalse(txn.toString(), removed.contains(txn)); assertTrue(txn.toString(), added.contains(txn)); it_ctr++; } // FOR assertEquals(added.size() - removed.size(), it_ctr); } // FOR } /** * testConcurrentOfferIterator */ @Test public void testConcurrentOfferIterator() throws Exception { Collection<AbstractTransaction> added = this.loadQueue(10); assertEquals(added.size(), this.queue.size()); LocalTransaction toOffer = new LocalTransaction(this.hstore_site); Long txnId = this.idManager.getNextUniqueTransactionId(); toOffer.testInit(txnId, 0, new PartitionSet(1), this.catalog_proc); assertFalse(this.queue.contains(toOffer)); Set<AbstractTransaction> found = new HashSet<AbstractTransaction>(); for (AbstractTransaction txn : this.queue) { if (found.isEmpty()) this.queue.offer(toOffer, false); found.add(txn); } // FOR assertFalse(found.contains(toOffer)); assertEquals(added.size(), found.size()); } /** * testPoll */ @Test public void testPoll() throws Exception { Collection<AbstractTransaction> added = this.loadQueue(NUM_TXNS); assertEquals(added.size(), this.queue.size()); Iterator<AbstractTransaction> it = added.iterator(); for (int i = 0; i < NUM_TXNS; i++) { ThreadUtil.sleep(TXN_DELAY); // EstTimeUpdater.update(System.currentTimeMillis()); if (i == 0) this.queueDbg.checkQueueState(); assertEquals(it.next(), this.queue.poll()); } // FOR } /** * testPollTooEarly */ @Test public void testPollTooEarly() throws Exception { // Try polling *before* the appropriate wait time this.queueDbg.setMaxWaitTime(TXN_DELAY * 5); Collection<AbstractTransaction> added = this.loadQueue(1); ThreadUtil.sleep(TXN_DELAY * 5); added.addAll(this.loadQueue(1)); assertEquals(added.size(), this.queue.size()); Iterator<AbstractTransaction> it = added.iterator(); for (int i = 0; i < NUM_TXNS; i++) { // Our first poll should return back a txn if (i == 0) { this.queueDbg.checkQueueState(); assertEquals(it.next(), this.queue.poll()); } // The second time should be null else { AbstractTransaction ts = this.queue.poll(); assertNull("Unexpected txn returned: " + ts, ts); break; } } // FOR } }