org.akubraproject.txn.derby.TestTransactionalStore.java Source code

Java tutorial

Introduction

Here is the source code for org.akubraproject.txn.derby.TestTransactionalStore.java

Source

/* $HeadURL$
 * $Id$
 *
 * Copyright (c) 2009-2010 DuraSpace
 * http://duraspace.org
 *
 * In collaboration with Topaz Inc.
 * http://www.topazproject.org
 *
 * 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 org.akubraproject.txn.derby;

import java.io.File;
import java.net.URI;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.util.Random;

import org.apache.commons.io.FileUtils;

import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.fail;

import org.akubraproject.Blob;
import org.akubraproject.BlobStoreConnection;
import org.akubraproject.mem.MemBlobStore;
import org.akubraproject.tck.TCKTestSuite;
import org.akubraproject.txn.ConcurrentBlobUpdateException;

/**
 * Unit tests for {@link TransactionalStore}.
 *
 * @author Ronald Tschalr
 */
public class TestTransactionalStore extends TCKTestSuite {
    private final File dbDir;
    private final boolean singleWriter;

    public TestTransactionalStore() throws Exception {
        super(getStore(), getStoreId(), true, false);

        dbDir = getDbDir();
        singleWriter = ((TransactionalStore) store).singleWriter();

        /*
        java.util.logging.LogManager.getLogManager().readConfiguration(
            new java.io.FileInputStream("/tmp/jdklog.properties"));
        */
    }

    private static URI getStoreId() {
        return URI.create("urn:example:txnstore");
    }

    private static File getDbDir() throws Exception {
        File base = new File(System.getProperty("basedir"), "target");
        return new File(base, "txn-db");
    }

    private static TransactionalStore getStore() throws Exception {
        File dbDir = getDbDir();
        FileUtils.deleteDirectory(dbDir);
        dbDir.getParentFile().mkdirs();

        File base = new File(System.getProperty("basedir"), "target");
        System.setProperty("derby.stream.error.file", new File(base, "derby.log").toString());
        return new TransactionalStore(getStoreId(), new MemBlobStore(), dbDir.getAbsolutePath());
    }

    @Override
    protected URI getInvalidId() {
        // one too long
        StringBuilder uri = new StringBuilder("urn:blobIdValidation");
        for (int idx = 0; idx < 98; idx++)
            uri.append("oooooooooo");
        uri.append("x");

        return URI.create(uri.toString() + "x");
    }

    /** all URI's are distinct */
    @Override
    protected URI[] getAliases(URI uri) {
        return new URI[] { uri };
    }

    @Override
    public void testListBlobs() throws Exception {
        super.testListBlobs();

        // test when there are old versions too
        URI id1 = createId("blobBasicList1");
        URI id2 = createId("blobBasicList2");
        createBlob(id1, "hello", true);
        createBlob(id2, "bye", true);

        final boolean[] cv = new boolean[] { false };
        doInThread(new ERunnable() {
            @Override
            public void erun() throws Exception {
                doInTxn(new ConAction() {
                    public void run(BlobStoreConnection con) throws Exception {
                        waitFor(cv, true, 0);
                    }
                }, false);
            }
        });

        listBlobs(getPrefixFor("blobBasicList"), new URI[] { id1, id2 });
        listBlobs(getPrefixFor("blobBasicLisT"), new URI[] {});
        listBlobs(getPrefixFor("blobBasicList2"), new URI[] { id2 });

        deleteBlob(id1, "hello", true);
        listBlobs(getPrefixFor("blobBasicList"), new URI[] { id2 });
        listBlobs(getPrefixFor("blobBasicLisT"), new URI[] {});
        listBlobs(getPrefixFor("blobBasicList2"), new URI[] { id2 });

        deleteBlob(id2, "bye", true);
        listBlobs(getPrefixFor("blobBasicList"), new URI[] {});
        listBlobs(getPrefixFor("blobBasicLisT"), new URI[] {});
        listBlobs(getPrefixFor("blobBasicList2"), new URI[] {});

        createBlob(id1, "hello2", true);
        listBlobs(getPrefixFor("blobBasicList"), new URI[] { id1 });
        listBlobs(getPrefixFor("blobBasicLisT"), new URI[] {});
        listBlobs(getPrefixFor("blobBasicList1"), new URI[] { id1 });

        deleteBlob(id1, "hello2", true);
        listBlobs(getPrefixFor("blobBasicList"), new URI[] {});
        listBlobs(getPrefixFor("blobBasicLisT"), new URI[] {});
        listBlobs(getPrefixFor("blobBasicList1"), new URI[] {});

        notify(cv, true);
    }

