All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jsimpledb.kv.test.KVDatabaseTest Maven / Gradle / Ivy

There is a newer version: 3.6.1
Show newest version

/*
 * Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
 */

package org.jsimpledb.kv.test;

import com.google.common.base.Converter;

import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Random;
import java.util.TreeMap;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import org.jsimpledb.kv.KVDatabase;
import org.jsimpledb.kv.KVPair;
import org.jsimpledb.kv.KVTransaction;
import org.jsimpledb.kv.RetryTransactionException;
import org.jsimpledb.kv.StaleTransactionException;
import org.jsimpledb.kv.TransactionTimeoutException;
import org.jsimpledb.util.ByteUtil;
import org.jsimpledb.util.ConvertedNavigableMap;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

public abstract class KVDatabaseTest extends KVTestSupport {

    protected ExecutorService executor;

    @BeforeClass(dependsOnGroups = "configure")
    public void setup() throws Exception {
        this.executor = Executors.newFixedThreadPool(33);
        for (KVDatabase[] kvdb : this.getDBs()) {
            if (kvdb.length > 0)
                kvdb[0].start();
        }
    }

    @AfterClass
    public void teardown() throws Exception {
        this.executor.shutdown();
        for (KVDatabase[] kvdb : this.getDBs()) {
            if (kvdb.length > 0)
                kvdb[0].stop();
        }
    }

    @DataProvider(name = "kvdbs")
    protected KVDatabase[][] getDBs() {
        final KVDatabase kvdb = this.getKVDatabase();
        return kvdb != null ? new KVDatabase[][] { { kvdb } } : new KVDatabase[0][];
    }

    protected abstract KVDatabase getKVDatabase();

    @Test(dataProvider = "kvdbs")
    public void testSimpleStuff(KVDatabase store) throws Exception {

        // Debug
        this.log.info("starting testSimpleStuff() on " + store);

        // Clear database
        this.log.info("testSimpleStuff() on " + store + ": clearing database");
        this.try3times(store, new Transactional() {
            @Override
            public Void transact(KVTransaction tx) {
                tx.removeRange(null, null);
                return null;
            }
        });
        this.log.info("testSimpleStuff() on " + store + ": done clearing database");

        // Verify database is empty
        this.log.info("testSimpleStuff() on " + store + ": verifying database is empty");
        this.try3times(store, new Transactional() {
            @Override
            public Void transact(KVTransaction tx) {
                KVPair p = tx.getAtLeast(null);
                Assert.assertNull(p);
                p = tx.getAtMost(null);
                Assert.assertNull(p);
                Iterator it = tx.getRange(null, null, false);
                Assert.assertFalse(it.hasNext());
                return null;
            }
        });
        this.log.info("testSimpleStuff() on " + store + ": done verifying database is empty");

        // tx 1
        this.log.info("testSimpleStuff() on " + store + ": starting tx1");
        this.try3times(store, new Transactional() {
            @Override
            public Void transact(KVTransaction tx) {
                final byte[] x = tx.get(b("01"));
                if (x != null)
                    Assert.assertEquals(tx.get(b("01")), b("02"));          // transaction was retried even though it succeeded
                tx.put(b("01"), b("02"));
                Assert.assertEquals(tx.get(b("01")), b("02"));
                return null;
            }
        });
        this.log.info("testSimpleStuff() on " + store + ": committed tx1");

        // tx 2
        this.log.info("testSimpleStuff() on " + store + ": starting tx2");
        this.try3times(store, new Transactional() {
            @Override
            public Void transact(KVTransaction tx) {
                final byte[] x = tx.get(b("01"));
                Assert.assertNotNull(x);
                Assert.assertTrue(Arrays.equals(x, b("02")) || Arrays.equals(x, b("03")));
                tx.put(b("01"), b("03"));
                Assert.assertEquals(tx.get(b("01")), b("03"));
                return null;
            }
        });
        this.log.info("testSimpleStuff() on " + store + ": committed tx2");

        // tx 3
        this.log.info("testSimpleStuff() on " + store + ": starting tx3");
        this.try3times(store, new Transactional() {
            @Override
            public Void transact(KVTransaction tx) {
                final byte[] x = tx.get(b("01"));
                Assert.assertEquals(x, b("03"));
                tx.put(b("10"), b("01"));
                return null;
            }
        });
        this.log.info("testSimpleStuff() on " + store + ": committed tx3");

        // Check stale access
        this.log.info("testSimpleStuff() on " + store + ": checking stale access");
        final KVTransaction tx = this.try3times(store, new Transactional() {
            @Override
            public KVTransaction transact(KVTransaction tx) {
                return tx;
            }
        });
        try {
            tx.get(b("01"));
            assert false;
        } catch (StaleTransactionException e) {
            // expected
        }
        this.log.info("finished testSimpleStuff() on " + store);
    }

