package org.infinispan.stress;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.infinispan.Cache;
import org.infinispan.commons.executors.BlockingThreadPoolExecutorFactory;
import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.Configuration;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.global.GlobalConfigurationBuilder;
import org.infinispan.distribution.ch.impl.SyncConsistentHashFactory;
import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.test.MultipleCacheManagersTest;
import org.infinispan.test.TestingUtil;
import org.infinispan.test.fwk.CleanupAfterTest;
import org.infinispan.commons.test.TestResourceTracker;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.Test;

/**
 * Test that we're able to start a large cluster in a single JVM.
 *
 * @author Dan Berindei
 * @since 5.3
 */
@CleanupAfterTest
@Test(groups = "stress", testName = "stress.LargeClusterStressTest", timeOut = 15*60*1000)
public class LargeClusterStressTest extends MultipleCacheManagersTest {

   private static final int NUM_NODES = 40;
   private static final int NUM_CACHES = 80;
   private static final int NUM_THREADS = 10;
   private static final int NUM_SEGMENTS = 1000;

   @Override
   protected void createCacheManagers() throws Throwable {
      // start the cache managers in the test itself
   }

   public void testLargeClusterStart() throws Exception {
      final Configuration distConfig = new ConfigurationBuilder()
            .clustering().cacheMode(CacheMode.DIST_SYNC)
            .clustering().stateTransfer().awaitInitialTransfer(false)
//            .hash().consistentHashFactory(new TopologyAwareSyncConsistentHashFactory()).numSegments(NUM_SEGMENTS)
            .hash().consistentHashFactory(new SyncConsistentHashFactory()).numSegments(NUM_SEGMENTS)
            .build();
      final Configuration replConfig = new ConfigurationBuilder()
            .clustering().cacheMode(CacheMode.REPL_SYNC)
            .clustering().hash().numSegments(NUM_SEGMENTS)
            .clustering().stateTransfer().awaitInitialTransfer(false)
            .build();

      // Start the caches (and the JGroups channels) in separate threads
      ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS, getTestThreadFactory("Worker"));
      ExecutorCompletionService<Object> completionService = new ExecutorCompletionService<Object>(executor);
      Future<Object>[] futures = new Future[NUM_NODES];
      try {
         for (int i = 0; i < NUM_NODES; i++) {
            final String nodeName = TestResourceTracker.getNameForIndex(i);
            final String machineId = "m" + (i / 2);
            futures[i] = completionService.submit(new Callable<Object>() {
               @Override
               public Object call() throws Exception {
                  GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
                  gcb.transport().defaultTransport().nodeName(nodeName);
//                  gcb.transport().machineId(machineId);
                  BlockingThreadPoolExecutorFactory remoteExecutorFactory = new BlockingThreadPoolExecutorFactory(
                        10, 1, 0, 60000);
                  gcb.transport().remoteCommandThreadPool().threadPoolFactory(remoteExecutorFactory);
                  BlockingThreadPoolExecutorFactory stateTransferExecutorFactory = new BlockingThreadPoolExecutorFactory(
                        4, 1, 0, 60000);
                  gcb.transport().stateTransferThreadPool().threadPoolFactory(stateTransferExecutorFactory);
                  EmbeddedCacheManager cm = new DefaultCacheManager(gcb.build());
                  try {
                     for (int j = 0; j < NUM_CACHES/2; j++) {
                        cm.defineConfiguration("repl-cache-" + j, replConfig);
                        cm.defineConfiguration("dist-cache-" + j, distConfig);
                     }
                     for (int j = 0; j < NUM_CACHES/2; j++) {
                        Cache<Object, Object> replCache = cm.getCache("repl-cache-" + j);
                        replCache.put(cm.getAddress(), "bla");
                        Cache<Object, Object> distCache = cm.getCache("dist-cache-" + j);
                        distCache.put(cm.getAddress(), "bla");
                     }
                  } finally {
                     registerCacheManager(cm);
                  }
                  log.infof("Started cache manager %s", cm.getAddress());
                  return null;
               }
            });
         }

         for (int i = 0; i < NUM_NODES; i++) {
            completionService.take();
         }
      } finally {
         executor.shutdownNow();
      }

      log.infof("All %d cache managers started, waiting for state transfer to finish for each cache", NUM_NODES);

      for (int j = 0; j < NUM_CACHES/2; j++) {
         waitForClusterToForm("repl-cache-" + j);
         waitForClusterToForm("dist-cache-" + j);
      }
   }

   @Test(dependsOnMethods = "testLargeClusterStart")
   public void testLargeClusterStop() {
      for (int i = 0; i < NUM_NODES - 1; i++) {
         int killIndex = -1;
         for (int j = 0; j < cacheManagers.size(); j++) {
            if (address(j).equals(manager(0).getCoordinator())) {
               killIndex = j;
               break;
            }
         }

         log.debugf("Killing coordinator %s", address(killIndex));
         manager(killIndex).stop();
         cacheManagers.remove(killIndex);
         if (cacheManagers.size() > 0) {
            TestingUtil.blockUntilViewsReceived(60000, false, cacheManagers);
            for (int j = 0; j < NUM_CACHES/2; j++) {
               TestingUtil.waitForNoRebalance(caches("repl-cache-" + j));
               TestingUtil.waitForNoRebalance(caches("dist-cache-" + j));
            }
         }
      }
   }

   @AfterMethod
   @Override
   protected void clearContent() throws Throwable {
      // Do nothing
   }
}