    /**
     * Test deletions are done and cleaned up properly under various combinations of
     * creating/moving/deleting blobs.
     */
    @Test(groups = { "blobs" }, dependsOnGroups = { "init" })
    public void testDeleteCleanup() throws Exception {
        final URI id = URI.create("urn:blobBlobDelete1");
        final URI id2 = URI.create("urn:blobBlobDelete2");
        final String body = "value";

        // create-delete in one txn
        doInTxn(new ConAction() {
            public void run(BlobStoreConnection con) throws Exception {
                Blob b = getBlob(con, id, null);
                createBlob(con, b, null);
                deleteBlob(con, b);
            }
        }, true);

        // create-delete-create in one txn
        doInTxn(new ConAction() {
            public void run(BlobStoreConnection con) throws Exception {
                Blob b = getBlob(con, id, null);
                createBlob(con, b, null);
                deleteBlob(con, b);
                createBlob(con, b, null);
            }
        }, true);

        // delete-create-delete in one txn
        doInTxn(new ConAction() {
            public void run(BlobStoreConnection con) throws Exception {
                Blob b = getBlob(con, id, "");
                deleteBlob(con, b);
                createBlob(con, b, null);
                deleteBlob(con, b);
            }
        }, true);

        // create-delete-create-delete in one txn
        doInTxn(new ConAction() {
            public void run(BlobStoreConnection con) throws Exception {
                Blob b = getBlob(con, id, null);
                createBlob(con, b, null);
                deleteBlob(con, b);
                createBlob(con, b, null);
                deleteBlob(con, b);
            }
        }, true);

        // create-update-delete-create-update-delete in one txn
        doInTxn(new ConAction() {
            public void run(BlobStoreConnection con) throws Exception {
                Blob b = getBlob(con, id, null);
                createBlob(con, b, body);
                deleteBlob(con, b);
                createBlob(con, b, body);
                deleteBlob(con, b);
            }
        }, true);

        // create in one, delete in another
        createBlob(id, body, true);
        deleteBlob(id, body, true);

        // create in one, delete + create in another
        createBlob(id, body, true);

        doInTxn(new ConAction() {
            public void run(BlobStoreConnection con) throws Exception {
                Blob b = getBlob(con, id, body);
                deleteBlob(con, b);
                createBlob(con, b, null);
            }
        }, true);

        // delete + create + update in one
        doInTxn(new ConAction() {
            public void run(BlobStoreConnection con) throws Exception {
                Blob b = getBlob(con, id, "");
                deleteBlob(con, b);
                createBlob(con, b, body);
            }
        }, true);

        // update + delete + create + update in one
        doInTxn(new ConAction() {
            public void run(BlobStoreConnection con) throws Exception {
                Blob b = getBlob(con, id, body);
                setBlob(con, b, "foo");
                deleteBlob(con, b);
                createBlob(con, b, body);
            }
        }, true);

        // delete + create + update + delete in one
        doInTxn(new ConAction() {
            public void run(BlobStoreConnection con) throws Exception {
                Blob b = getBlob(con, id, body);
                deleteBlob(con, b);
                createBlob(con, b, body);
                deleteBlob(con, b);
            }
        }, true);

        // create-move-delete in one txn
        doInTxn(new ConAction() {
            public void run(BlobStoreConnection con) throws Exception {
                Blob b = getBlob(con, id, null);
                Blob b2 = getBlob(con, id2, null);

                createBlob(con, b, null);
                moveBlob(con, b, id2, "");
                deleteBlob(con, b2);
            }
        }, true);

        // create in one, move-delete in another txn
        createBlob(id, body, true);

        doInTxn(new ConAction() {
            public void run(BlobStoreConnection con) throws Exception {
                Blob b = getBlob(con, id, body);
                Blob b2 = getBlob(con, id2, null);

                moveBlob(con, b, id2, body);
                deleteBlob(con, b2);
            }
        }, true);

        // create-move-delete-create-move in one txn
        doInTxn(new ConAction() {
            public void run(BlobStoreConnection con) throws Exception {
                Blob b = getBlob(con, id, null);
                Blob b2 = getBlob(con, id2, null);

                createBlob(con, b, null);
                moveBlob(con, b, id2, "");
                deleteBlob(con, b2);
                createBlob(con, b2, null);
                moveBlob(con, b2, id, "");
                setBlob(con, b, body);
            }
        }, true);

        // move-delete-create-move-again-yada-yada...
        doInTxn(new ConAction() {
            public void run(BlobStoreConnection con) throws Exception {
                Blob b = getBlob(con, id, body);
                Blob b2 = getBlob(con, id2, null);

                moveBlob(con, b, id2, body);
                deleteBlob(con, b2);
                createBlob(con, b, null);
                moveBlob(con, b, id2, "");
                moveBlob(con, b2, id, "");
                deleteBlob(con, b);
                createBlob(con, b2, null);
                moveBlob(con, b2, id, "");
                setBlob(con, b, body);
                moveBlob(con, b, id2, body);
                deleteBlob(con, b2);
                createBlob(con, b, body);
            }
        }, true);

        // clean up
        deleteBlob(id, body, true);
        getBlob(id, null, true);

        assertNoBlobs("urn:blobBlobDelete");
    }