    @Test(dataProvider = "kvdbs")
    public void testKeyWatch(KVDatabase store) throws Exception {

        // Debug
        this.log.info("starting testKeyWatch() on " + store);

        // Clear database
        this.log.info("testKeyWatch() on " + store + ": clearing database");
        this.try3times(store, new Transactional() {
            @Override
            public Void transact(KVTransaction tx) {
                tx.removeRange(null, null);
                return null;
            }
        });
        this.log.info("testKeyWatch() on " + store + ": done clearing database");

        // Set up the modifications we want to test
        final ArrayList> mods = new ArrayList<>();
        mods.add(new Transactional() {
            @Override
            public Void transact(KVTransaction tx) {
                tx.put(b("0123"), b("4567"));
                return null;
            }
        });
        mods.add(new Transactional() {
            @Override
            public Void transact(KVTransaction tx) {
                tx.put(b("0123"), b("89ab"));
                return null;
            }
        });
        mods.add(new Transactional() {
            @Override
            public Void transact(KVTransaction tx) {
                tx.put(b("0123"), tx.encodeCounter(1234));
                return null;
            }
        });
        mods.add(new Transactional() {
            @Override
            public Void transact(KVTransaction tx) {
                tx.adjustCounter(b("0123"), 99);
                return null;
            }
        });
        mods.add(new Transactional() {
            @Override
            public Void transact(KVTransaction tx) {
                tx.removeRange(b("01"), b("02"));
                return null;
            }
        });
        mods.add(new Transactional() {
            @Override
            public Void transact(KVTransaction tx) {
                tx.put(b("0123"), b(""));
                return null;
            }
        });
        mods.add(new Transactional() {
            @Override
            public Void transact(KVTransaction tx) {
                tx.remove(b("0123"));
                return null;
            }
        });

        // Set watches, perform modifications, and test notifications
        for (Transactional mod : mods) {

            // Set watch
            this.log.info("testKeyWatch() on " + store + ": creating key watch for " + mod);
            final Future watch = this.try3times(store, new Transactional>() {
                @Override
                public Future transact(KVTransaction tx) {
                    try {
                        return tx.watchKey(b("0123"));
                    } catch (UnsupportedOperationException e) {
                        return null;
                    }
                }
            });
            if (watch == null) {
                this.log.info("testKeyWatch() on " + store + ": key watches not supported, bailing out");
                return;
            }
            this.log.info("testKeyWatch() on " + store + ": created key watch: " + watch);

            // Perform modification
            this.log.info("testKeyWatch() on " + store + ": testing " + mod);
            this.try3times(store, mod);

            // Get notification
            this.log.info("testKeyWatch() on " + store + ": waiting for notification");
            final long start = System.nanoTime();
            watch.get(1, TimeUnit.SECONDS);
            this.log.info("testKeyWatch() on " + store + ": got notification in " + ((System.nanoTime() - start) / 1000000) + "ms");
        }

        // Done
        this.log.info("finished testKeyWatch() on " + store);
    }

