/*
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.flume.channel;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import org.apache.flume.ChannelException;
import org.apache.flume.ChannelFullException;
import org.apache.flume.Context;
import org.apache.flume.Event;
import org.apache.flume.Transaction;
import org.apache.flume.conf.Configurables;
import org.apache.flume.event.EventBuilder;
import org.apache.flume.channel.file.FileChannelConfiguration;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;

public class TestSpillableMemoryChannel {

  private SpillableMemoryChannel channel;

  @Rule
  public TemporaryFolder fileChannelDir = new TemporaryFolder();

  private void configureChannel(Map<String, String> overrides) {
    Context context = new Context();
    File checkPointDir = fileChannelDir.newFolder("checkpoint");
    File dataDir = fileChannelDir.newFolder("data");
    context.put(FileChannelConfiguration.CHECKPOINT_DIR, checkPointDir.getAbsolutePath());
    context.put(FileChannelConfiguration.DATA_DIRS, dataDir.getAbsolutePath());
    // Set checkpoint for 5 seconds otherwise test will run out of memory
    context.put(FileChannelConfiguration.CHECKPOINT_INTERVAL, "5000");

    if (overrides != null) {
      context.putAll(overrides);
    }

    Configurables.configure(channel, context);
  }

  private void reconfigureChannel(Map<String, String> overrides) {
    configureChannel(overrides);
    channel.stop();
    channel.start();
  }

  private void startChannel(Map<String, String> params) {
    configureChannel(params);
    channel.start();
  }

  // performs a hard restart of the channel... creates a new channel object
  private void restartChannel(Map<String, String> params) {
    channel.stop();
    setUp();
    startChannel(params);
  }

  static class NullFound extends RuntimeException {
    public int expectedValue;

    public NullFound(int expected) {
      super("Expected " + expected + ",  but null found");
      expectedValue = expected;
    }
  }

  static class TooManyNulls extends RuntimeException {
    private int nullsFound;

    public TooManyNulls(int count) {
      super("Total nulls found in thread ("
            + Thread.currentThread().getName() + ") : " + count);
      nullsFound = count;
    }
  }

  @Before
  public void setUp() {
    channel = new SpillableMemoryChannel();
    channel.setName("spillChannel-" + UUID.randomUUID());
  }

  @After
  public void tearDown() {
    channel.stop();
  }

  private static void putN(int first, int count, AbstractChannel channel) {
    for (int i = 0; i < count; ++i) {
      channel.put(EventBuilder.withBody(String.valueOf(first++).getBytes()));
    }
  }

  private static void takeNull(AbstractChannel channel) {
    channel.take();
  }

  private static void takeN(int first, int count, AbstractChannel channel) {
    int last = first + count;
    for (int i = first; i < last; ++i) {
      Event e = channel.take();
      if (e == null) {
        throw new NullFound(i);
      }
      Event expected = EventBuilder.withBody(String.valueOf(i).getBytes());
      Assert.assertArrayEquals(e.getBody(), expected.getBody());
    }
  }

  // returns the number of non null events found
  private static int takeN_NoCheck(int batchSize, AbstractChannel channel) {
    int i = 0;
    for (; i < batchSize; ++i) {
      Event e = channel.take();
      if (e == null) {
        try {
          Thread.sleep(0);
        } catch (InterruptedException ex) { /* ignore */ }
        return i;
      }
    }
    return i;
  }

  private static void transactionalPutN(int first, int count, AbstractChannel channel) {
    Transaction tx = channel.getTransaction();
    tx.begin();
    try {
      putN(first, count, channel);
      tx.commit();
    } catch (RuntimeException e) {
      tx.rollback();
      throw e;
    } finally {
      tx.close();
    }
  }

  private static void transactionalTakeN(int first, int count, AbstractChannel channel) {
    Transaction tx = channel.getTransaction();
    tx.begin();
    try {
      takeN(first, count, channel);
      tx.commit();
    } catch (NullFound e) {
      tx.commit();
      throw e;
    } catch (AssertionError e) {
      tx.rollback();
      throw e;
    } catch (RuntimeException e) {
      tx.rollback();
      throw e;
    } finally {
      tx.close();
    }
  }

  private static int transactionalTakeN_NoCheck(int count, AbstractChannel channel) {
    Transaction tx = channel.getTransaction();
    tx.begin();
    try {
      int eventCount = takeN_NoCheck(count, channel);
      tx.commit();
      return eventCount;
    } catch (RuntimeException e) {
      tx.rollback();
      throw e;
    } finally {
      tx.close();
    }
  }

  private static void transactionalTakeNull(int count, AbstractChannel channel) {
    Transaction tx = channel.getTransaction();
    tx.begin();
    try {
      for (int i = 0; i < count; ++i) {
        takeNull(channel);
      }
      tx.commit();
    } catch (AssertionError e) {
      tx.rollback();
      throw e;
    } catch (RuntimeException e) {
      tx.rollback();
      throw e;
    } finally {
      tx.close();
    }
  }

  private Thread makePutThread(String threadName, final int first, final int count,
                               final int batchSize, final AbstractChannel channel) {
    return new Thread(threadName) {
      public void run() {
        int maxdepth = 0;
        StopWatch watch = new StopWatch();
        for (int i = first; i < first + count; i = i + batchSize) {
          transactionalPutN(i, batchSize, channel);
        }
        watch.elapsed();
      }
    };
  }

  private static Thread makeTakeThread(String threadName, final int first, final int count,
                                       final int batchSize, final AbstractChannel channel) {
    return new Thread(threadName) {
      public void run() {
        StopWatch watch = new StopWatch();
        for (int i = first; i < first + count; ) {
          try {
            transactionalTakeN(i, batchSize, channel);
            i = i + batchSize;
          } catch (NullFound e) {
            i = e.expectedValue;
          }
        }
        watch.elapsed();
      }
    };
  }

  private static Thread makeTakeThread_noCheck(String threadName, final int totalEvents,
                                               final int batchSize, final AbstractChannel channel) {
    return new Thread(threadName) {
      public void run() {
        int batchSz = batchSize;
        StopWatch watch = new StopWatch();
        int i = 0, attempts = 0;
        while (i < totalEvents) {
          int remaining = totalEvents - i;
          batchSz = (remaining > batchSz) ? batchSz : remaining;
          int takenCount = transactionalTakeN_NoCheck(batchSz, channel);
          if (takenCount < batchSz) {
            try {
              Thread.sleep(20);
            } catch (InterruptedException ex) { /* ignore */ }
          }
          i += takenCount;
          ++attempts;
          if (attempts > totalEvents * 3) {
            throw new TooManyNulls(attempts);
          }
        }
        watch.elapsed(" items = " + i + ", attempts = " + attempts);
      }
    };
  }

  @Test
  public void testPutTake() {
    Map<String, String> params = new HashMap<String, String>();
    params.put("memoryCapacity", "5");
    params.put("overflowCapacity", "5");
    params.put(FileChannelConfiguration.TRANSACTION_CAPACITY, "5");
    startChannel(params);

    Transaction tx = channel.getTransaction();
    tx.begin();
    putN(0, 2, channel);
    tx.commit();
    tx.close();

    tx = channel.getTransaction();
    tx.begin();
    takeN(0, 2, channel);
    tx.commit();
    tx.close();
  }

  @Test
  public void testCapacityDisableOverflow() {
    Map<String, String> params = new HashMap<String, String>();
    params.put("memoryCapacity", "2");
    params.put("overflowCapacity", "0");   // overflow is disabled effectively
    params.put("overflowTimeout", "0");
    startChannel(params);

    transactionalPutN(0, 2, channel);

    boolean threw = false;
    try {
      transactionalPutN(2, 1, channel);
    } catch (ChannelException e) {
      threw = true;
    }
    Assert.assertTrue("Expecting ChannelFullException to be thrown", threw);

    transactionalTakeN(0, 2, channel);

    Transaction tx = channel.getTransaction();
    tx.begin();
    Assert.assertNull(channel.take());
    tx.commit();
    tx.close();
  }

  @Test
  public void testCapacityWithOverflow() {
    Map<String, String> params = new HashMap<String, String>();
    params.put("memoryCapacity", "2");
    params.put("overflowCapacity", "4");
    params.put(FileChannelConfiguration.TRANSACTION_CAPACITY, "3");
    params.put("overflowTimeout", "0");
    startChannel(params);

    transactionalPutN(1, 2, channel);
    transactionalPutN(3, 2, channel);
    transactionalPutN(5, 2, channel);

    boolean threw = false;
    try {
      transactionalPutN(7, 2, channel);   // cannot fit in channel
    } catch (ChannelFullException e) {
      threw = true;
    }
    Assert.assertTrue("Expecting ChannelFullException to be thrown", threw);

    transactionalTakeN(1, 2, channel);
    transactionalTakeN(3, 2, channel);
    transactionalTakeN(5, 2, channel);
  }

  @Test
  public void testRestart() {
    Map<String, String> params = new HashMap<String, String>();
    params.put("memoryCapacity", "2");
    params.put("overflowCapacity", "10");
    params.put(FileChannelConfiguration.TRANSACTION_CAPACITY, "4");
    params.put("overflowTimeout", "0");
    startChannel(params);

    transactionalPutN(1, 2, channel);
    transactionalPutN(3, 2, channel);  // goes in overflow

    restartChannel(params);

    // from overflow, as in memory stuff should be lost
    transactionalTakeN(3, 2, channel);
  }

  @Test
  public void testBasicStart() {
    Map<String, String> params = new HashMap<String, String>();
    params.put("memoryCapacity", "10000000");
    params.put("overflowCapacity", "20000000");
    params.put(FileChannelConfiguration.TRANSACTION_CAPACITY, "10");
    params.put("overflowTimeout", "1");

    startChannel(params);

    transactionalPutN(1, 5, channel);
    transactionalPutN(6, 5, channel);
    transactionalPutN(11, 5, channel); // these should go to overflow

    transactionalTakeN(1, 10, channel);
    transactionalTakeN(11, 5, channel);
  }

  @Test
  public void testOverflow() {
    Map<String, String> params = new HashMap<String, String>();
    params.put("memoryCapacity", "10");
    params.put("overflowCapacity", "20");
    params.put(FileChannelConfiguration.TRANSACTION_CAPACITY, "10");
    params.put("overflowTimeout", "1");

    startChannel(params);

    transactionalPutN(1, 5, channel);
    transactionalPutN(6, 5, channel);
    transactionalPutN(11, 5, channel); // these should go to overflow

    transactionalTakeN(1, 10, channel);
    transactionalTakeN(11, 5, channel);
  }

  @Test
  public void testDrainOrder() {
    Map<String, String> params = new HashMap<String, String>();
    params.put("memoryCapacity", "10");
    params.put("overflowCapacity", "10");
    params.put(FileChannelConfiguration.TRANSACTION_CAPACITY, "5");
    params.put("overflowTimeout", "1");

    startChannel(params);

    transactionalPutN(1, 5, channel);
    transactionalPutN(6, 5, channel);
    transactionalPutN(11, 5, channel); // into overflow
    transactionalPutN(16, 5, channel); // into overflow

    transactionalTakeN(1, 1, channel);
    transactionalTakeN(2, 5, channel);
    transactionalTakeN(7, 4, channel);

    transactionalPutN(20, 2, channel);
    transactionalPutN(22, 3, channel);

    transactionalTakeN(11, 3, channel); // from overflow
    transactionalTakeN(14, 5, channel); // from overflow
    transactionalTakeN(19, 2, channel); // from overflow
  }

  @Test
  public void testByteCapacity() {
    Map<String, String> params = new HashMap<String, String>();
    params.put("memoryCapacity", "1000");
    // configure to hold 8 events of 10 bytes each (plus 20% event header space)
    params.put("byteCapacity", "100");
    params.put("avgEventSize", "10");
    params.put("overflowCapacity", "20");
    params.put(FileChannelConfiguration.TRANSACTION_CAPACITY, "10");
    params.put("overflowTimeout", "1");
    startChannel(params);

    transactionalPutN(1, 8, channel);   // this wil max the byteCapacity
    transactionalPutN(9, 10, channel);
    transactionalPutN(19, 10, channel);  // this will fill up the overflow

    boolean threw = false;
    try {
      transactionalPutN(11, 1, channel);  // into overflow
    } catch (ChannelFullException e) {
      threw = true;
    }
    Assert.assertTrue("byteCapacity did not throw as expected", threw);
  }

  @Test
  public void testDrainingOnChannelBoundary() {

    Map<String, String> params = new HashMap<String, String>();
    params.put("memoryCapacity", "5");
    params.put("overflowCapacity", "15");
    params.put(FileChannelConfiguration.TRANSACTION_CAPACITY, "10");
    params.put("overflowTimeout", "1");
    startChannel(params);

    transactionalPutN(1, 5, channel);
    transactionalPutN(6, 5, channel);  // into overflow
    transactionalPutN(11, 5, channel);  // into overflow
    transactionalPutN(16, 5, channel);  // into overflow

    transactionalTakeN(1, 3, channel);

    Transaction tx = channel.getTransaction();
    tx.begin();
    takeN(4, 2, channel);
    takeNull(channel);  // expect null since next event is in overflow
    tx.commit();
    tx.close();

    transactionalTakeN(6, 5, channel);  // from overflow

    transactionalTakeN(11, 5, channel); // from overflow
    transactionalTakeN(16, 2, channel); // from overflow

    transactionalPutN(21, 5, channel);

    tx = channel.getTransaction();
    tx.begin();
    takeN(18, 3, channel);              // from overflow
    takeNull(channel);  // expect null since next event is in primary
    tx.commit();
    tx.close();

    transactionalTakeN(21, 5, channel);
  }

  @Test
  public void testRollBack() {
    Map<String, String> params = new HashMap<String, String>();
    params.put("memoryCapacity", "100");
    params.put("overflowCapacity", "900");
    params.put(FileChannelConfiguration.TRANSACTION_CAPACITY, "900");
    params.put("overflowTimeout", "0");
    startChannel(params);

    //1 Rollback for Puts
    transactionalPutN(1, 5, channel);
    Transaction tx = channel.getTransaction();
    tx.begin();
    putN(6, 5, channel);
    tx.rollback();
    tx.close();

    transactionalTakeN(1, 5, channel);
    transactionalTakeNull(2, channel);

    //2.  verify things back to normal after put rollback
    transactionalPutN(11, 5, channel);
    transactionalTakeN(11, 5, channel);

    //3 Rollback for Takes
    transactionalPutN(16, 5, channel);
    tx = channel.getTransaction();
    tx.begin();
    takeN(16, 5, channel);
    takeNull(channel);
    tx.rollback();
    tx.close();

    transactionalTakeN_NoCheck(5, channel);

    //4.  verify things back to normal after take rollback
    transactionalPutN(21, 5, channel);
    transactionalTakeN(21, 5, channel);
  }

  @Test
  public void testReconfigure() {
    //1) bring up with small capacity
    Map<String, String> params = new HashMap<String, String>();
    params.put("memoryCapacity", "10");
    params.put("overflowCapacity", "0");
    params.put("overflowTimeout", "0");
    startChannel(params);

    Assert.assertTrue("overflowTimeout setting did not reconfigure correctly",
                      channel.getOverflowTimeout() == 0);
    Assert.assertTrue("memoryCapacity did not reconfigure correctly",
                      channel.getMemoryCapacity() == 10);
    Assert.assertTrue("overflowCapacity did not reconfigure correctly",
                      channel.isOverflowDisabled());

    transactionalPutN(1, 10, channel);
    boolean threw = false;
    try {
      transactionalPutN(11, 10, channel);  // should throw an error
    } catch (ChannelException e) {
      threw = true;
    }
    Assert.assertTrue("Expected the channel to fill up and throw an exception, "
                      + "but it did not throw", threw);

    //2) Resize and verify
    params = new HashMap<String, String>();
    params.put("memoryCapacity", "20");
    params.put("overflowCapacity", "0");
    reconfigureChannel(params);

    Assert.assertTrue("overflowTimeout setting did not reconfigure correctly",
                      channel.getOverflowTimeout() ==
                      SpillableMemoryChannel.defaultOverflowTimeout);
    Assert.assertTrue("memoryCapacity did not reconfigure correctly",
                      channel.getMemoryCapacity() == 20);
    Assert.assertTrue("overflowCapacity did not reconfigure correctly",
                      channel.isOverflowDisabled());

    // pull out the values inserted prior to reconfiguration
    transactionalTakeN(1, 10, channel);

    transactionalPutN(11, 10, channel);
    transactionalPutN(21, 10, channel);

    threw = false;
    try {
      transactionalPutN(31, 10, channel);  // should throw an error
    } catch (ChannelException e) {
      threw = true;
    }
    Assert.assertTrue("Expected the channel to fill up and throw an exception, "
                      + "but it did not throw", threw);

    transactionalTakeN(11, 10, channel);
    transactionalTakeN(21, 10, channel);

    // 3) Reconfigure with empty config and verify settings revert to default
    params = new HashMap<String, String>();
    reconfigureChannel(params);

    Assert.assertTrue("overflowTimeout setting did not reconfigure correctly",
                      channel.getOverflowTimeout() ==
                      SpillableMemoryChannel.defaultOverflowTimeout);
    Assert.assertTrue("memoryCapacity did not reconfigure correctly",
                      channel.getMemoryCapacity() == SpillableMemoryChannel.defaultMemoryCapacity);
    Assert.assertTrue("overflowCapacity did not reconfigure correctly",
                      channel.getOverflowCapacity() ==
                      SpillableMemoryChannel.defaultOverflowCapacity);
    Assert.assertFalse("overflowCapacity did not reconfigure correctly",
                       channel.isOverflowDisabled());

    // 4) Reconfiguring of  overflow
    params = new HashMap<String, String>();
    params.put("memoryCapacity", "10");
    params.put("overflowCapacity", "10");
    params.put("transactionCapacity", "5");
    params.put("overflowTimeout", "1");
    reconfigureChannel(params);

    transactionalPutN(1, 5, channel);
    transactionalPutN(6, 5, channel);
    transactionalPutN(11, 5, channel);
    transactionalPutN(16, 5, channel);
    threw = false;
    try {
      // should error out as both primary & overflow are full
      transactionalPutN(21, 5, channel);
    } catch (ChannelException e) {
      threw = true;
    }
    Assert.assertTrue("Expected the last insertion to fail, but it didn't.", threw);

    // reconfig the overflow
    params = new HashMap<String, String>();
    params.put("memoryCapacity", "10");
    params.put("overflowCapacity", "20");
    params.put("transactionCapacity", "10");
    params.put("overflowTimeout", "1");
    reconfigureChannel(params);

    // should succeed now as we have made room in the overflow
    transactionalPutN(21, 5, channel);

    transactionalTakeN(1, 10, channel);
    transactionalTakeN(11, 5, channel);
    transactionalTakeN(16, 5, channel);
    transactionalTakeN(21, 5, channel);
  }

  @Test
  public void testParallelSingleSourceAndSink() throws InterruptedException {
    Map<String, String> params = new HashMap<String, String>();
    params.put("memoryCapacity", "1000020");
    params.put("overflowCapacity", "0");
    params.put("overflowTimeout", "3");
    startChannel(params);

    // run source and sink concurrently
    Thread sourceThd = makePutThread("src", 1, 500000, 100, channel);
    Thread sinkThd = makeTakeThread("sink", 1, 500000, 100, channel);

    StopWatch watch = new StopWatch();

    sinkThd.start();
    sourceThd.start();

    sourceThd.join();
    sinkThd.join();

    watch.elapsed();
    System.out.println("Max Queue size " + channel.getMaxMemQueueSize());
  }

  @Test
  public void testCounters() throws InterruptedException {
    Map<String, String> params = new HashMap<String, String>();
    params.put("memoryCapacity", "5000");
    params.put("overflowCapacity", "5000");
    params.put("transactionCapacity", "5000");
    params.put("overflowTimeout", "0");
    startChannel(params);

    Assert.assertTrue("channel.channelCounter should have started",
        channel.channelCounter.getStartTime() > 0);

    //1. fill up mem queue
    Thread sourceThd = makePutThread("src", 1, 5000, 2500, channel);
    sourceThd.start();
    sourceThd.join();

    Assert.assertEquals(5000, channel.getTotalStored());
    Assert.assertEquals(5000, channel.channelCounter.getChannelSize());
    Assert.assertEquals(5000, channel.channelCounter.getEventPutAttemptCount());
    Assert.assertEquals(5000, channel.channelCounter.getEventPutSuccessCount());

    //2. empty mem queue
    Thread sinkThd = makeTakeThread("sink", 1, 5000, 1000, channel);
    sinkThd.start();
    sinkThd.join();
    Assert.assertEquals(0, channel.getTotalStored());
    Assert.assertEquals(0, channel.channelCounter.getChannelSize());
    Assert.assertEquals(5000, channel.channelCounter.getEventTakeAttemptCount());
    Assert.assertEquals(5000, channel.channelCounter.getEventTakeSuccessCount());

    //3. fill up mem & overflow
    sourceThd = makePutThread("src", 1, 10000, 1000, channel);
    sourceThd.start();
    sourceThd.join();
    Assert.assertEquals(10000, channel.getTotalStored());
    Assert.assertEquals(10000, channel.channelCounter.getChannelSize());
    Assert.assertEquals(15000, channel.channelCounter.getEventPutAttemptCount());
    Assert.assertEquals(15000, channel.channelCounter.getEventPutSuccessCount());

    //4. empty memory
    sinkThd = makeTakeThread("sink", 1, 5000, 1000, channel);
    sinkThd.start();
    sinkThd.join();
    Assert.assertEquals(5000, channel.getTotalStored());
    Assert.assertEquals(5000, channel.channelCounter.getChannelSize());
    Assert.assertEquals(10000, channel.channelCounter.getEventTakeAttemptCount());
    Assert.assertEquals(10000, channel.channelCounter.getEventTakeSuccessCount());

    //5. empty overflow
    transactionalTakeN(5001, 1000, channel);
    transactionalTakeN(6001, 1000, channel);
    transactionalTakeN(7001, 1000, channel);
    transactionalTakeN(8001, 1000, channel);
    transactionalTakeN(9001, 1000, channel);
    Assert.assertEquals(0, channel.getTotalStored());
    Assert.assertEquals(0, channel.channelCounter.getChannelSize());
    Assert.assertEquals(15000, channel.channelCounter.getEventTakeAttemptCount());
    Assert.assertEquals(15000, channel.channelCounter.getEventTakeSuccessCount());

    //6. now do it concurrently
    sourceThd = makePutThread("src1", 1, 5000, 1000, channel);
    Thread sourceThd2 = makePutThread("src2", 1, 5000, 500, channel);
    sinkThd = makeTakeThread_noCheck("sink1", 5000, 1000, channel);
    sourceThd.start();
    sourceThd2.start();
    sinkThd.start();
    sourceThd.join();
    sourceThd2.join();
    sinkThd.join();
    Assert.assertEquals(5000, channel.getTotalStored());
    Assert.assertEquals(5000, channel.channelCounter.getChannelSize());
    Thread sinkThd2 = makeTakeThread_noCheck("sink2", 2500, 500, channel);
    Thread sinkThd3 = makeTakeThread_noCheck("sink3", 2500, 1000, channel);
    sinkThd2.start();
    sinkThd3.start();
    sinkThd2.join();
    sinkThd3.join();
    Assert.assertEquals(0, channel.getTotalStored());
    Assert.assertEquals(0, channel.channelCounter.getChannelSize());
    Assert.assertEquals(25000, channel.channelCounter.getEventTakeSuccessCount());
    Assert.assertEquals(25000, channel.channelCounter.getEventPutSuccessCount());
    Assert.assertTrue("TakeAttempt channel counter value larger than expected",
                      25000 <= channel.channelCounter.getEventTakeAttemptCount());
    Assert.assertTrue("PutAttempt channel counter value larger than expected",
                      25000 <= channel.channelCounter.getEventPutAttemptCount());
  }

  public ArrayList<Thread> createSourceThreads(int count, int totalEvents, int batchSize) {
    ArrayList<Thread> sourceThds = new ArrayList<Thread>();

    for (int i = 0; i < count; ++i) {
      sourceThds.add(makePutThread("src" + i, 1, totalEvents / count, batchSize, channel));
    }
    return sourceThds;
  }

  public ArrayList<Thread> createSinkThreads(int count, int totalEvents, int batchSize) {
    ArrayList<Thread> sinkThreads = new ArrayList<Thread>(count);

    for (int i = 0; i < count; ++i) {
      sinkThreads.add(makeTakeThread_noCheck("sink" + i, totalEvents / count, batchSize, channel));
    }
    return sinkThreads;
  }

  public void startThreads(ArrayList<Thread> threads) {
    for (Thread thread : threads) {
      thread.start();
    }
  }

  public void joinThreads(ArrayList<Thread> threads) throws InterruptedException {
    for (Thread thread : threads) {
      try {
        thread.join();
      } catch (InterruptedException e) {
        System.out.println("Interrupted while waiting on " + thread.getName());
        throw e;
      }
    }
  }

  @Test
  public void testParallelMultipleSourcesAndSinks() throws InterruptedException {
    int sourceCount = 8;
    int sinkCount = 8;
    int eventCount = 1000000;
    int batchSize = 100;

    Map<String, String> params = new HashMap<String, String>();
    params.put("memoryCapacity", "0");
    params.put("overflowCapacity", String.valueOf(eventCount));
    params.put("overflowTimeout", "3");
    startChannel(params);

    ArrayList<Thread> sinks = createSinkThreads(sinkCount, eventCount, batchSize);

    ArrayList<Thread> sources = createSourceThreads(sourceCount, eventCount, batchSize);

    StopWatch watch = new StopWatch();
    startThreads(sinks);
    startThreads(sources);

    joinThreads(sources);
    joinThreads(sinks);

    watch.elapsed();

    System.out.println("Max Queue size " + channel.getMaxMemQueueSize());

    Assert.assertEquals(eventCount, channel.drainOrder.totalPuts);
    Assert.assertEquals("Channel not fully drained", 0, channel.getTotalStored());
    System.out.println("testParallelMultipleSourcesAndSinks done");
  }

  static class StopWatch {
    long startTime;

    public StopWatch() {
      startTime = System.currentTimeMillis();
    }

    public void elapsed() {
      elapsed(null);
    }

    public void elapsed(String suffix) {
      long elapsed = System.currentTimeMillis() - startTime;
      if (suffix == null) {
        suffix = "";
      } else {
        suffix = "{ " + suffix + " }";
      }

      if (elapsed < 10000) {
        System.out.println(Thread.currentThread().getName()
                           + " : [ " + elapsed + " ms ].        " + suffix);
      } else {
        System.out.println(Thread.currentThread().getName()
                           + " : [ " + elapsed / 1000 + " sec ].       " + suffix);
      }
    }
  }

}