    /**
     * Test conflicts between two transactions (creating, updating, or deleting a blob that
     * the other has touched).
     */
    @Test(groups = { "blobs" }, dependsOnGroups = { "init" })
    public void testConflicts() throws Exception {
        if (singleWriter)
            return;

        final URI id1 = URI.create("urn:blobConflict1");
        final URI id2 = URI.create("urn:blobConflict2");
        final String body1 = "original blob";
        final String body11 = "modified blob";
        final String body2 = "create me";
        // create blob1
        createBlob(id1, body1, true);

        // create a set of actions
        ConAction createNoBody = new ConAction() {
            public void run(BlobStoreConnection con) throws Exception {
                Blob b = getBlob(con, id2, null);
                createBlob(con, b, null);
            }
        };

        ConAction createWithBody = new ConAction() {
            public void run(BlobStoreConnection con) throws Exception {
                Blob b = getBlob(con, id2, null);
                createBlob(con, b, body2);
            }
        };

        ConAction delete1 = new ConAction() {
            public void run(BlobStoreConnection con) throws Exception {
                Blob b = getBlob(con, id1, body1);
                deleteBlob(con, b);
            }
        };

        ConAction modify1 = new ConAction() {
            public void run(BlobStoreConnection con) throws Exception {
                Blob b = getBlob(con, id1, body1);
                setBlob(con, b, body11);
            }
        };

        ConAction rename12 = new ConAction() {
            public void run(BlobStoreConnection con) throws Exception {
                Blob b1 = getBlob(con, id1, body1);
                moveBlob(con, b1, id2, body1);
            }
        };

        // test create-create conflict
        testConflict(createNoBody, createNoBody, id2, new ERunnable() {
            @Override
            public void erun() throws Exception {
                getBlob(id2, "", true);
                deleteBlob(id2, "", true);
            }
        });

        testConflict(createWithBody, createNoBody, id2, new ERunnable() {
            @Override
            public void erun() throws Exception {
                getBlob(id2, body2, true);
                deleteBlob(id2, body2, true);
            }
        });

        testConflict(createWithBody, createWithBody, id2, new ERunnable() {
            @Override
            public void erun() throws Exception {
                getBlob(id2, body2, true);
                deleteBlob(id2, body2, true);
            }
        });

        testConflict(createNoBody, createWithBody, id2, new ERunnable() {
            @Override
            public void erun() throws Exception {
                getBlob(id2, "", true);
                deleteBlob(id2, "", true);
            }
        });

        // test delete-delete conflict
        testConflict(delete1, delete1, id1, new ERunnable() {
            @Override
            public void erun() throws Exception {
                getBlob(id1, null, true);
                createBlob(id1, body1, true);
            }
        });

        // test delete-modify conflict
        testConflict(delete1, modify1, id1, new ERunnable() {
            @Override
            public void erun() throws Exception {
                getBlob(id1, null, true);
                createBlob(id1, body1, true);
            }
        });

        testConflict(modify1, delete1, id1, new ERunnable() {
            @Override
            public void erun() throws Exception {
                getBlob(id1, body11, true);
                setBlob(id1, body1, true);
            }
        });

        // test modify-modify conflict
        testConflict(modify1, modify1, id1, new ERunnable() {
            @Override
            public void erun() throws Exception {
                getBlob(id1, body11, true);
                setBlob(id1, body1, true);
            }
        });

        // test rename-rename conflict
        testConflict(rename12, rename12, id1, new ERunnable() {
            @Override
            public void erun() throws Exception {
                getBlob(id2, body1, true);
                renameBlob(id2, id1, body1, true);
            }
        });

        // test rename-modify conflict
        testConflict(rename12, modify1, id1, new ERunnable() {
            @Override
            public void erun() throws Exception {
                getBlob(id2, body1, true);
                renameBlob(id2, id1, body1, true);
            }
        });

        testConflict(modify1, rename12, id1, new ERunnable() {
            @Override
            public void erun() throws Exception {
                getBlob(id1, body11, true);
                setBlob(id1, body1, true);
            }
        });

        // test rename-create conflict
        testConflict(rename12, createNoBody, id2, new ERunnable() {
            @Override
            public void erun() throws Exception {
                getBlob(id2, body1, true);
                renameBlob(id2, id1, body1, true);
            }
        });

        testConflict(createNoBody, rename12, id2, new ERunnable() {
            @Override
            public void erun() throws Exception {
                getBlob(id1, body1, true);
                getBlob(id2, "", true);
                deleteBlob(id2, "", true);
            }
        });

        testConflict(createWithBody, rename12, id2, new ERunnable() {
            @Override
            public void erun() throws Exception {
                getBlob(id1, body1, true);
                getBlob(id2, body2, true);
                deleteBlob(id2, body2, true);
            }
        });

        // test rename-delete conflict
        testConflict(rename12, delete1, id1, new ERunnable() {
            @Override
            public void erun() throws Exception {
                getBlob(id2, body1, true);
                renameBlob(id2, id1, body1, true);
            }
        });

        testConflict(delete1, rename12, id1, new ERunnable() {
            @Override
            public void erun() throws Exception {
                getBlob(id1, null, true);
                createBlob(id1, body1, true);
            }
        });

        // clean up

        deleteBlob(id1, body1, true);
        getBlob(id1, null, true);

        assertNoBlobs("urn:blobConflict");
    }

