com.emc.ecs.sync.storage.CasStorageTest.java Source code

Java tutorial

Introduction

Here is the source code for com.emc.ecs.sync.storage.CasStorageTest.java

Source

/*
 * Copyright 2013-2016 EMC Corporation. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 * http://www.apache.org/licenses/LICENSE-2.0.txt
 *
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */
package com.emc.ecs.sync.storage;

import com.emc.ecs.sync.EcsSync;
import com.emc.ecs.sync.config.SyncConfig;
import com.emc.ecs.sync.config.SyncOptions;
import com.emc.ecs.sync.config.storage.CasConfig;
import com.emc.ecs.sync.rest.LogLevel;
import com.emc.ecs.sync.service.SyncJobService;
import com.emc.ecs.sync.test.ByteAlteringFilter;
import com.emc.ecs.sync.test.TestConfig;
import com.emc.ecs.sync.util.Iso8601Util;
import com.filepool.fplibrary.*;
import org.apache.commons.codec.binary.Hex;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class CasStorageTest {
    private static final Logger log = LoggerFactory.getLogger(CasStorageTest.class);

    private static final int CLIP_OPTIONS = 0;
    private static final int BUFFER_SIZE = 1048576; // 1MB
    private static final int CAS_THREADS = 32;
    private static final int CAS_SETUP_WAIT_MINUTES = 5;

    private String connectString1, connectString2;
    private LogLevel logLevel;

    @Before
    public void setup() throws Exception {
        logLevel = SyncJobService.getInstance().getLogLevel();
        SyncJobService.getInstance().setLogLevel(LogLevel.verbose);
        try {
            Properties syncProperties = TestConfig.getProperties();

            connectString1 = syncProperties.getProperty(TestConfig.PROP_CAS_CONNECT_STRING);
            connectString2 = syncProperties.getProperty(TestConfig.PROP_CAS_CONNECT_STRING + "2");

            Assume.assumeNotNull(connectString1, connectString2);
        } catch (FileNotFoundException e) {
            Assume.assumeFalse("Could not load ecs-sync.properties", true);
        }
    }

    public void tearDown() throws Exception {
        SyncJobService.getInstance().setLogLevel(logLevel);
    }

    @Test
    public void testPipedStreams() throws Exception {
        Random random = new Random();

        // test smaller than pipe buffer
        byte[] source = new byte[random.nextInt(BUFFER_SIZE) + 1];
        random.nextBytes(source);
        String md5 = Hex.encodeHexString(MessageDigest.getInstance("MD5").digest(source));
        Assert.assertEquals("MD5 mismatch", md5, pipeAndGetMd5(source));

        // test larger than pipe buffer
        source = new byte[random.nextInt(BUFFER_SIZE) + BUFFER_SIZE + 1];
        random.nextBytes(source);
        md5 = Hex.encodeHexString(MessageDigest.getInstance("MD5").digest(source));
        Assert.assertEquals("MD5 mismatch", md5, pipeAndGetMd5(source));
    }

    private String pipeAndGetMd5(byte[] source) throws Exception {
        PipedInputStream pin = new PipedInputStream(BUFFER_SIZE);
        PipedOutputStream pout = new PipedOutputStream(pin);

        Producer producer = new Producer(source, pout);

        // produce in parallel
        Thread producerThread = new Thread(producer);
        producerThread.start();

        // consume inside this thread
        byte[] dest = new byte[source.length];
        try {
            int read = 0;
            while (read < dest.length && read != -1) {
                read += pin.read(dest, read, dest.length - read);
            }
        } finally {
            try {
                pin.close();
            } catch (Throwable t) {
                // ignore
            }
        }

        // synchronize
        producerThread.join();

        return Hex.encodeHexString(MessageDigest.getInstance("MD5").digest(dest));
    }

    @Test
    public void testCasSingleObject() throws Exception {
        FPPool sourcePool = new FPPool(connectString1);
        FPPool targetPool = new FPPool(connectString2);

        try {
            // create clip in source (<=1MB blob size) - capture summary for comparison
            StringWriter sourceSummary = new StringWriter();
            List<String> clipIds = createTestClips(sourcePool, 1048576, 1, sourceSummary);
            String clipID = clipIds.iterator().next();

            // open clip in source
            FPClip clip = new FPClip(sourcePool, clipID, FPLibraryConstants.FP_OPEN_FLAT);

            // buffer CDF
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            clip.RawRead(baos);

            // write CDF to target
            FPClip targetClip = new FPClip(targetPool, clipID, new ByteArrayInputStream(baos.toByteArray()),
                    CLIP_OPTIONS);

            // migrate blobs
            FPTag tag, targetTag;
            int tagCount = 0;
            while ((tag = clip.FetchNext()) != null) {
                targetTag = targetClip.FetchNext();
                Assert.assertEquals("Tag names don't match", tag.getTagName(), targetTag.getTagName());
                Assert.assertTrue("Tag " + tag.getTagName() + " attributes not equal",
                        Arrays.equals(tag.getAttributes(), targetTag.getAttributes()));

                int blobStatus = tag.BlobExists();
                if (blobStatus == 1) {
                    PipedInputStream pin = new PipedInputStream(BUFFER_SIZE);
                    PipedOutputStream pout = new PipedOutputStream(pin);
                    BlobReader reader = new BlobReader(tag, pout);

                    // start reading in parallel
                    Thread readThread = new Thread(reader);
                    readThread.start();

                    // write inside this thread
                    targetTag.BlobWrite(pin);

                    readThread.join(); // this shouldn't do anything, but just in case

                    if (!reader.isSuccess())
                        throw new Exception("blob read failed", reader.getError());
                } else {
                    if (blobStatus != -1)
                        System.out.println("blob unavailable, clipId=" + clipID + ", tagNum=" + tagCount
                                + ", blobStatus=" + blobStatus);
                }
                tag.Close();
                targetTag.Close();
                tagCount++;
            }

            clip.Close();

            Assert.assertEquals("clip IDs not equal", clipID, targetClip.Write());
            targetClip.Close();

            // check target blob data
            targetClip = new FPClip(targetPool, clipID, FPLibraryConstants.FP_OPEN_FLAT);
            Assert.assertEquals("content mismatch", sourceSummary.toString(), summarizeClip(targetClip));
            targetClip.Close();

            // delete in source and target
            FPClip.Delete(sourcePool, clipID);
            FPClip.Delete(targetPool, clipID);
        } finally {
            try {
                sourcePool.Close();
            } catch (Throwable t) {
                log.warn("failed to close source pool", t);
            }
            try {
                targetPool.Close();
            } catch (Throwable t) {
                log.warn("failed to close dest pool", t);
            }
        }
    }

    @Test
    public void testReopenClip() throws Exception {
        FPPool pool = new FPPool(connectString1);
        String clipID = null;

        try {
            // create clip in source (<=1MB blob size) - capture summary for comparison
            StringWriter sourceSummary = new StringWriter();
            List<String> clipIds = createTestClips(pool, 1048576, 1, sourceSummary);
            clipID = clipIds.iterator().next();

            // open clip
            FPClip clip = new FPClip(pool, clipID);
            long size = clip.getTotalSize();
            log.info("clip {} has total size {} bytes", clipID, size);
            // close clip
            clip.Close();

            // reopen clip
            clip = new FPClip(pool, clipID, FPLibraryConstants.FP_OPEN_FLAT);
            Assert.assertNotNull(clip);
            clip.Close();
        } finally {
            if (clipID != null)
                FPClip.Delete(pool, clipID);
            try {
                pool.Close();
            } catch (Throwable t) {
                log.warn("failed to close pool", t);
            }
        }
    }

    @Test
    public void testSyncSingleClip() throws Exception {
        testSyncClipList(1, 102400);
    }

    @Test
    public void testSyncClipListSmallBlobs() throws Exception {
        int numClips = 250, maxBlobSize = 102400;
        testSyncClipList(numClips, maxBlobSize);
    }

    @Test
    public void testSyncClipListLargeBlobs() throws Exception {
        int numClips = 25, maxBlobSize = 2048000;
        testSyncClipList(numClips, maxBlobSize);
    }

    private void testSyncClipList(int numClips, int maxBlobSize) throws Exception {
        FPPool sourcePool = new FPPool(connectString1);
        FPPool destPool = new FPPool(connectString2);

        // create random data (capture summary for comparison)
        StringWriter sourceSummary = new StringWriter();
        List<String> clipIds = createTestClips(sourcePool, maxBlobSize, numClips, sourceSummary);

        try {
            // write clip file
            File clipFile = File.createTempFile("clip", "lst");
            clipFile.deleteOnExit();
            BufferedWriter writer = new BufferedWriter(new FileWriter(clipFile));
            for (String clipId : clipIds) {
                log.debug("created {}", clipId);
                writer.write(clipId);
                writer.newLine();
            }
            writer.close();

            EcsSync sync = createEcsSync(connectString1, connectString2, CAS_THREADS, true);
            sync.getSyncConfig().getOptions().setSourceListFile(clipFile.getAbsolutePath());

            run(sync);

            Assert.assertEquals(0, sync.getStats().getObjectsFailed());
            Assert.assertEquals(numClips, sync.getStats().getObjectsComplete());

            String destSummary = summarize(destPool, clipIds);
            Assert.assertEquals("query summaries different", sourceSummary.toString(), destSummary);
        } finally {
            delete(sourcePool, clipIds);
            delete(destPool, clipIds);
            try {
                sourcePool.Close();
            } catch (Throwable t) {
                log.warn("failed to close source pool", t);
            }
            try {
                destPool.Close();
            } catch (Throwable t) {
                log.warn("failed to close dest pool", t);
            }
        }
    }

    @Test
    public void testVerify() throws Exception {
        FPPool sourcePool = new FPPool(connectString1);
        FPPool destPool = new FPPool(connectString2);

        // create random data (capture summary for comparison)
        StringWriter sourceSummary = new StringWriter();
        List<String> clipIds = createTestClips(sourcePool, 10240, 250, sourceSummary);

        try {
            // write clip file
            File clipFile = File.createTempFile("clip", "lst");
            clipFile.deleteOnExit();
            BufferedWriter writer = new BufferedWriter(new FileWriter(clipFile));
            for (String clipId : clipIds) {
                writer.write(clipId);
                writer.newLine();
            }
            writer.close();

            // test sync with verify
            EcsSync sync = createEcsSync(connectString1, connectString2, CAS_THREADS, true);
            sync.getSyncConfig().getOptions().setSourceListFile(clipFile.getAbsolutePath());
            sync.getSyncConfig().getOptions().setVerify(true);

            run(sync);

            Assert.assertEquals(0, sync.getStats().getObjectsFailed());

            // test verify only
            sync = createEcsSync(connectString1, connectString2, CAS_THREADS, true);
            sync.getSyncConfig().getOptions().setSourceListFile(clipFile.getAbsolutePath());
            sync.getSyncConfig().getOptions().setVerifyOnly(true);

            run(sync);

            Assert.assertEquals(0, sync.getStats().getObjectsFailed());

            // delete clips from both
            delete(sourcePool, clipIds);
            delete(destPool, clipIds);

            // create new clips (ECS has a problem reading previously deleted and recreated clip IDs)
            clipIds = createTestClips(sourcePool, 10240, 250, sourceSummary);
            writer = new BufferedWriter(new FileWriter(clipFile));
            for (String clipId : clipIds) {
                writer.write(clipId);
                writer.newLine();
            }
            writer.close();

            // test sync+verify with failures
            sync = createEcsSync(connectString1, connectString2, CAS_THREADS, true);
            sync.getSyncConfig().getOptions().setSourceListFile(clipFile.getAbsolutePath());
            ByteAlteringFilter.ByteAlteringConfig filter = new ByteAlteringFilter.ByteAlteringConfig();
            sync.getSyncConfig().setFilters(Collections.singletonList(filter));
            sync.getSyncConfig().getOptions().setRetryAttempts(0); // retries will circumvent this test
            sync.getSyncConfig().getOptions().setVerify(true);

            run(sync);

            Assert.assertTrue(filter.getModifiedObjects() > 0);
            Assert.assertEquals(filter.getModifiedObjects(), sync.getStats().getObjectsFailed());
        } finally {
            // delete clips from both
            delete(sourcePool, clipIds);
            delete(destPool, clipIds);
            try {
                sourcePool.Close();
            } catch (Throwable t) {
                log.warn("failed to close source pool", t);
            }
            try {
                destPool.Close();
            } catch (Throwable t) {
                log.warn("failed to close dest pool", t);
            }
        }
    }

    @Test
    public void testSyncQuerySmallBlobs() throws Exception {
        int numClips = 250, maxBlobSize = 102400;

        FPPool sourcePool = new FPPool(connectString1);
        FPPool destPool = new FPPool(connectString2);

        // make sure both pools are empty
        Assert.assertEquals("source pool contains objects", 0, query(sourcePool).size());
        Assert.assertEquals("target pool contains objects", 0, query(destPool).size());

        // create random data (capture summary for comparison)
        StringWriter sourceSummary = new StringWriter();
        List<String> clipIds = createTestClips(sourcePool, maxBlobSize, numClips, sourceSummary);

        try {
            EcsSync sync = createEcsSync(connectString1, connectString2, CAS_THREADS, true);

            run(sync);

            Assert.assertEquals(0, sync.getStats().getObjectsFailed());
            Assert.assertEquals(numClips, sync.getStats().getObjectsComplete());

            String destSummary = summarize(destPool, query(destPool));
            Assert.assertEquals("query summaries different", sourceSummary.toString(), destSummary);
        } finally {
            delete(sourcePool, clipIds);
            delete(destPool, clipIds);
            try {
                sourcePool.Close();
            } catch (Throwable t) {
                log.warn("failed to close source pool", t);
            }
            try {
                destPool.Close();
            } catch (Throwable t) {
                log.warn("failed to close dest pool", t);
            }
        }
    }

    @Test
    public void testQueryTimes() throws Exception {
        int numClips = 100, maxBlobSize = 102400;

        FPPool sourcePool = new FPPool(connectString1);
        FPPool destPool = new FPPool(connectString2);

        // make sure both pools are empty
        Assert.assertEquals("source pool contains objects", 0, query(sourcePool).size());
        Assert.assertEquals("target pool contains objects", 0, query(destPool).size());

        // create random data (capture summary for comparison)
        StringWriter sourceSummary = new StringWriter();
        List<String> clipIds = createTestClips(sourcePool, maxBlobSize, numClips, sourceSummary);

        // compensate for up to 5 seconds of clock skew
        Thread.sleep(5000);

        Calendar startTime = Calendar.getInstance(), endTime = Calendar.getInstance();
        startTime.add(Calendar.MINUTE, -10); // set start time to 10 minutes ago
        try {
            EcsSync sync = createEcsSync(connectString1, connectString2, CAS_THREADS, false);

            // set query start/end times
            CasConfig sourceConfig = (CasConfig) sync.getSyncConfig().getSource();
            sourceConfig.setQueryStartTime(Iso8601Util.format(startTime.getTime()));
            sourceConfig.setQueryEndTime(Iso8601Util.format(endTime.getTime()));

            sync.run();

            Assert.assertEquals(0, sync.getStats().getObjectsFailed());
            Assert.assertEquals(numClips, sync.getStats().getObjectsComplete());

            String destSummary = summarize(destPool, query(destPool));
            Assert.assertEquals("query summaries different", sourceSummary.toString(), destSummary);
        } finally {
            delete(sourcePool, clipIds);
            delete(destPool, clipIds);
            try {
                sourcePool.Close();
            } catch (Throwable t) {
                log.warn("failed to close source pool", t);
            }
            try {
                destPool.Close();
            } catch (Throwable t) {
                log.warn("failed to close dest pool", t);
            }
        }
    }

    @Test
    public void testDeleteClipList() throws Exception {
        int numClips = 100, maxBlobSize = 512000;

        FPPool pool = new FPPool(connectString1);

        try {
            // get clip count before test
            int originalClipCount = query(pool).size();

            // create random data
            StringWriter sourceSummary = new StringWriter();
            List<String> clipIds = createTestClips(pool, maxBlobSize, numClips, sourceSummary);

            // verify test clips were created
            Assert.assertEquals("wrong test clip count", originalClipCount + numClips, query(pool).size());

            // write clip ID file
            File clipFile = File.createTempFile("clip", "lst");
            clipFile.deleteOnExit();
            BufferedWriter writer = new BufferedWriter(new FileWriter(clipFile));
            for (String clipId : clipIds) {
                writer.write(clipId);
                writer.newLine();
            }
            writer.close();

            // construct EcsSync instance
            SyncConfig syncConfig = new SyncConfig();
            syncConfig.setOptions(new SyncOptions().withThreadCount(CAS_THREADS)
                    .withSourceListFile(clipFile.getAbsolutePath()).withDeleteSource(true));
            syncConfig.setSource(new CasConfig().withConnectionString(connectString1));
            syncConfig.setTarget(new com.emc.ecs.sync.config.storage.TestConfig());

            EcsSync sync = new EcsSync();
            sync.setSyncConfig(syncConfig);

            // run EcsSync
            sync.run();
            System.out.println(sync.getStats().getStatsString());

            // verify test clips were deleted
            int afterDeleteCount = query(pool).size();
            if (originalClipCount != afterDeleteCount) {
                delete(pool, clipIds);
                Assert.fail("test clips not fully deleted");
            }
        } finally {
            try {
                pool.Close();
            } catch (Throwable t) {
                log.warn("failed to close pool", t);
            }
        }
    }

    private EcsSync createEcsSync(String connectString1, String connectString2, int threadCount,
            boolean enableTimings) throws Exception {
        SyncConfig syncConfig = new SyncConfig();
        syncConfig.setSource(new CasConfig().withConnectionString(connectString1));
        syncConfig.setTarget(new CasConfig().withConnectionString(connectString2));
        syncConfig.setOptions(new SyncOptions().withThreadCount(threadCount).withRetryAttempts(1)
                .withTimingsEnabled(enableTimings));

        EcsSync sync = new EcsSync();
        sync.setSyncConfig(syncConfig);

        return sync;
    }

    protected void run(EcsSync sync) {
        System.gc();
        long startSize = Runtime.getRuntime().totalMemory();
        sync.run();
        System.gc();
        long endSize = Runtime.getRuntime().totalMemory();
        System.out.println(String.format("memory before sync: %d, after sync: %d", startSize, endSize));
        System.out.println(sync.getStats().getStatsString());
    }

    private List<String> createTestClips(FPPool pool, int maxBlobSize, int thisMany, Writer summaryWriter)
            throws Exception {
        ExecutorService service = Executors.newFixedThreadPool(CAS_THREADS);

        System.out.print("Creating clips");

        List<String> clipIds = Collections.synchronizedList(new ArrayList<String>());
        List<String> summaries = Collections.synchronizedList(new ArrayList<String>());
        for (int clipIdx = 0; clipIdx < thisMany; clipIdx++) {
            service.submit(new ClipWriter(pool, clipIds, maxBlobSize, summaries));
        }

        service.shutdown();
        service.awaitTermination(CAS_SETUP_WAIT_MINUTES, TimeUnit.MINUTES);
        service.shutdownNow();

        Collections.sort(summaries);
        for (String summary : summaries) {
            summaryWriter.append(summary);
        }

        System.out.println();

        return clipIds;
    }

    private void deleteAll(FPPool pool) throws Exception {
        delete(pool, query(pool));
    }

    private void delete(FPPool pool, List<String> clipIds) throws Exception {
        ExecutorService service = Executors.newFixedThreadPool(CAS_THREADS);

        System.out.print("Deleting clips");

        for (String clipId : clipIds) {
            service.submit(new ClipDeleter(pool, clipId));
        }

        service.shutdown();
        service.awaitTermination(CAS_SETUP_WAIT_MINUTES, TimeUnit.MINUTES);
        service.shutdownNow();

        System.out.println();
    }

    private String summarize(FPPool pool, List<String> clipIds) throws Exception {
        List<String> summaries = Collections.synchronizedList(new ArrayList<String>());

        ExecutorService service = Executors.newFixedThreadPool(CAS_THREADS);

        System.out.print("Summarizing clips");

        for (String clipId : clipIds) {
            service.submit(new ClipReader(pool, clipId, summaries));
        }

        service.shutdown();
        service.awaitTermination(CAS_SETUP_WAIT_MINUTES, TimeUnit.MINUTES);
        service.shutdownNow();

        System.out.println();

        Collections.sort(summaries);
        StringBuilder out = new StringBuilder();
        for (String summary : summaries) {
            out.append(summary);
        }
        return out.toString();
    }

    private List<String> query(FPPool pool) throws Exception {
        List<String> clipIds = new ArrayList<>();

        System.out.println("Querying for clips");

        FPQueryExpression query = new FPQueryExpression();
        query.setStartTime(0);
        query.setEndTime(-1);
        query.setType(FPLibraryConstants.FP_QUERY_TYPE_EXISTING);
        FPPoolQuery poolQuery = new FPPoolQuery(pool, query);

        FPQueryResult queryResult = null;
        boolean searching = true;
        while (searching) {
            queryResult = poolQuery.FetchResult();
            switch (queryResult.getResultCode()) {

            case FPLibraryConstants.FP_QUERY_RESULT_CODE_OK:
                clipIds.add(queryResult.getClipID());
                break;

            case FPLibraryConstants.FP_QUERY_RESULT_CODE_INCOMPLETE:
            case FPLibraryConstants.FP_QUERY_RESULT_CODE_COMPLETE:
            case FPLibraryConstants.FP_QUERY_RESULT_CODE_PROGRESS:
            case FPLibraryConstants.FP_QUERY_RESULT_CODE_ERROR:
                break;

            case FPLibraryConstants.FP_QUERY_RESULT_CODE_END:
                System.out.println("End of query reached, exiting.");
                searching = false;
                break;

            default:
                // Unknown error, stop running query
                throw new RuntimeException("received error: " + queryResult.getResultCode());
            }

            queryResult.Close();
        } //while

        queryResult.Close();
        poolQuery.Close();

        return clipIds;
    }

    private String summarizeClip(FPClip clip) throws Exception {
        FPTag tag;
        List<String> tagNames = new ArrayList<>();
        List<Long> tagSizes = new ArrayList<>();
        List<byte[]> tagByteArrays = new ArrayList<>();
        while ((tag = clip.FetchNext()) != null) {
            byte[] tagBytes = null;
            if (tag.BlobExists() == 1) {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                tag.BlobRead(baos);
                tagBytes = baos.toByteArray();
            }
            tagNames.add(tag.getTagName());
            tagSizes.add(tag.getBlobSize());
            tagByteArrays.add(tagBytes);
            tag.Close();
        }
        return summarizeClip(clip.getClipID(), tagNames, tagSizes, tagByteArrays);
    }

    private String summarizeClip(String clipId, List<String> tagNames, List<Long> tagSizes,
            List<byte[]> tagByteArrays) throws NoSuchAlgorithmException {
        StringBuilder out = new StringBuilder();
        out.append(String.format("Clip ID: %s", clipId)).append("\n");
        if (tagNames != null) {
            for (int i = 0; i < tagNames.size(); i++) {
                String md5 = "n/a";
                if (tagByteArrays.get(i) != null)
                    md5 = Hex.encodeHexString(MessageDigest.getInstance("MD5").digest(tagByteArrays.get(i)));
                out.append(String.format("<--tag:%s--> size:%d, md5:%s", tagNames.get(i), tagSizes.get(i), md5))
                        .append("\n");
            }
        }
        return out.toString();
    }

    private class BlobReader implements Runnable {
        private FPTag sourceTag;
        private OutputStream out;
        private boolean success = false;
        private Throwable error;

        BlobReader(FPTag sourceTag, OutputStream out) {
            this.sourceTag = sourceTag;
            this.out = out;
        }

        @Override
        public synchronized void run() {
            try {
                sourceTag.BlobRead(out);
                success = true;
            } catch (Throwable t) {
                success = false;
                error = t;
            } finally {
                // make sure you always close piped streams!
                try {
                    out.close();
                } catch (Throwable t) {
                    // ignore
                }
            }
        }

        Throwable getError() {
            return error;
        }

        boolean isSuccess() {
            return success;
        }
    }

    private class ClipWriter implements Runnable {
        private FPPool pool;
        private List<String> clipIds;
        private int maxBlobSize;
        private List<String> summaries;
        private Random random;

        ClipWriter(FPPool pool, List<String> clipIds, int maxBlobSize, List<String> summaries) {
            this.pool = pool;
            this.clipIds = clipIds;
            this.maxBlobSize = maxBlobSize;
            this.summaries = summaries;
            random = new Random();
        }

        @Override
        public void run() {
            try {
                FPClip clip = new FPClip(pool);
                FPTag topTag = clip.getTopTag();

                List<String> tagNames = new ArrayList<>();
                List<Long> tagSizes = new ArrayList<>();
                List<byte[]> tagByteArrays = new ArrayList<>();

                // random number of tags per clip (<= 10)
                for (int tagIdx = 0; tagIdx <= random.nextInt(10); tagIdx++) {
                    FPTag tag = new FPTag(topTag, "test_tag_" + tagIdx);
                    byte[] blobContent = null;
                    // random whether tag has blob
                    if (random.nextBoolean()) {
                        // random blob length (<= maxBlobSize)
                        blobContent = new byte[random.nextInt(maxBlobSize) + 1];
                        // random blob content
                        random.nextBytes(blobContent);
                        tag.BlobWrite(new ByteArrayInputStream(blobContent));
                    }
                    tagNames.add(tag.getTagName());
                    tagSizes.add(tag.getBlobSize());
                    tagByteArrays.add(blobContent);
                    tag.Close();
                }
                topTag.Close();
                String clipId = clip.Write();
                clip.Close();

                clipIds.add(clipId);
                summaries.add(summarizeClip(clipId, tagNames, tagSizes, tagByteArrays));

                System.out.print(".");
            } catch (Exception e) {
                e.printStackTrace();
                if (e instanceof RuntimeException)
                    throw (RuntimeException) e;
                throw new RuntimeException(e);
            }
        }
    }

    private class ClipReader implements Runnable {
        private FPPool pool;
        private String clipId;
        private List<String> summaries;

        ClipReader(FPPool pool, String clipId, List<String> summaries) {
            this.pool = pool;
            this.clipId = clipId;
            this.summaries = summaries;
        }

        @Override
        public void run() {
            try {
                FPClip clip = new FPClip(pool, clipId, FPLibraryConstants.FP_OPEN_FLAT);
                summaries.add(summarizeClip(clip));
                clip.Close();
                System.out.print(".");
            } catch (Exception e) {
                if (e instanceof RuntimeException)
                    throw (RuntimeException) e;
                throw new RuntimeException(e);
            }
        }
    }

    private class ClipDeleter implements Runnable {
        private FPPool pool;
        private String clipId;

        ClipDeleter(FPPool pool, String clipId) {
            this.pool = pool;
            this.clipId = clipId;
        }

        @Override
        public void run() {
            try {
                System.out.print(".");
                FPClip.Delete(pool, clipId);
            } catch (Exception e) {
                if (e instanceof RuntimeException)
                    throw (RuntimeException) e;
                throw new RuntimeException(e);
            }
        }
    }

    private class Producer implements Runnable {
        private byte[] data;
        private OutputStream out;

        Producer(byte[] data, OutputStream out) {
            this.data = data;
            this.out = out;
        }

        @Override
        public void run() {
            try {
                out.write(data);
            } catch (IOException e) {
                throw new RuntimeException(e);
            } finally {
                try {
                    out.close();
                } catch (IOException e) {
                    System.out.println("could not close output stream" + e.getMessage());
                }
            }
        }
    }
}