    @Test(dataProvider = "kvdbs")
    public void testConflictingTransactions(KVDatabase store) throws Exception {

        // Clear database
        this.log.info("starting testConflictingTransactions() on " + store);
        this.try3times(store, new Transactional() {
            @Override
            public Void transact(KVTransaction tx) {
                tx.removeRange(null, null);
                return null;
            }
        });

        // Both read the same key
        final KVTransaction[] txs = new KVTransaction[] { store.createTransaction(), store.createTransaction() };
        this.log.info("tx[0] is " + txs[0]);
        this.log.info("tx[1] is " + txs[1]);
        this.executor.submit(new Reader(txs[0], b("10"))).get();
        this.executor.submit(new Reader(txs[1], b("10"))).get();

        // Both write to the same key but with different values
        final String[] fails = new String[] { "uninitialized status", "uninitialized status" };
        Future[] futures = new Future[] {
          this.executor.submit(new Writer(txs[0], b("10"), b("01"))),
          this.executor.submit(new Writer(txs[1], b("10"), b("02")))
        };

        // See what happened - we might have gotten a conflict at write time
        for (int i = 0; i < 2; i++) {
            try {
                futures[i].get();
                this.log.info(txs[i] + " #" + (i + 1) + " succeeded on write");
                fails[i] = null;
            } catch (Exception e) {
                while (e instanceof ExecutionException)
                    e = (Exception)e.getCause();
                if (!(e instanceof RetryTransactionException))
                    throw new AssertionError("wrong exception type: " + e, e);
                final RetryTransactionException retry = (RetryTransactionException)e;
                Assert.assertSame(retry.getTransaction(), txs[i]);
                this.log.info(txs[i] + " #" + (i + 1) + " failed on write");
                if (this.log.isTraceEnabled())
                    this.log.trace(txs[i] + " #" + (i + 1) + " write failure exception trace:", e);
                fails[i] = "" + e;
            }
        }

        // Show contents of surviving transactions; note exception(s) could occur here also
        for (int i = 0; i < 2; i++) {
            if (fails[i] == null) {
                final Exception e = this.showKV(txs[i], "tx[" + i + "] of " + store + " after write");
                if (e != null)
                    fails[i] = "" + e;
            }
        }

        // If both succeeded, then we should get a conflict on commit instead
        for (int i = 0; i < 2; i++) {
            if (fails[i] == null)
                futures[i] = this.executor.submit(new Committer(txs[i]));
        }
        for (int i = 0; i < 2; i++) {
            if (fails[i] == null) {
                try {
                    futures[i].get();
                    this.log.info(txs[i] + " #" + (i + 1) + " succeeded on commit");
                    fails[i] = null;
                } catch (Exception e) {
                    while (e instanceof ExecutionException)
                        e = (Exception)e.getCause();
                    assert e instanceof RetryTransactionException : "wrong exception type: " + e;
                    final RetryTransactionException retry = (RetryTransactionException)e;
                    Assert.assertSame(retry.getTransaction(), txs[i]);
                    this.log.info(txs[i] + " #" + (i + 1) + " failed on commit");
                    if (this.log.isTraceEnabled())
                        this.log.trace(txs[i] + " #" + (i + 1) + " commit failure exception trace:", e);
                    fails[i] = "" + e;
                }
            }
        }

        // Exactly one should have failed and one should have succeeded (for most databases)
        if (!this.allowBothTransactionsToFail()) {
            assert fails[0] == null || fails[1] == null : "both transactions failed:"
              + "\n  fails[0]: " + fails[0] + "\n  fails[1]: " + fails[1];
        }
        assert fails[0] != null || fails[1] != null : "both transactions succeeded";
        this.log.info("exactly one transaction failed:\n  fails[0]: " + fails[0] + "\n  fails[1]: " + fails[1]);

        // Verify the resulting change is consistent with the tx that succeeded
        final byte[] expected = fails[0] == null ? b("01") : fails[1] == null ? b("02") : null;
        if (expected != null) {
            final KVTransaction tx2 = store.createTransaction();
            this.showKV(tx2, "TX2 of " + store);
            byte[] x = this.executor.submit(new Reader(tx2, b("10"))).get();
            Assert.assertEquals(x, expected);
            tx2.rollback();
        }
        this.log.info("finished testConflictingTransactions() on " + store);
    }