    private void testConflict(final ConAction first, final ConAction second, final URI id, final ERunnable reset)
            throws Exception {
        final boolean[] cv = new boolean[] { false };
        final boolean[] failed = new boolean[] { false };
        Thread[] threads = new Thread[2];

        // Test two threads, both operations occurring while the other hasn't committed yet
        threads[0] = doInThread(new ERunnable() {
            @Override
            public void erun() throws Exception {
                doInTxn(new ConAction() {
                    public void run(BlobStoreConnection con) throws Exception {
                        notifyAndWait(cv, true);

                        first.run(con);

                        notifyAndWait(cv, true);
                    }
                }, true);

                TestTransactionalStore.notify(cv, true);
            }
        }, failed);

        threads[1] = doInThread(new ERunnable() {
            @Override
            public void erun() throws Exception {
                doInTxn(new ConAction() {
                    public void run(BlobStoreConnection con) throws Exception {
                        waitFor(cv, true, 0);
                        notifyAndWait(cv, false);

                        try {
                            second.run(con);
                            fail("Did not get expected ConcurrentBlobUpdateException");
                        } catch (ConcurrentBlobUpdateException cbue) {
                            assertEquals(cbue.getBlobId(), id);
                        }

                        notifyAndWait(cv, false);
                    }
                }, false);
            }
        }, failed);

        for (int t = 0; t < threads.length; t++)
            threads[t].join();

        assertFalse(failed[0]);

        reset.erun();

        /* Test two threads, both starting, then the first doing its operation and comitting, then the
         * second doing its operation.
         */
        cv[0] = false;

        threads[0] = doInThread(new ERunnable() {
            @Override
            public void erun() throws Exception {
                notifyAndWait(cv, true);

                doInTxn(new ConAction() {
                    public void run(BlobStoreConnection con) throws Exception {
                        notifyAndWait(cv, true);
                        first.run(con);
                    }
                }, true);

                TestTransactionalStore.notify(cv, true);
            }
        }, failed);

        threads[1] = doInThread(new ERunnable() {
            @Override
            public void erun() throws Exception {
                waitFor(cv, true, 0);
                notifyAndWait(cv, false);

                doInTxn(new ConAction() {
                    public void run(BlobStoreConnection con) throws Exception {
                        notifyAndWait(cv, false);
                        try {
                            second.run(con);
                            fail("Did not get expected ConcurrentBlobUpdateException");
                        } catch (ConcurrentBlobUpdateException cbue) {
                            assertEquals(cbue.getBlobId(), id);
                        }
                    }
                }, false);
            }
        }, failed);

        for (int t = 0; t < threads.length; t++)
            threads[t].join();

        assertFalse(failed[0]);

        reset.erun();
    }

