/*
 * 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.geode.internal.offheap;

import org.apache.geode.distributed.internal.DistributionConfig;
import org.apache.geode.test.junit.categories.IntegrationTest;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.categories.Category;

import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

/**
 * Tests fill pattern validation for the {@link MemoryAllocatorImpl}.
 */
@Category(IntegrationTest.class)
public class MemoryAllocatorFillPatternIntegrationTest {
  private static Random random = ThreadLocalRandom.current();

  /**
   * Chunk operation types.
   */
  static enum Operation {
    ALLOCATE, FREE, WRITE;

    // Holds all Operation values
    private static Operation[] values = Operation.values();

    static Operation randomOperation() {
      return values[random.nextInt(values.length)];
    }
  };

  /** Number of worker threads for advanced tests. */
  private static final int WORKER_THREAD_COUNT = 5;

  /** Size of single test slab. */
  private static final int SLAB_SIZE = 1024 * 1024 * 50;

  /** Maximum number of bytes a worker thread can allocate during advanced tests. */
  private static final int MAX_WORKER_ALLOCATION_TOTAL_SIZE = SLAB_SIZE / WORKER_THREAD_COUNT / 2;

  /** Maximum allocation for a single Chunk. */
  private static final int MAX_WORKER_ALLOCATION_SIZE = 512;

  /** Canned data for write operations. */
  private static final byte[] WRITE_BYTES = new String("Some string data.").getBytes();

  /** Minimum size for write operations. */
  private static final int MIN_WORKER_ALLOCATION_SIZE = WRITE_BYTES.length;

  /** Runtime for worker threads. */
  private static final long RUN_TIME_IN_MILLIS = 1 * 1000 * 5;

  /** Chunk size for basic huge allocation test. */
  private static final int HUGE_CHUNK_SIZE = 1024 * 200;

  /** Our test victim. */
  private MemoryAllocatorImpl allocator = null;

  /** Our test victim's memory slab. */
  private SlabImpl slab = null;

  /**
   * Enables fill validation and creates the test victim.
   */
  @Before
  public void setUp() throws Exception {
    System.setProperty(DistributionConfig.GEMFIRE_PREFIX + "validateOffHeapWithFill", "true");
    this.slab = new SlabImpl(SLAB_SIZE);
    this.allocator = MemoryAllocatorImpl.createForUnitTest(new NullOutOfOffHeapMemoryListener(),
        new NullOffHeapMemoryStats(), new SlabImpl[] {this.slab});
  }

  /**
   * Frees off heap memory.
   */
  @After
  public void tearDown() throws Exception {
    MemoryAllocatorImpl.freeOffHeapMemory();
    System.clearProperty(DistributionConfig.GEMFIRE_PREFIX + "validateOffHeapWithFill");
  }

  /**
   * This test hammers a MemoryAllocatorImpl with multiple threads exercising the fill validation of
   * tiny Chunks for one minute. This, of course, exercises many aspects of the MemoryAllocatorImpl
   * and its helper classes.
   * 
   * @throws Exception
   */
  @Test
  public void testFillPatternAdvancedForTinyAllocations() throws Exception {
    doFillPatternAdvancedTest(new ChunkSizer() {
      @Override
      public int allocationSize() {
        int allocation = random.nextInt(MAX_WORKER_ALLOCATION_SIZE + 1);

        while (allocation < MIN_WORKER_ALLOCATION_SIZE) {
          allocation = random.nextInt(MAX_WORKER_ALLOCATION_SIZE + 1);
        }
        return allocation;
      }
    });
  }

  /**
   * This test hammers a MemoryAllocatorImpl with multiple threads exercising the fill validation of
   * huge Chunks for one minute. This, of course, exercises many aspects of the MemoryAllocatorImpl
   * and its helper classes.
   * 
   * @throws Exception
   */
  @Test
  public void testFillPatternAdvancedForHugeAllocations() throws Exception {
    doFillPatternAdvancedTest(new ChunkSizer() {
      @Override
      public int allocationSize() {
        return HUGE_CHUNK_SIZE;
      }
    });
  }

  private interface ChunkSizer {
    int allocationSize();
  }

  private void doFillPatternAdvancedTest(final ChunkSizer chunkSizer) throws InterruptedException {
    // Used to manage worker thread completion
    final CountDownLatch latch = new CountDownLatch(WORKER_THREAD_COUNT);

    // Use to track any errors the worker threads will encounter
    final List<Throwable> threadErrorList =
        Collections.synchronizedList(new LinkedList<Throwable>());

    /*
     * Start up a number of worker threads. These threads will randomly allocate, free, and write to
     * Chunks.
     */
    for (int i = 0; i < WORKER_THREAD_COUNT; ++i) {
      new Thread(new Runnable() {
        // Total allocation in bytes for this thread
        private int totalAllocation = 0;

        // List of Chunks allocated by this thread
        private List<OffHeapStoredObject> chunks = new LinkedList<OffHeapStoredObject>();

        // Time to end thread execution
        private long endTime = System.currentTimeMillis() + RUN_TIME_IN_MILLIS;

        /**
         * Allocates a chunk and adds it to the thread's Chunk list.
         */
        private void allocate() {
          int allocation = chunkSizer.allocationSize();
          OffHeapStoredObject chunk = (OffHeapStoredObject) allocator.allocate(allocation);

          // This should always work just after allocation
          chunk.validateFill();

          chunks.add(chunk);
          totalAllocation += chunk.getSize();
        }

        /**
         * Frees a random chunk from the Chunk list.
         */
        private void free() {
          OffHeapStoredObject chunk = chunks.remove(random.nextInt(chunks.size()));
          totalAllocation -= chunk.getSize();

          /*
           * Chunk is filled here but another thread may have already grabbed it so we cannot
           * validate the fill.
           */
          chunk.release();
        }

        /**
         * Writes canned data to a random Chunk from the Chunk list.
         */
        private void write() {
          OffHeapStoredObject chunk = chunks.get(random.nextInt(chunks.size()));
          chunk.writeDataBytes(0, WRITE_BYTES);
        }

        /**
         * Randomly selects Chunk operations and executes them for a period of time. Collects any
         * error thrown during execution.
         */
        @Override
        public void run() {
          try {
            for (long currentTime = System.currentTimeMillis(); currentTime < endTime; currentTime =
                System.currentTimeMillis()) {
              Operation op = (totalAllocation == 0 ? Operation.ALLOCATE
                  : (totalAllocation >= MAX_WORKER_ALLOCATION_TOTAL_SIZE ? Operation.FREE
                      : Operation.randomOperation()));
              switch (op) {
                case ALLOCATE:
                  allocate();
                  break;
                case FREE:
                  free();
                  break;
                case WRITE:
                  write();
                  break;
              }
            }
          } catch (Throwable t) {
            threadErrorList.add(t);
          } finally {
            latch.countDown();
          }
        }
      }).start();
    }

    // Make sure each thread ended cleanly
    assertTrue(latch.await(2, TimeUnit.MINUTES));

    // Fail on the first error we find
    if (!threadErrorList.isEmpty()) {
      fail(threadErrorList.get(0).getMessage());
    }
  }

}