    protected boolean allowBothTransactionsToFail() {
        return false;
    }

    @Test(dataProvider = "kvdbs")
    public void testNonconflictingTransactions(KVDatabase store) throws Exception {

        // Clear database
        this.log.info("starting testNonconflictingTransactions() on " + store);
        this.try3times(store, new Transactional() {
            @Override
            public Void transact(KVTransaction tx) {
                tx.removeRange(null, null);
                return null;
            }
        });

        // Multiple concurrent transactions with overlapping read ranges and non-intersecting write ranges
        int done = 0;
        KVTransaction[] txs = new KVTransaction[10];
        for (int i = 0; i < txs.length; i++)
            txs[i] = store.createTransaction();
        while (true) {
            boolean finished = true;
            for (int i = 0; i < txs.length; i++) {
                if (txs[i] == null)
                    continue;
                finished = false;
                Future rf = this.executor.submit(new Reader(txs[i], new byte[] { (byte)i }, true));
                Future wf = this.executor.submit(new Writer(txs[i], new byte[] { (byte)(i + 128) }, b("02")));
                for (Future f : new Future[] { rf, wf }) {
                    try {
                        f.get();
                    } catch (ExecutionException e) {
                        if (e.getCause() instanceof RetryTransactionException) {
                            txs[i] = store.createTransaction();
                            break;
                        }
                        throw e;
                    }
                }
            }
            if (finished)
                break;
            for (int i = 0; i < txs.length; i++) {
                if (txs[i] == null)
                    continue;
                try {
                    txs[i].commit();
                } catch (RetryTransactionException e) {
                    txs[i] = store.createTransaction();
                    continue;
                }
                txs[i] = null;
            }
        }
        this.log.info("finished testNonconflictingTransactions() on " + store);
    }

    /**
     * This test runs transactions in parallel and verifies there is no "leakage" between them.
     * Database must be configured for linearizable isolation.
     *
     * @param store underlying store
     * @throws Exception if an error occurs
     */
    @Test(dataProvider = "kvdbs")
    public void testParallelTransactions(KVDatabase store) throws Exception {
        this.log.info("starting testParallelTransactions() on " + store);
        for (int count = 0; count < 25; count++) {
            final RandomTask[] tasks = new RandomTask[25];
            for (int i = 0; i < tasks.length; i++) {
                tasks[i] = new RandomTask(i, store, this.random.nextLong());
                tasks[i].start();
            }
            for (int i = 0; i < tasks.length; i++)
                tasks[i].join();
            for (int i = 0; i < tasks.length; i++) {
                final Throwable fail = tasks[i].getFail();
                if (fail != null)
                    throw new Exception("task #" + i + " failed: >>>" + this.show(fail).trim() + "<<<");
            }
        }
        this.log.info("finished testParallelTransactions() on " + store);
        if (store instanceof Closeable)
            ((Closeable)store).close();
    }

    /**
     * This test runs transactions sequentially and verifies that each transaction sees
     * the changes that were committed in the previous transaction.
     *
     * @param store underlying store
     * @throws Exception if an error occurs
     */
    @Test(dataProvider = "kvdbs")
    public void testSequentialTransactions(KVDatabase store) throws Exception {
        this.log.info("starting testSequentialTransactions() on " + store);

        // Clear database
        this.try3times(store, new Transactional() {
            @Override
            public Void transact(KVTransaction tx) {
                tx.removeRange(null, null);
                return null;
            }
        });

        // Keep an in-memory record of what is in the committed database
        final TreeMap committedData = new TreeMap(ByteUtil.COMPARATOR);

        // Run transactions
        for (int i = 0; i < 50; i++) {
            final RandomTask task = new RandomTask(i, store, committedData, this.random.nextLong());
            task.run();
            final Throwable fail = task.getFail();
            if (fail != null)
                throw new Exception("task #" + i + " failed: >>>" + this.show(fail).trim() + "<<<");
        }
        this.log.info("finished testSequentialTransactions() on " + store);
    }