    /**
     * Create, get, rename, update, delete in other transactions should not affect current
     * transaction.
     */
    @Test(groups = { "blobs" }, dependsOnGroups = { "init" })
    public void testBasicTransactionIsolation() throws Exception {
        if (singleWriter)
            return;

        final URI id1 = URI.create("urn:blobBasicTxnIsol1");
        final URI id2 = URI.create("urn:blobBasicTxnIsol2");
        final URI id3 = URI.create("urn:blobBasicTxnIsol3");
        final URI id4 = URI.create("urn:blobBasicTxnIsol4");

        final String body1 = "long lived blob";
        final String body2 = "long lived, modified blob";
        final String body3 = "create me";
        final String body4 = "delete me";
        final String body22 = "body1, v2";
        final String body42 = "delete me, v2";
        final String body43 = "delete me, v3";

        // create blob1
        createBlob(id1, body1, true);

        // first start txn1, then run a bunch of other transactions while txn1 is active
        doInTxn(new ConAction() {
            public void run(final BlobStoreConnection con) throws Exception {
                // check our snapshot
                getBlob(con, id1, body1);
                getBlob(con, id2, null);
                getBlob(con, id3, null);
                getBlob(con, id4, null);

                // create another blob and modify
                Blob b = getBlob(con, id2, null);
                createBlob(con, b, body1);
                setBlob(con, b, body2);

                // create a new blob and verify we don't see it but others see it
                boolean[] failed = new boolean[] { false };

                doInThread(new ERunnable() {
                    @Override
                    public void erun() throws Exception {
                        createBlob(id3, body3, true);
                    }
                }, failed).join();
                assertFalse(failed[0]);

                doInThread(new ERunnable() {
                    @Override
                    public void erun() throws Exception {
                        getBlob(id1, body1, true);
                        getBlob(id2, null, true);
                        getBlob(id3, body3, true);
                        getBlob(id4, null, true);
                    }
                }, failed).join();
                assertFalse(failed[0]);

                getBlob(con, id1, body1);
                getBlob(con, id2, body2);
                getBlob(con, id3, null);
                getBlob(con, id4, null);

                // delete the new blob
                doInThread(new ERunnable() {
                    @Override
                    public void erun() throws Exception {
                        deleteBlob(id3, body3, true);
                    }
                }, failed).join();
                assertFalse(failed[0]);

                doInThread(new ERunnable() {
                    @Override
                    public void erun() throws Exception {
                        getBlob(id1, body1, true);
                        getBlob(id2, null, true);
                        getBlob(id3, null, true);
                        getBlob(id4, null, true);
                    }
                }, failed).join();
                assertFalse(failed[0]);

                getBlob(con, id1, body1);
                getBlob(con, id2, body2);
                getBlob(con, id3, null);
                getBlob(con, id4, null);

                // delete the first blob
                doInThread(new ERunnable() {
                    @Override
                    public void erun() throws Exception {
                        deleteBlob(id1, body1, true);
                    }
                }, failed).join();
                assertFalse(failed[0]);

                doInThread(new ERunnable() {
                    @Override
                    public void erun() throws Exception {
                        getBlob(id1, null, true);
                        getBlob(id2, null, true);
                        getBlob(id3, null, true);
                        getBlob(id4, null, true);
                    }
                }, failed).join();
                assertFalse(failed[0]);

                getBlob(con, id1, body1);
                getBlob(con, id2, body2);
                getBlob(con, id3, null);
                getBlob(con, id4, null);

                // re-create the first blob, but with different content
                doInThread(new ERunnable() {
                    @Override
                    public void erun() throws Exception {
                        createBlob(id1, body22, true);
                    }
                }, failed).join();
                assertFalse(failed[0]);

                doInThread(new ERunnable() {
                    @Override
                    public void erun() throws Exception {
                        getBlob(id1, body22, true);
                        getBlob(id2, null, true);
                        getBlob(id3, null, true);
                        getBlob(id4, null, true);
                    }
                }, failed).join();
                assertFalse(failed[0]);

                getBlob(con, id1, body1);
                getBlob(con, id2, body2);
                getBlob(con, id3, null);
                getBlob(con, id4, null);

                // step through, making sure we don't see anything from active transactions
                final boolean[] cv = new boolean[1];
                Thread t = doInThread(new ERunnable() {
                    @Override
                    public void erun() throws Exception {
                        doInTxn(new ConAction() {
                            public void run(BlobStoreConnection c2) throws Exception {
                                Blob b = getBlob(c2, id4, null);
                                createBlob(c2, b, body4);

                                notifyAndWait(cv, true);

                                deleteBlob(c2, b);

                                notifyAndWait(cv, true);

                                createBlob(c2, b, body42);

                                notifyAndWait(cv, true);

                                setBlob(c2, b, body43);

                                notifyAndWait(cv, true);
                            }
                        }, true);
                    }
                }, failed);

                waitFor(cv, true, 0);

                assertFalse(con.getBlob(id4, null).exists());

                notifyAndWait(cv, false);

                assertFalse(con.getBlob(id4, null).exists());

                notifyAndWait(cv, false);

                assertFalse(con.getBlob(id4, null).exists());

                notifyAndWait(cv, false);

                assertFalse(con.getBlob(id4, null).exists());

                TestTransactionalStore.notify(cv, false);
                t.join();
                assertFalse(failed[0]);

                assertFalse(con.getBlob(id4, null).exists());
            }
        }, true);

        deleteBlob(id1, body22, true);
        deleteBlob(id2, body2, true);
        deleteBlob(id3, null, true);
        deleteBlob(id4, body43, true);

        assertNoBlobs("urn:blobBasicTxnIsol");
    }

    /**
     * Test single stepping two transactions.
     */
    @Test(groups = { "blobs" }, dependsOnGroups = { "init" })
    public void testTransactionIsolation2() throws Exception {
        if (singleWriter)
            return;

        final URI id1 = URI.create("urn:blobTxnIsol2_1");
        final URI id2 = URI.create("urn:blobTxnIsol2_2");
        final URI id3 = URI.create("urn:blobTxnIsol2_3");
        final URI id4 = URI.create("urn:blobTxnIsol2_4");
        final URI id5 = URI.create("urn:blobTxnIsol2_5");
        final URI id6 = URI.create("urn:blobTxnIsol2_6");

        final String body1 = "blob1";
        final String body2 = "blob2";
        final String body3 = "blob3";
        final String body4 = "blob4";

        final boolean[] failed = new boolean[] { false };
        final boolean[] cv = new boolean[] { false };
        final Thread[] threads = new Thread[2];

        // 2 threads, which will single-step, each doing a step and then waiting for the other
        for (int t = 0; t < threads.length; t++) {
            final URI[] ids = new URI[] { t == 0 ? id1 : id3, t == 0 ? id2 : id4 };
            final URI[] oids = new URI[] { t != 0 ? id1 : id3, t != 0 ? id2 : id4 };
            final URI tid = t == 0 ? id5 : id6;
            final String[] bodies = new String[] { t == 0 ? body1 : body3, t == 0 ? body2 : body4 };
            final String[] obodies = new String[] { t != 0 ? body1 : body3, t != 0 ? body2 : body4 };
            final boolean cvVal = (t == 0);

            threads[t] = doInThread(new ERunnable() {
                @Override
                public void erun() throws Exception {
                    // create two blobs
                    doInTxn(new ConAction() {
                        public void run(final BlobStoreConnection con) throws Exception {
                            getBlob(con, id1, false);
                            getBlob(con, id2, false);
                            getBlob(con, id3, false);
                            getBlob(con, id4, false);

                            waitFor(cv, cvVal, 0);

                            notifyAndWait(cv, !cvVal);

                            for (int idx = 0; idx < 2; idx++) {
                                Blob b = getBlob(con, ids[idx], null);
                                createBlob(con, b, null);

                                for (int idx2 = 0; idx2 < 2; idx2++)
                                    getBlob(con, oids[idx2], false);

                                notifyAndWait(cv, !cvVal);

                                setBlob(con, b, bodies[idx]);

                                notifyAndWait(cv, !cvVal);
                            }
                        }
                    }, true);

                    notifyAndWait(cv, !cvVal);

                    // exchange the two blobs
                    doInTxn(new ConAction() {
                        public void run(final BlobStoreConnection con) throws Exception {
                            getBlob(con, id1, body1);
                            getBlob(con, id2, body2);
                            getBlob(con, id3, body3);
                            getBlob(con, id4, body4);

                            notifyAndWait(cv, !cvVal);

                            URI[][] seq = new URI[][] { { ids[0], tid }, { ids[1], ids[0] }, { tid, ids[1] }, };

                            for (int idx = 0; idx < seq.length; idx++) {
                                Blob bs = getBlob(con, seq[idx][0], true);
                                moveBlob(con, bs, seq[idx][1], null);

                                notifyAndWait(cv, !cvVal);
                            }
                        }
                    }, true);

                    notifyAndWait(cv, !cvVal);

                    // delete the two blobs
                    doInTxn(new ConAction() {
                        public void run(final BlobStoreConnection con) throws Exception {
                            getBlob(con, id1, body2);
                            getBlob(con, id2, body1);
                            getBlob(con, id3, body4);
                            getBlob(con, id4, body3);

                            notifyAndWait(cv, !cvVal);

                            for (int idx = 0; idx < 2; idx++) {
                                Blob b = getBlob(con, ids[idx], bodies[1 - idx]);
                                deleteBlob(con, b);

                                for (int idx2 = 0; idx2 < 2; idx2++)
                                    getBlob(con, oids[idx2], obodies[1 - idx2]);

                                notifyAndWait(cv, !cvVal);
                            }
                        }
                    }, true);

                    notifyAndWait(cv, !cvVal);
                    TestTransactionalStore.notify(cv, !cvVal);
                }
            }, failed);
        }

        for (int t = 0; t < threads.length; t++)
            threads[t].join();

        assertFalse(failed[0]);

        assertNoBlobs("urn:blobTxnIsol2_");
    }

    /**
     * Stress test the stuff a bit.
     */
    @Test(groups = { "blobs" }, dependsOnGroups = { "init" })
    public void stressTest() throws Exception {
        // get our config
        final int numFillers = Integer.getInteger("akubra.txn.test.numFillers", 0);
        final int numReaders = Integer.getInteger("akubra.txn.test.numReaders", 10);
        final int numWriters = Integer.getInteger("akubra.txn.test.numWriters", 10);
        final int numObjects = Integer.getInteger("akubra.txn.test.numObjects", 10);
        final int numRounds = Integer.getInteger("akubra.txn.test.numRounds", 10);

        long t0 = System.currentTimeMillis();

        // "fill" the db a bit
        for (int b = 0; b < numFillers / 1000; b++) {
            final int start = b * 1000;

            doInTxn(new ConAction() {
                public void run(BlobStoreConnection con) throws Exception {
                    for (int idx = start; idx < start + 1000; idx++) {
                        Blob b = con.getBlob(URI.create("urn:blobStressTestFiller" + idx), null);
                        setBody(b, "v" + idx);
                    }
                }
            }, true);
        }

        long t1 = System.currentTimeMillis();

        // set up
        Thread[] writers = new Thread[numWriters];
        Thread[] readers = new Thread[numReaders];
        boolean[] failed = new boolean[] { false };

        final boolean[] testDone = new boolean[] { false };
        final int[] lowIds = new int[numWriters];
        final int[] highId = new int[] { 0 };

        // start/run the writers
        for (int t = 0; t < writers.length; t++) {
            final int tid = t;
            final int start = t * numRounds * numObjects;

            writers[t] = doInThread(new ERunnable() {
                @Override
                public void erun() throws Exception {
                    for (int r = 0; r < numRounds; r++) {
                        final int off = start + r * numObjects;

                        doInTxn(new ConAction() {
                            public void run(BlobStoreConnection con) throws Exception {
                                for (int o = 0; o < numObjects; o++) {
                                    int idx = off + o;
                                    URI id = URI.create("urn:blobStressTest" + idx);
                                    String val = "v" + idx;

                                    Blob b = getBlob(con, id, null);
                                    createBlob(con, b, val);
                                }
                            }
                        }, true);

                        synchronized (testDone) {
                            highId[0] = Math.max(highId[0], off + numObjects);
                        }

                        doInTxn(new ConAction() {
                            public void run(BlobStoreConnection con) throws Exception {
                                for (int o = 0; o < numObjects; o++) {
                                    int idx = off + o;
                                    URI id = URI.create("urn:blobStressTest" + idx);
                                    String val = "v" + idx;

                                    Blob b = getBlob(con, id, val);
                                    deleteBlob(con, b);
                                }
                            }
                        }, true);

                        synchronized (testDone) {
                            lowIds[tid] = off + numObjects;
                        }
                    }
                }
            }, failed);
        }

        // start/run the readers
        for (int t = 0; t < readers.length; t++) {
            readers[t] = doInThread(new ERunnable() {
                @Override
                public void erun() throws Exception {
                    final Random rng = new Random();
                    final int[] found = new int[] { 0 };

                    while (true) {
                        final int low, high;
                        synchronized (testDone) {
                            if (testDone[0])
                                break;

                            high = highId[0];

                            int tmp = Integer.MAX_VALUE;
                            for (int id : lowIds)
                                tmp = Math.min(tmp, id);
                            low = tmp;
                        }

                        if (low == high) {
                            Thread.yield();
                            continue;
                        }

                        doInTxn(new ConAction() {
                            public void run(BlobStoreConnection con) throws Exception {
                                for (int o = 0; o < numObjects; o++) {
                                    int idx = rng.nextInt(high - low) + low;
                                    URI id = URI.create("urn:blobStressTest" + idx);
                                    String val = "v" + idx;

                                    Blob b = con.getBlob(id, null);
                                    if (b.exists()) {
                                        assertEquals(getBody(b), val);
                                        found[0]++;
                                    }
                                }
                            }
                        }, true);
                    }

                    if (found[0] == 0)
                        System.out.println("Warning: this reader found no blobs");
                }
            }, failed);
        }

        // wait for things to end
        for (int t = 0; t < writers.length; t++)
            writers[t].join();

        synchronized (testDone) {
            testDone[0] = true;
        }

        for (int t = 0; t < readers.length; t++)
            readers[t].join();

        long t2 = System.currentTimeMillis();

        // remove the fillers again
        for (int b = 0; b < numFillers / 1000; b++) {
            final int start = b * 1000;

            doInTxn(new ConAction() {
                public void run(BlobStoreConnection con) throws Exception {
                    for (int idx = start; idx < start + 1000; idx++)
                        con.getBlob(URI.create("urn:blobStressTestFiller" + idx), null).delete();
                }
            }, true);
        }

        long t3 = System.currentTimeMillis();

        System.out.println("Time to create " + numFillers + " fillers: " + ((t1 - t0) / 1000.) + " s");
        System.out.println("Time to remove " + numFillers + " fillers: " + ((t3 - t2) / 1000.) + " s");
        System.out.println("Time to run test (" + numWriters + "/" + numRounds + "/" + numObjects + "): "
                + ((t2 - t1) / 1000.) + " s");

        assertFalse(failed[0]);
    }

    /**
     * Test that things get cleaned up. This runs after all other tests that create or otherwise
     * manipulate blobs.
     */
    @Override
    public void testCleanup() throws Exception {
        super.testCleanup();

        // verify that the tables are truly empty
        Connection connection = DriverManager.getConnection("jdbc:derby:" + dbDir);
        ResultSet rs = connection.createStatement().executeQuery("SELECT * FROM " + TransactionalStore.NAME_TABLE);
        assertFalse(rs.next(), "unexpected entries in name-map table;");

        rs = connection.createStatement().executeQuery("SELECT * FROM " + TransactionalStore.DEL_TABLE);
        assertFalse(rs.next(), "unexpected entries in deleted-list table;");
    }

    private void doInTxn(ConAction a, boolean commit) throws Exception {
        tm.begin();
        BlobStoreConnection con = store.openConnection(tm.getTransaction(), null);

        try {
            a.run(con);

            if (commit)
                tm.commit();
            else
                tm.rollback();
        } finally {
            if (tm.getTransaction() != null) {
                try {
                    tm.rollback();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

            con.close();
        }
    }
}