    protected  V try3times(KVDatabase kvdb, Transactional transactional) {
        RetryTransactionException retry = null;
        for (int count = 0; count < 3; count++) {
            final KVTransaction tx = kvdb.createTransaction();
            try {
                final V result = transactional.transact(tx);
                tx.commit();
                return result;
            } catch (RetryTransactionException e) {
                KVDatabaseTest.this.log.debug("retry #" + (count + 1) + " on " + e);
                retry = e;
            }
            try {
                Thread.sleep(100 + count * 200);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        throw retry;
    }

    protected interface Transactional {
        V transact(KVTransaction kvt);
    }

// RandomTask

    public class RandomTask extends Thread {

        private final int id;
        private final KVDatabase store;
        private final Random random;
        private final TreeMap committedData;            // tracks actual committed data, if known
        private final NavigableMap committedDataView;
        private final Converter converter = ByteUtil.STRING_CONVERTER.reverse();

        private Throwable fail;

        public RandomTask(int id, KVDatabase store, long seed) {
            this(id, store, null, seed);
        }

        public RandomTask(int id, KVDatabase store, TreeMap committedData, long seed) {
            super("Random[" + id + "]");
            this.id = id;
            this.store = store;
            this.committedData = committedData;
            this.committedDataView = this.stringView(this.committedData);
            this.random = new Random(seed);
            this.log("seed = " + seed);
        }

        @Override
        public void run() {
            try {
                this.test();
                this.log("succeeded");
            } catch (Throwable t) {
                final ByteArrayOutputStream buf = new ByteArrayOutputStream();
                t.printStackTrace(new PrintStream(buf, true));
                this.log("failed: " + t + "\n" + new String(buf.toByteArray(), StandardCharsets.UTF_8));
                this.fail = t;
            }
        }

        public Throwable getFail() {
            return this.fail;
        }

        @SuppressWarnings("unchecked")
        private void test() throws Exception {

            // Keep track of key/value pairs that we know should exist in the transaction
            final TreeMap knownValues = new TreeMap(ByteUtil.COMPARATOR);
            final NavigableMap knownValuesView = this.stringView(knownValues);

            // Create transaction
            final KVTransaction tx = this.store.createTransaction();

            // Load actual committed database contents (if known) into "known values" tracker
            if (this.committedData != null)
                knownValues.putAll(this.committedData);

            // Save a copy of committed data
            final TreeMap previousCommittedData = this.committedData != null ?
              (TreeMap)this.committedData.clone() : null;
            final NavigableMap previousCommittedDataView = this.stringView(previousCommittedData);

            // Verify committed data is accurate before starting
            if (this.committedData != null)
                Assert.assertEquals(this.stringView(this.readDatabase(tx)), knownValuesView);

            // Note: if this.committedData != null, then knownValues will exactly track the transaction, otherwise,
            // knownValues only contains values we know are in there; nothing is known about uncontained values.

            // Make a bunch of random changes
            Boolean committed = null;
            try {
                final int limit = this.r(1000);
                for (int j = 0; j < limit; j++) {
                    byte[] key;
                    byte[] val;
                    byte[] min;
                    byte[] max;
                    KVPair pair;
                    int option = this.r(62);
                    boolean knownValuesChanged = false;
                    if (option < 10) {                                              // get
                        key = this.rb(2, false);
                        val = tx.get(key);
                        this.log("get: " + s(key) + " -> " + s(val));
                        if (val == null)
                            Assert.assertTrue(!knownValues.containsKey(key));
                        else if (knownValues.containsKey(key))
                            Assert.assertEquals(s(knownValues.get(key)), s(val));
                        else {
                            knownValues.put(key, val);
                            knownValuesChanged = true;
                        }
                    } else if (option < 20) {                                       // put
                        key = this.rb(2, false);
                        val = this.rb(2, true);
                        this.log("put: " + s(key) + " -> " + s(val));
                        tx.put(key, val);
                        knownValues.put(key, val);
                        knownValuesChanged = true;
                    } else if (option < 30) {                                       // getAtLeast
                        min = this.rb(2, true);
                        pair = tx.getAtLeast(min);
                        this.log("getAtLeast: " + s(min) + " -> " + s(pair));
                        if (pair == null)
                            Assert.assertTrue(knownValues.tailMap(min).isEmpty());
                        else if (knownValues.containsKey(pair.getKey()))
                            Assert.assertEquals(s(knownValues.get(pair.getKey())), s(pair.getValue()));
                        else {
                            knownValues.put(pair.getKey(), pair.getValue());
                            knownValuesChanged = true;
                        }
                    } else if (option < 40) {                                       // getAtMost
                        max = this.rb(2, true);
                        pair = tx.getAtMost(max);
                        this.log("getAtMost: " + s(max) + " -> " + s(pair));
                        if (pair == null) {
                            Assert.assertTrue(knownValues.headMap(max).isEmpty(),
                              "getAtMost(" + s(max) + ") returned null but knownValues has " + knownValuesView);
                        } else if (knownValues.containsKey(pair.getKey()))
                            Assert.assertEquals(s(knownValues.get(pair.getKey())), s(pair.getValue()));
                        else {
                            knownValues.put(pair.getKey(), pair.getValue());
                            knownValuesChanged = true;
                        }
                    } else if (option < 50) {                                       // remove
                        key = this.rb(2, false);
                        if (this.r(5) == 0 && (pair = tx.getAtLeast(this.rb(1, false))) != null)
                            key = pair.getKey();
                        this.log("remove: " + s(key));
                        tx.remove(key);
                        knownValues.remove(key);
                        knownValuesChanged = true;
                    } else if (option < 52) {                                       // removeRange
                        min = this.rb2(2, 20);
                        do {
                            max = this.rb2(2, 30);
                        } while (max != null && min != null && ByteUtil.COMPARATOR.compare(min, max) > 0);
                        this.log("removeRange: " + s(min) + " to " + s(max));
                        tx.removeRange(min, max);
                        if (min == null && max == null)
                            knownValues.clear();
                        else if (min == null)
                            knownValues.headMap(max).clear();
                        else if (max == null)
                            knownValues.tailMap(min).clear();
                        else
                            knownValues.subMap(min, max).clear();
                        knownValuesChanged = true;
                    } else if (option < 60) {                                       // adjustCounter
                        key = this.rb(1, false);
                        key[0] = (byte)(key[0] & 0x0f);
                        val = tx.get(key);
                        long counter = -1;
                        if (val != null) {
                            try {
                                counter = tx.decodeCounter(val);
                                this.log("adj: valid value " + s(val) + " (" + counter + ") at key " + s(key));
                            } catch (IllegalArgumentException e) {
                                this.log("adj: bogus value " + s(val) + " at key " + s(key));
                                val = null;
                            }
                        }
                        if (val == null) {
                            counter = this.random.nextLong();
                            final byte[] encodedCounter = tx.encodeCounter(counter);
                            tx.put(key, encodedCounter);
                            this.log("adj: initialize " + s(key) + " to " + s(encodedCounter));
                        }
                        final long adj = this.random.nextInt(1 << this.random.nextInt(24)) - 1024;
                        final byte[] encodedCounter = tx.encodeCounter(counter + adj);
                        this.log("adj: " + s(key) + " by " + adj + " -> should now be " + s(encodedCounter));
                        tx.adjustCounter(key, adj);
                        knownValues.put(key, encodedCounter);
                        knownValuesChanged = true;
                    } else {                                                        // sleep
                        final int millis = this.r(50);
                        this.log("sleep " + millis + "ms");
                        try {
                            Thread.sleep(millis);
                        } catch (InterruptedException e) {
                            // ignore
                        }
                    }
                    if (knownValuesChanged)
                        this.log("new knownValues: " + knownValuesView);
                }

                // TODO: keep track of removes as well as knownValues
                for (Map.Entry entry : knownValues.entrySet()) {
                    final byte[] key = entry.getKey();
                    final byte[] val = entry.getValue();
                    final byte[] txVal = tx.get(key);
                    Assert.assertEquals(txVal, val, "tx has " + s(txVal) + " for key " + s(key)
                      + " but knownValues has:\n*** KNOWN VALUES: " + knownValuesView);
                }

                // Maybe commit
                final boolean rollback = this.r(5) == 3;
                this.log("about to " + (rollback ? "rollback" : "commit") + ":\n   KNOWN: "
                  + knownValuesView + "\n  COMMITTED: " + committedDataView);
                if (rollback) {
                    tx.rollback();
                    committed = false;
                    this.log("rolled-back");
                } else {
                    tx.commit();
                    committed = true;
                    this.log("committed");
                }

            } catch (TransactionTimeoutException e) {
                this.log("got " + e);
                committed = false;
            } catch (RetryTransactionException e) {             // might have committed, might not have, we don't know for sure
                this.log("got " + e);
            }

            // Doing this should always be allowed and shouldn't affect anything
            tx.rollback();

            // Verify committed database contents are now equal to what's expected
            if (this.committedData != null) {

                // Read actual content
                final TreeMap actual = new TreeMap(ByteUtil.COMPARATOR);
                final NavigableMap actualView = this.stringView(actual);
                actual.putAll(this.readDatabase());

                // Update what we think is in the database and then compare to actual content
                if (Boolean.TRUE.equals(committed)) {

                    // Verify
                    this.log("tx was definitely committed");
                    Assert.assertEquals(actualView, knownValuesView,
                      "\n*** ACTUAL:\n" + actualView + "\n*** EXPECTED:\n" + knownValuesView + "\n");
                } else if (Boolean.FALSE.equals(committed)) {

                    // Verify
                    this.log("tx was definitely rolled back");
                    Assert.assertEquals(actualView, committedDataView,
                      "\n*** ACTUAL:\n" + actualView + "\n*** EXPECTED:\n" + committedDataView + "\n");
                } else {

                    // We don't know whether transaction got committed or not .. check both possibilities
                    final boolean matchCommit = actualView.equals(knownValuesView);
                    final boolean matchRollback = actualView.equals(committedDataView);
                    this.log("tx was either committed (" + matchCommit + ") or rolled back (" + matchRollback + ")");

                    // Verify one or the other
                    assert matchCommit || matchRollback :
                      "\n*** ACTUAL:\n" + actualView
                      + "\n*** COMMIT:\n" + knownValuesView
                      + "\n*** ROLLBACK:\n" + committedDataView + "\n";
                    committed = matchCommit;
                }

                // Update model of database
                if (committed) {
                    this.committedData.clear();
                    this.committedData.putAll(knownValues);
                }
            }
        }

        private NavigableMap stringView(NavigableMap byteMap) {
            if (byteMap == null)
                return null;
            return new ConvertedNavigableMap(byteMap, this.converter, this.converter);
        }

        private TreeMap readDatabase() {
            return KVDatabaseTest.this.try3times(this.store, new Transactional>() {
                @Override
                public TreeMap transact(KVTransaction tx) {
                    return RandomTask.this.readDatabase(tx);
                }
            });
        }

        private TreeMap readDatabase(KVTransaction tx) {
            final TreeMap values = new TreeMap(ByteUtil.COMPARATOR);
            final Iterator i = tx.getRange(null, null, false);
            while (i.hasNext()) {
                final KVPair pair = i.next();
                values.put(pair.getKey(), pair.getValue());
            }
            if (i instanceof AutoCloseable) {
                try {
                    ((AutoCloseable)i).close();
                } catch (Exception e) {
                    // ignore
                }
            }
            return values;
        }

        private void log(String s) {
            if (KVDatabaseTest.this.log.isTraceEnabled())
                KVDatabaseTest.this.log.trace("Random[" + this.id + "]: " + s);
        }

        private int r(int max) {
            return this.random.nextInt(max);
        }

        private byte[] rb(int len, boolean allowFF) {
            final byte[] b = new byte[this.r(len) + 1];
            this.random.nextBytes(b);
            if (!allowFF && b[0] == (byte)0xff)
                b[0] = (byte)random.nextInt(0xff);
            return b;
        }

        private byte[] rb2(int len, int nullchance) {
            if (this.r(nullchance) == 0)
                return null;
            return this.rb(len, true);
        }
    }

// Reader

    public class Reader implements Callable {

        final KVTransaction tx;
        final byte[] key;
        final boolean range;

        public Reader(KVTransaction tx, byte[] key, boolean range) {
            this.tx = tx;
            this.key = key;
            this.range = range;
        }

        public Reader(KVTransaction tx, byte[] key) {
            this(tx, key, false);
        }

        @Override
        public byte[] call() {
            if (this.range) {
                if (KVDatabaseTest.this.log.isTraceEnabled())
                    KVDatabaseTest.this.log.trace("reading at least " + s(this.key) + " in " + this.tx);
                final KVPair pair = this.tx.getAtLeast(this.key);
                KVDatabaseTest.this.log.info("finished reading at least " + s(this.key) + " -> " + pair + " in " + this.tx);
                return pair != null ? pair.getValue() : null;
            } else {
                if (KVDatabaseTest.this.log.isTraceEnabled())
                    KVDatabaseTest.this.log.trace("reading " + s(this.key) + " in " + this.tx);
                final byte[] value = this.tx.get(this.key);
                KVDatabaseTest.this.log.info("finished reading " + s(this.key) + " -> " + s(value) + " in " + this.tx);
                return value;
            }
        }
    }

// Writer

    public class Writer implements Runnable {

        final KVTransaction tx;
        final byte[] key;
        final byte[] value;

        public Writer(KVTransaction tx, byte[] key, byte[] value) {
            this.tx = tx;
            this.key = key;
            this.value = value;
        }

        @Override
        public void run() {
            try {
                KVDatabaseTest.this.log.info("putting " + s(this.key) + " -> " + s(this.value) + " in " + this.tx);
                this.tx.put(this.key, this.value);
            } catch (RuntimeException e) {
                KVDatabaseTest.this.log.info("exception putting " + s(this.key) + " -> " + s(this.value)
                  + " in " + this.tx + ": " + e);
                if (KVDatabaseTest.this.log.isTraceEnabled()) {
                    KVDatabaseTest.this.log.trace(this.tx + " put " + s(this.key) + " -> " + s(this.value)
                      + " failure exception trace:", e);
                }
                throw e;
            }
        }
    }

// Committer

    public class Committer implements Runnable {

        final KVTransaction tx;

        public Committer(KVTransaction tx) {
            this.tx = tx;
        }

        @Override
        public void run() {
            try {
                KVDatabaseTest.this.log.info("committing " + this.tx);
                this.tx.commit();
            } catch (RuntimeException e) {
                KVDatabaseTest.this.log.info("exception committing " + this.tx + ": " + e);
                if (KVDatabaseTest.this.log.isTraceEnabled())
                    KVDatabaseTest.this.log.trace(this.tx + " commit failure exception trace:", e);
                throw e;
            }
        }
    }
}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy