/*
 * 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.cache;

import static org.apache.geode.test.awaitility.GeodeAwaitility.await;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.IntStream;

import org.apache.commons.io.FileUtils;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;

import org.apache.geode.StatisticsFactory;
import org.apache.geode.cache.CacheWriterException;
import org.apache.geode.cache.DiskAccessException;
import org.apache.geode.cache.DiskStore;
import org.apache.geode.cache.DiskStoreFactory;
import org.apache.geode.cache.EntryEvent;
import org.apache.geode.cache.EntryNotFoundException;
import org.apache.geode.cache.RegionFactory;
import org.apache.geode.cache.RegionShortcut;
import org.apache.geode.cache.Scope;
import org.apache.geode.cache.util.CacheWriterAdapter;
import org.apache.geode.internal.InternalDataSerializer;
import org.apache.geode.internal.cache.Oplog.OPLOG_TYPE;
import org.apache.geode.internal.cache.entries.AbstractDiskLRURegionEntry;
import org.apache.geode.internal.cache.entries.DiskEntry;
import org.apache.geode.test.dunit.ThreadUtils;

/**
 * Testing Oplog API's
 */
public class OplogJUnitTest extends DiskRegionTestingBase {

  private boolean proceed = false;

  private final DiskRegionProperties diskProps = new DiskRegionProperties();

  private long delta;

  private volatile boolean assertDone = false;

  private boolean failure = false;

  @Override
  protected final void postSetUp() throws Exception {
    diskProps.setDiskDirs(dirs);
    DiskStoreImpl.SET_IGNORE_PREALLOCATE = true;
  }

  @Override
  protected final void postTearDown() throws Exception {
    DiskStoreImpl.SET_IGNORE_PREALLOCATE = false;
  }

  /**
   * Test method for 'org.apache.geode.internal.cache.Oplog.isBackup()'
   */
  @Test
  public void testIsBackup() {

    InternalRegion overFlowAndPersistRegionRegion =
        (InternalRegion) DiskRegionHelperFactory.getSyncOverFlowAndPersistRegion(cache, diskProps);
    assertTrue("Not correctly setup for overflow and persist",
        overFlowAndPersistRegionRegion.getDiskRegion().isBackup());
    closeDown(overFlowAndPersistRegionRegion);

    InternalRegion overFlowOnlyRegion =
        (InternalRegion) DiskRegionHelperFactory.getSyncOverFlowOnlyRegion(cache, diskProps);
    assertFalse("Not correctly setup for overflow only mode",
        overFlowOnlyRegion.getDiskRegion().isBackup());
    closeDown(overFlowOnlyRegion);

    InternalRegion persistOnlyRegion = (InternalRegion) DiskRegionHelperFactory
        .getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);
    assertTrue("Not correctly setup for  persist only mode",
        persistOnlyRegion.getDiskRegion().isBackup());
    closeDown(persistOnlyRegion);
  }

  /*
   * Test method for 'org.apache.geode.internal.cache.Oplog.useSyncWrites()'
   */
  @Test
  public void testUseSyncWritesWhenSet() {
    diskProps.setSynchronous(true);
    InternalRegion syncOverFlowAndPersistRegion =
        (InternalRegion) DiskRegionHelperFactory.getSyncOverFlowAndPersistRegion(cache, diskProps);
    assertTrue(syncOverFlowAndPersistRegion.getAttributes().isDiskSynchronous());
    closeDown(syncOverFlowAndPersistRegion);

    InternalRegion syncOverFlowOnlyRegion =
        (InternalRegion) DiskRegionHelperFactory.getSyncOverFlowOnlyRegion(cache, diskProps);
    assertTrue(syncOverFlowOnlyRegion.getAttributes().isDiskSynchronous());
    closeDown(syncOverFlowOnlyRegion);

    InternalRegion syncPersistOnlyRegion = (InternalRegion) DiskRegionHelperFactory
        .getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);
    assertTrue(syncPersistOnlyRegion.getAttributes().isDiskSynchronous());
    closeDown(syncPersistOnlyRegion);
  }

  @Test
  public void testNotUseSyncWritesWhenNotSet() {
    diskProps.setSynchronous(false);
    InternalRegion asyncOverFlowAndPersistRegion =
        (InternalRegion) DiskRegionHelperFactory.getAsyncOverFlowAndPersistRegion(cache, diskProps);
    assertFalse(asyncOverFlowAndPersistRegion.getAttributes().isDiskSynchronous());
    closeDown(asyncOverFlowAndPersistRegion);

    InternalRegion asyncOverFlowOnlyRegion =
        (InternalRegion) DiskRegionHelperFactory.getAsyncOverFlowOnlyRegion(cache, diskProps);
    assertFalse(asyncOverFlowOnlyRegion.getAttributes().isDiskSynchronous());
    closeDown(asyncOverFlowOnlyRegion);

    InternalRegion asyncPersistOnlyRegion =
        (InternalRegion) DiskRegionHelperFactory.getAsyncPersistOnlyRegion(cache, diskProps);
    assertFalse(asyncPersistOnlyRegion.getAttributes().isDiskSynchronous());
    closeDown(asyncPersistOnlyRegion);
  }

  /**
   * Test method for 'org.apache.geode.internal.cache.Oplog.clear(File)'
   */
  @Test
  public void testClear() {
    region = DiskRegionHelperFactory.getSyncOverFlowAndPersistRegion(cache, diskProps);
    putTillOverFlow(region);
    region.clear();
    region.close();
    region = DiskRegionHelperFactory.getSyncOverFlowAndPersistRegion(cache, diskProps);
    assertTrue(" failed in get OverflowAndPersist ", region.get(0) == null);
    closeDown();

    region = DiskRegionHelperFactory.getSyncOverFlowOnlyRegion(cache, diskProps);
    putTillOverFlow(region);
    region.clear();
    region.close();
    region = DiskRegionHelperFactory.getSyncOverFlowOnlyRegion(cache, diskProps);
    assertTrue(" failed in get OverflowOnly ", region.get(0) == null);
    closeDown();

    region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);
    put100Int();
    region.clear();
    region.close();
    region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);
    assertTrue(" failed in get PersistOnly ", region.get(0) == null);
    closeDown();
  }

  /**
   * Test method for 'org.apache.geode.internal.cache.Oplog.close()'
   */
  @Test
  public void testClose() {
    {
      deleteFiles();
      region = DiskRegionHelperFactory.getSyncOverFlowAndPersistRegion(cache, diskProps);
      DiskRegion dr = ((LocalRegion) region).getDiskRegion();
      Oplog oplog = dr.testHook_getChild();
      long id = oplog.getOplogId();
      oplog.close();

      StatisticsFactory factory = cache.getDistributedSystem();
      Oplog newOplog =
          new Oplog(id, dr.getOplogSet(), new DirectoryHolder(factory, dirs[0], 1000, 0));
      dr.getOplogSet().setChild(newOplog);
      closeDown();
    }
    {
      deleteFiles();
      region = DiskRegionHelperFactory.getSyncOverFlowOnlyRegion(cache, diskProps);
      DiskRegion dr = ((LocalRegion) region).getDiskRegion();
      dr.testHookCloseAllOverflowOplogs();
      checkIfContainsFile("OVERFLOW");
      closeDown();
    }
    {
      deleteFiles();
      region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);
      DiskRegion dr = ((LocalRegion) region).getDiskRegion();
      Oplog oplog = dr.testHook_getChild();
      long id = oplog.getOplogId();
      oplog.close();
      StatisticsFactory factory = cache.getDistributedSystem();
      Oplog newOplog =
          new Oplog(id, dr.getOplogSet(), new DirectoryHolder(factory, dirs[0], 1000, 2));
      dr.setChild(newOplog);
      closeDown();
    }

  }

  private void closeDown(InternalRegion region) {
    super.closeDown(region);
    DiskRegion diskRegion = region != null ? region.getDiskRegion() : null;
    if (diskRegion != null) {
      diskRegion.getDiskStore().close();
      ((InternalCache) cache).removeDiskStore(diskRegion.getDiskStore());
    }
  }

  @Override
  protected void closeDown() {
    DiskRegion dr = null;
    if (region != null) {
      dr = ((LocalRegion) region).getDiskRegion();
    }
    super.closeDown();
    if (dr != null) {
      dr.getDiskStore().close();
      ((LocalRegion) region).getGemFireCache().removeDiskStore(dr.getDiskStore());
    }
  }

  private void checkIfContainsFile(String fileExtension) {
    for (File dir : dirs) {
      File[] files = dir.listFiles();
      for (File file : files) {
        if (file.getAbsolutePath().endsWith(fileExtension)) {
          fail("file " + file + " still exists after oplog.close()");
        }
      }
    }
  }

  /**
   * Test method for 'org.apache.geode.internal.cache.Oplog.destroy()'
   */
  @Test
  public void testDestroy() {
    region = DiskRegionHelperFactory.getSyncOverFlowAndPersistRegion(cache, diskProps);
    put100Int();
    putTillOverFlow(region);
    try {
      region.destroy(0);
    } catch (EntryNotFoundException e1) {
      logWriter.error("Exception occurred", e1);
      fail(" Entry not found when it was expected to be there");
    }
    region.close();
    region = DiskRegionHelperFactory.getSyncOverFlowAndPersistRegion(cache, diskProps);
    assertTrue(" failed in get OverflowAndPersist ", region.get(0) == null);
    closeDown();

    region = DiskRegionHelperFactory.getSyncOverFlowOnlyRegion(cache, diskProps);
    put100Int();
    putTillOverFlow(region);
    try {
      region.destroy(0);
    } catch (EntryNotFoundException e1) {
      logWriter.error("Exception occurred", e1);
      fail(" Entry not found when it was expected to be there");
    }
    region.close();
    region = DiskRegionHelperFactory.getSyncOverFlowOnlyRegion(cache, diskProps);
    assertTrue(" failed in get OverflowOnly ", region.get(0) == null);

    closeDown();

    region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);
    put100Int();
    try {
      region.destroy(0);
    } catch (EntryNotFoundException e1) {
      logWriter.error("Exception occurred", e1);
      fail(" Entry not found when it was expected to be there");
    }
    region.close();
    region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);
    assertTrue(" failed in get PersistOnly ", region.get(0) == null);
    closeDown();

  }

  /**
   * Test method for 'org.apache.geode.internal.cache.Oplog.remove(long)'
   */
  @Test
  public void testRemove() {
    region = DiskRegionHelperFactory.getSyncOverFlowAndPersistRegion(cache, diskProps);
    putTillOverFlow(region);
    region.remove(0);
    region.close();
    region = DiskRegionHelperFactory.getSyncOverFlowAndPersistRegion(cache, diskProps);
    assertTrue(" failed in get OverflowAndPersist ", region.get(0) == null);
    closeDown();

    region = DiskRegionHelperFactory.getSyncOverFlowOnlyRegion(cache, diskProps);
    putTillOverFlow(region);
    region.remove(0);
    assertTrue(" failed in get OverflowOnly ", region.get(0) == null);
    region.close();
    region = DiskRegionHelperFactory.getSyncOverFlowOnlyRegion(cache, diskProps);
    closeDown();

    region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);
    put100Int();
    region.remove(0);
    assertTrue(" failed in get PersistOnly ", region.get(0) == null);
    region.close();
    region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);
    closeDown();

  }

  /**
   * Tests whether the data is written in the right format on the disk
   *
   */
  @Test
  public void testFaultInOfValuesFromDisk() {
    try {
      // Asif First create a persist only disk region which is of aysnch
      // & switch of OplOg type
      diskProps.setMaxOplogSize(1000);

      diskProps.setPersistBackup(true);
      diskProps.setRolling(false);
      diskProps.setSynchronous(true);
      diskProps.setTimeInterval(-1);
      diskProps.setOverflow(false);

      region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);
      byte[] val = new byte[10];
      for (int i = 0; i < val.length; ++i) {
        val[i] = (byte) i;
      }
      region.put(1, val);

      DiskEntry entry = ((DiskEntry) ((LocalRegion) region).basicGetEntry(1));
      DiskRegion dr = ((LocalRegion) region).getDiskRegion();

      val = (byte[]) dr.getNoBuffer(entry.getDiskId());
      for (int i = 0; i < val.length; ++i) {
        if (val[i] != (byte) i) {
          fail("Test for fault in from disk failed");
        }
      }
      val = (byte[]) DiskStoreImpl.convertBytesAndBitsIntoObject(
          dr.getBytesAndBitsWithoutLock(entry.getDiskId(), true, false), (InternalCache) cache);
      for (int i = 0; i < val.length; ++i) {
        if (val[i] != (byte) i) {
          fail("Test for fault in from disk failed");
        }
      }
      region.invalidate(1);
      assertTrue(dr.getNoBuffer(entry.getDiskId()) == Token.INVALID);

    } catch (Exception e) {
      logWriter.error("Exception occurred", e);
      fail(e.toString());
    }
    closeDown();
  }

  /**
   * Tests the original ByteBufferPool gets transferred to the new Oplog for synch mode
   *
   */
  @Test
  public void testByteBufferPoolTransferForSynchMode() {
    diskProps.setMaxOplogSize(1024);
    diskProps.setBytesThreshold(0);
    diskProps.setPersistBackup(true);
    diskProps.setRolling(false);
    diskProps.setSynchronous(true);
    diskProps.setTimeInterval(10000);
    diskProps.setOverflow(false);

    region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);
    DiskRegion dr = ((LocalRegion) region).getDiskRegion();
    // Populate data just below the switch over threshold
    byte[] val = new byte[5];
    for (int i = 0; i < val.length; ++i) {
      val[i] = (byte) i;
    }

    region.put(1, val);

    ((LocalRegion) region).basicGetEntry(1);
    Oplog old = dr.testHook_getChild();
    ByteBuffer oldWriteBuf = old.getWriteBuf();
    dr.forceRolling();
    region.put(2, val);
    Oplog switched = dr.testHook_getChild();
    assertTrue(old != switched);
    assertEquals(dr.getDiskStore().getPersistentOplogs().getChild(2), switched);
    assertEquals(oldWriteBuf, switched.getWriteBuf());
    assertEquals(null, old.getWriteBuf());
    closeDown();

  }

  /**
   * Tests the bug which arises in case of asynch mode during oplog switching caused by conflation
   * of create/destroy operation.The bug occurs if a create operation is followed by destroy but
   * before destroy proceeds some other operation causes oplog switching
   *
   */
  @Test
  public void testBug34615() {
    final int MAX_OPLOG_SIZE = 100;
    diskProps.setMaxOplogSize(MAX_OPLOG_SIZE);
    diskProps.setPersistBackup(true);
    diskProps.setRolling(false);
    diskProps.setSynchronous(false);
    diskProps.setOverflow(false);
    diskProps.setBytesThreshold(150);

    LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = true;
    final byte[] val = new byte[50];
    region = DiskRegionHelperFactory.getAsyncPersistOnlyRegion(cache, diskProps);
    final CacheObserver old = CacheObserverHolder.setInstance(new CacheObserverAdapter() {
      @Override
      public void afterConflation(ByteBuffer orig, ByteBuffer conflated) {
        Thread th = new Thread(() -> region.put("2", new byte[75]));
        assertNull(conflated);
        th.start();
        ThreadUtils.join(th, 30 * 1000);
        LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = false;

      }
    });

    region.put("1", val);
    region.remove("1");
    assertFalse(failureCause, testFailed);
    CacheObserverHolder.setInstance(old);
    closeDown();

  }

  @Test
  public void testConflation() throws Exception {

    final int MAX_OPLOG_SIZE = 1000;
    diskProps.setMaxOplogSize(MAX_OPLOG_SIZE);
    diskProps.setPersistBackup(true);
    diskProps.setRolling(false);
    diskProps.setSynchronous(false);
    diskProps.setOverflow(false);
    diskProps.setBytesThreshold(1500);

    final byte[] val = new byte[50];
    final byte[][] bb = new byte[2][];
    bb[0] = new byte[5];
    region = DiskRegionHelperFactory.getAsyncPersistOnlyRegion(cache, diskProps);
    try {
      region.put("1", val);
      region.put("1", new byte[10]);
      region.put("2", val);
      region.put("2", new byte[100]);
      region.create("3", null);
      region.put("3", new byte[10]);
      region.create("4", null);
      region.put("4", new byte[0]);

      // tests for byte[][]
      region.create("5", bb);
      region.put("6", val);
      region.put("6", bb);
      region.create("7", null);
      region.put("7", bb);

      region.create("8", new byte[9]);
      region.invalidate("8");
      region.create("9", new byte[0]);
      region.invalidate("9");

      region.create("10", new byte[9]);
      region.localInvalidate("10");
      region.create("11", new byte[0]);
      region.localInvalidate("11");

      DiskRegion dr = ((LocalRegion) region).getDiskRegion();
      dr.flushForTesting();
      byte[] val_1 = ((byte[]) ((LocalRegion) region).getValueOnDisk("1"));
      assertEquals(val_1.length, 10);
      byte[] val_2 = ((byte[]) ((LocalRegion) region).getValueOnDisk("2"));
      assertEquals(val_2.length, 100);
      byte[] val_3 = ((byte[]) ((LocalRegion) region).getValueOnDisk("3"));
      assertEquals(val_3.length, 10);
      byte[] val_4 = ((byte[]) ((LocalRegion) region).getValueOnDisk("4"));
      assertEquals(val_4.length, 0);
      byte[][] val_5 = (byte[][]) ((LocalRegion) region).getValueOnDisk("5");
      assertEquals(val_5.length, 2);
      assertEquals(val_5[0].length, 5);
      assertNull(val_5[1]);
      byte[][] val_6 = (byte[][]) ((LocalRegion) region).getValueOnDisk("6");
      assertEquals(val_6.length, 2);
      assertEquals(val_6[0].length, 5);
      assertNull(val_6[1]);
      byte[][] val_7 = (byte[][]) ((LocalRegion) region).getValueOnDisk("7");
      assertEquals(val_7.length, 2);
      assertEquals(val_7[0].length, 5);
      assertNull(val_7[1]);
      Object val_8 = ((LocalRegion) region).getValueOnDisk("8");
      assertEquals(val_8, Token.INVALID);
      Object val_9 = ((LocalRegion) region).getValueOnDisk("9");
      assertEquals(val_9, Token.INVALID);
      Object val_10 = ((LocalRegion) region).getValueOnDisk("10");
      assertEquals(val_10, Token.LOCAL_INVALID);
      Object val_11 = ((LocalRegion) region).getValueOnDisk("11");
      assertEquals(val_11, Token.LOCAL_INVALID);

    } finally {
      closeDown();
    }
  }

  /**
   * This tests the retrieval of empty byte array when present in asynch buffers
   *
   */
  @Test
  public void testGetEmptyByteArrayInAsynchBuffer() {
    final int MAX_OPLOG_SIZE = 1000;
    diskProps.setMaxOplogSize(MAX_OPLOG_SIZE);
    diskProps.setPersistBackup(true);
    diskProps.setRolling(false);
    diskProps.setSynchronous(false);
    diskProps.setOverflow(false);
    diskProps.setBytesThreshold(1500);

    final byte[] val = new byte[50];
    region = DiskRegionHelperFactory.getAsyncPersistOnlyRegion(cache, diskProps);
    try {
      region.put("1", val);
      region.put("1", new byte[0]);
      byte[] val_1 = ((byte[]) ((LocalRegion) region).getValueOnDiskOrBuffer("1"));
      assertEquals(val_1.length, 0);
    } catch (Exception e) {
      logWriter.error("Exception occurred", e);
      fail("The test failed due to exception = " + e);
    }
    closeDown();
  }

  /**
   * This tests the retrieval of empty byte array in synch mode
   *
   */
  @Test
  public void testGetEmptyByteArrayInSynchMode() {
    final int MAX_OPLOG_SIZE = 1000;
    diskProps.setMaxOplogSize(MAX_OPLOG_SIZE);
    diskProps.setPersistBackup(true);
    diskProps.setRolling(false);
    diskProps.setSynchronous(true);
    diskProps.setOverflow(false);
    diskProps.setBytesThreshold(1500);

    final byte[] val = new byte[50];
    region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);
    try {
      region.put("1", val);
      region.put("1", new byte[0]);
      byte[] val_1 = ((byte[]) ((LocalRegion) region).getValueOnDiskOrBuffer("1"));
      assertEquals(val_1.length, 0);
    } catch (Exception e) {
      logWriter.error("Exception occurred", e);
      fail("The test failed due to exception = " + e);
    }
    closeDown();
  }

  /**
   * This tests the bug which caused the oplogRoller to attempt to roll a removed entry whose value
   * is Token.Removed This bug can occur if a remove operation causes oplog switching & hence roller
   * thread gets notified, & the roller thread obtains the iterator of the concurrent region map
   * before the remove
   *
   */
  @Test
  public void testBug34702() {
    final int MAX_OPLOG_SIZE = 500 * 2;
    diskProps.setMaxOplogSize(MAX_OPLOG_SIZE);
    diskProps.setPersistBackup(true);
    diskProps.setRolling(true);
    diskProps.setSynchronous(true);
    diskProps.setOverflow(false);
    final byte[] val = new byte[200];
    proceed = false;

    region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);
    region.put("key1", val);
    region.put("key2", val);
    LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = true;
    final CacheObserver old = CacheObserverHolder.setInstance(new CacheObserverAdapter() {

      @Override
      public void afterSettingOplogOffSet(long offset) {
        ((LocalRegion) region).getDiskRegion().forceRolling();
        // Let the operation thread yield to the Roller so that
        // it is able to obtain the iterator of the concurrrent region map
        // & thus get the reference to the entry which will contain
        // value as Token.Removed as the entry though removed from
        // concurrent
        // map still will be available to the roller
        Thread.yield();
        // Sleep for some time
        try {
          Thread.sleep(5000);
        } catch (InterruptedException e) {
          testFailed = true;
          failureCause = "No guarantee that test is succesful";
          fail("No guarantee that test is succesful");
        }
      }

      @Override
      public void afterHavingCompacted() {
        proceed = true;
        synchronized (OplogJUnitTest.this) {
          OplogJUnitTest.this.notify();
        }
      }
    });
    try {
      region.destroy("key1");
      region.destroy("key2");
    } catch (Exception e1) {
      LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = false;
      CacheObserverHolder.setInstance(old);
      fail("Test failed as entry deletion threw exception. Exception = " + e1);
    }
    // Wait for some time & check if the after having rolled callabck
    // is issued sucessfully or not.
    if (!proceed) {
      synchronized (this) {
        if (!proceed) {
          try {
            wait(20000);
          } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            // The test will automatically fail due to proceed flag
          }
        }
      }
    }
    assertFalse(failureCause, testFailed);
    LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = false;
    CacheObserverHolder.setInstance(old);

    if (!proceed) {
      fail("Test failed as afterHavingCompacted callabck not issued even after sufficient wait");
    }
    closeDown();

  }

  /**
   * tests a potential deadlock situation if the operation causing a swithcing of Oplog is waiting
   * for roller to free space. The problem can arise if the operation causing Oplog switching is
   * going on an Entry , which already has its oplog ID referring to the Oplog being switched. In
   * such case, when the roller will try to roll the entries referencing the current oplog , it will
   * not be able to acquire the lock on the entry as the switching thread has already taken a lock
   * on it.
   *
   */
  @Test
  public void testRollingDeadlockSituation() {
    final int MAX_OPLOG_SIZE = 2000;
    diskProps.setMaxOplogSize(MAX_OPLOG_SIZE);
    diskProps.setPersistBackup(true);
    diskProps.setRolling(true);
    diskProps.setSynchronous(true);
    diskProps.setOverflow(false);
    diskProps.setDiskDirsAndSizes(new File[] {dirs[0]}, new int[] {1400});
    final byte[] val = new byte[500];
    proceed = false;
    region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);
    region.put("key1", val);
    region.put("key1", val);
    try {
      region.put("key1", val);
    } catch (DiskAccessException dae) {
      logWriter.error("Exception occurred", dae);
      fail(
          "Test failed as DiskAccessException was encountered where as the operation should ideally have proceeded without issue . exception = "
              + dae);
    }
  }

  /**
   * This tests whether an empty byte array is correctly writtem to the disk as a zero value length
   * operation & hence the 4 bytes field for recording the value length is absent & also since the
   * value length is zero no byte for it should also get added. Similary during recover from HTree
   * as well as Oplog , the empty byte array should be read correctly
   *
   */
  @Test
  public void testEmptyByteArrayPutAndRecovery() {
    CacheObserver old = CacheObserverHolder.setInstance(new CacheObserverAdapter() {
      @Override
      public void afterConflation(ByteBuffer origBB, ByteBuffer conflatedBB) {
        if ((2 + 4 + 1 + EntryEventImpl.serialize("key1").length) != origBB.capacity()) {
          failureCause =
              "For a backup region, addition of an empty array should result in an offset of 6 bytes where as actual offset is ="
                  + origBB.capacity();
          testFailed = true;
        }
        Assert.assertTrue(
            "For a backup region, addition of an empty array should result in an offset of 6 bytes where as actual offset is ="
                + origBB.capacity(),
            (2 + 4 + 1 + EntryEventImpl.serialize("key1").length) == origBB.capacity());

      }
    });
    try {
      final int MAX_OPLOG_SIZE = 2000;
      diskProps.setMaxOplogSize(MAX_OPLOG_SIZE);
      diskProps.setPersistBackup(true);
      diskProps.setSynchronous(true);
      diskProps.setOverflow(false);
      diskProps.setDiskDirsAndSizes(new File[] {dirs[0]}, new int[] {1400});
      final byte[] val = new byte[0];
      LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = true;

      region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);
      region.put("key1", val);
      LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = false;
      region.close();
      region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);
      byte[] _val = (byte[]) region.get("key1");
      assertTrue(
          "value of key1 after restarting the region is not an empty byte array. This may indicate problem in reading from Oplog",
          _val.length == 0);
      if (logWriter.infoEnabled()) {
        logWriter.info(
            "After first region close & opening again no problems encountered & hence Oplog has been read successfully.");
        logWriter.info(
            "Closing the region again without any operation done, would indicate that next time data will be loaded from HTree .");
      }
      region.close();
      region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);
      _val = (byte[]) region.get("key1");
      assertTrue(
          "value of key1 after restarting the region is not an empty byte array. This may indicate problem in reading from HTRee",
          _val.length == 0);
      assertFalse(failureCause, testFailed);
      // region.close();

    } finally {
      LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = false;
      CacheObserverHolder.setInstance(old);

    }
  }

  /**
   * This is used to test bug 35012 where a remove operation on a key gets unrecorded due to
   * switching of Oplog if it happens just after the remove operation has destroyed the in memory
   * entry & is about to acquire the readlock in DiskRegion to record the same. If the Oplog has
   * switched during that duration , the bug would appear
   *
   */

  @Test
  public void testBug35012() {
    final int MAX_OPLOG_SIZE = 500;
    diskProps.setMaxOplogSize(MAX_OPLOG_SIZE);
    diskProps.setPersistBackup(true);
    diskProps.setRolling(false);
    diskProps.setSynchronous(true);
    diskProps.setOverflow(false);
    final byte[] val = new byte[200];
    try {

      region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);

      region.put("key1", val);
      region.put("key2", val);
      region.put("key3", val);
      final Thread th = new Thread(() -> region.remove("key1"));
      // main thread acquires the write lock
      ((LocalRegion) region).getDiskRegion().acquireWriteLock();
      try {
        th.start();
        Thread.yield();
        DiskRegion dr = ((LocalRegion) region).getDiskRegion();
        dr.testHook_getChild().forceRolling(dr);
      } finally {
        ((LocalRegion) region).getDiskRegion().releaseWriteLock();
      }
      ThreadUtils.join(th, 30 * 1000);
      region.close();
      region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);
      assertEquals(region.size(), 2);
    } catch (Exception e) {
      logWriter.error("Exception occurred ", e);
      fail("The test could not be completed because of exception .Exception=" + e);
    }
    closeDown();

  }

  /**
   * Tests if buffer size & time are not set , the asynch writer gets awakened on time basis of
   * default 1 second
   *
   */
  @Test
  public void testAsynchWriterAttribBehaviour1() throws Exception {
    DiskStoreFactory dsf = cache.createDiskStoreFactory();
    ((DiskStoreFactoryImpl) dsf).setMaxOplogSizeInBytes(10000);
    File dir = new File("testingDirectoryDefault");
    dir.mkdir();
    dir.deleteOnExit();
    File[] dirs = {dir};
    dsf.setDiskDirs(dirs);
    RegionFactory<Object, Object> factory =
        cache.createRegionFactory(RegionShortcut.REPLICATE_PERSISTENT);
    final long t1 = System.currentTimeMillis();
    DiskStore ds = dsf.create("test");
    factory.setDiskSynchronous(false);
    factory.setDiskStoreName(ds.getName());
    factory.setScope(Scope.LOCAL);
    region = factory.create("test");

    LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = true;
    CacheObserver old = CacheObserverHolder.setInstance(

        new CacheObserverAdapter() {
          private long t2;

          @Override
          public void goingToFlush() {
            t2 = System.currentTimeMillis();
            delta = t2 - t1;
            LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = false;
            synchronized (OplogJUnitTest.this) {
              OplogJUnitTest.this.notify();
            }
          }
        });

    region.put("key1", "111111111111");
    synchronized (this) {
      if (LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER) {
        wait(10000);
        assertFalse(LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER);
      }
    }
    CacheObserverHolder.setInstance(old);
    // Windows clock has an accuracy of 15 ms. Accounting for the same.
    assertTrue("delta is in miilliseconds=" + delta, delta >= 985);
    closeDown();
  }

  /**
   * Tests if buffer size is set but time is not set , the asynch writer gets awakened on buffer
   * size basis
   */
  @Ignore("TODO:DARREL_DISABLE: test is disabled")
  @Test
  public void testAsynchWriterAttribBehaviour2() throws Exception {
    DiskStoreFactory dsf = cache.createDiskStoreFactory();
    ((DiskStoreFactoryImpl) dsf).setMaxOplogSizeInBytes(10000);
    dsf.setQueueSize(2);
    File dir = new File("testingDirectoryDefault");
    dir.mkdir();
    dir.deleteOnExit();
    File[] dirs = {dir};
    dsf.setDiskDirs(dirs);
    RegionFactory<Object, Object> factory =
        cache.createRegionFactory(RegionShortcut.REPLICATE_PERSISTENT);
    DiskStore ds = dsf.create("test");
    factory.setDiskSynchronous(false);
    factory.setDiskStoreName(ds.getName());
    factory.setScope(Scope.LOCAL);
    region = factory.create("test");

    LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = true;
    CacheObserver old = CacheObserverHolder.setInstance(

        new CacheObserverAdapter() {

          @Override
          public void goingToFlush() {
            synchronized (OplogJUnitTest.this) {
              LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = false;
              OplogJUnitTest.this.notify();
            }
          }
        });

    region.put("key1", new byte[25]);
    Thread.sleep(1000);
    assertTrue(LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER);
    region.put("key2", new byte[25]);
    synchronized (this) {
      if (LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER) {
        wait(10000);
        assertFalse(LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER);
      }
    }
    CacheObserverHolder.setInstance(old);
    closeDown();
  }

  /**
   * Tests if buffer size & time interval are explicitly set to zero then the flush will occur due
   * to asynchForceFlush or due to switching of Oplog
   *
   */
  @Test
  public void testAsynchWriterAttribBehaviour3() throws Exception {
    DiskStoreFactory dsf = cache.createDiskStoreFactory();
    ((DiskStoreFactoryImpl) dsf).setMaxOplogSizeInBytes(500);
    dsf.setQueueSize(0);
    dsf.setTimeInterval(0);
    File dir = new File("testingDirectoryDefault");
    dir.mkdir();
    dir.deleteOnExit();
    File[] dirs = {dir};
    dsf.setDiskDirs(dirs);
    RegionFactory<Object, Object> factory =
        cache.createRegionFactory(RegionShortcut.REPLICATE_PERSISTENT);
    DiskStore ds = dsf.create("test");
    factory.setDiskSynchronous(false);
    factory.setDiskStoreName(ds.getName());
    factory.setScope(Scope.LOCAL);
    region = factory.create("test");

    LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = true;
    CacheObserver old = CacheObserverHolder.setInstance(

        new CacheObserverAdapter() {

          @Override
          public void goingToFlush() {
            LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = false;
            synchronized (OplogJUnitTest.this) {
              OplogJUnitTest.this.notify();
            }
          }
        });
    region.put("key1", new byte[100]);
    region.put("key2", new byte[100]);
    region.put("key3", new byte[100]);
    region.put("key4", new byte[100]);
    region.put("key5", new byte[100]);
    Thread.sleep(1000);
    assertTrue(LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER);


    ((LocalRegion) region).getDiskRegion().forceRolling();
    synchronized (this) {
      if (LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER) {
        wait(10000);
      }
    }
    assertFalse(LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER);
    CacheObserverHolder.setInstance(old);
    closeDown();
  }

  /**
   * Tests if the byte buffer pool in asynch mode tries to contain the pool size
   *
   */
  @Test
  public void testByteBufferPoolContainment() throws Exception {

    diskProps.setPersistBackup(true);
    diskProps.setRolling(false);
    diskProps.setMaxOplogSize(1024 * 1024);
    diskProps.setSynchronous(false);
    diskProps.setOverflow(false);
    diskProps.setBytesThreshold(10); // this is now item count
    diskProps.setTimeInterval(0);
    region = DiskRegionHelperFactory.getAsyncPersistOnlyRegion(cache, diskProps);
    final byte[] val = new byte[1000];
    LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = true;

    CacheObserverHolder.setInstance(new CacheObserverAdapter() {

      @Override
      public void goingToFlush() { // Delay flushing

        assertEquals(10, region.size());
        for (int i = 10; i < 20; ++i) {
          region.put("" + i, val);
        }
        synchronized (OplogJUnitTest.this) {
          OplogJUnitTest.this.notify();
          LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = false;
        }

      }
    });
    for (int i = 0; i < 10; ++i) {
      region.put("" + i, val);
    }
    synchronized (this) {
      if (LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER) {
        wait(9000);
        assertEquals(false, LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER);
      }
    }
  }

  /**
   * tests async stats are correctly updated
   */
  @Test
  public void testAsyncStats() throws InterruptedException {
    diskProps.setBytesThreshold(101);
    diskProps.setTimeInterval(1000000);
    region = DiskRegionHelperFactory.getAsyncOverFlowAndPersistRegion(cache, diskProps);
    final DiskStoreStats dss = ((LocalRegion) region).getDiskRegion().getDiskStore().getStats();

    assertEquals(0, dss.getQueueSize());
    put100Int();
    await()
        .timeout(10, TimeUnit.SECONDS).untilAsserted(() -> assertEquals(100, dss.getQueueSize()));

    assertEquals(0, dss.getFlushes());

    DiskRegion diskRegion = ((LocalRegion) region).getDiskRegion();
    diskRegion.getDiskStore().flush();
    await()
        .timeout(10, TimeUnit.SECONDS).untilAsserted(() -> assertEquals(0, dss.getQueueSize()));
    await()
        .timeout(10, TimeUnit.SECONDS).untilAsserted(() -> assertEquals(100, dss.getFlushes()));
    put100Int();
    await()
        .timeout(10, TimeUnit.SECONDS).untilAsserted(() -> assertEquals(100, dss.getQueueSize()));
    diskRegion.getDiskStore().flush();
    await()
        .timeout(10, TimeUnit.SECONDS).untilAsserted(() -> assertEquals(0, dss.getQueueSize()));
    await()
        .timeout(10, TimeUnit.SECONDS).untilAsserted(() -> assertEquals(200, dss.getFlushes()));
    closeDown();
  }

  /**
   * Tests delayed creation of DiskID in overflow only mode
   *
   */
  @Test
  public void testDelayedDiskIdCreationInOverflowOnlyMode() {
    diskProps.setPersistBackup(true);
    diskProps.setRolling(false);
    diskProps.setMaxOplogSize(1024 * 1024);
    diskProps.setSynchronous(false);
    diskProps.setOverflow(true);
    diskProps.setBytesThreshold(10000);
    diskProps.setTimeInterval(0);
    diskProps.setOverFlowCapacity(1);
    region = DiskRegionHelperFactory.getAsyncOverFlowOnlyRegion(cache, diskProps);
    final byte[] val = new byte[1000];
    region.put("1", val);
    DiskEntry entry = (DiskEntry) ((LocalRegion) region).basicGetEntry("1");
    assertTrue(entry instanceof AbstractDiskLRURegionEntry);
    assertNull(entry.getDiskId());
    region.put("2", val);
    assertNotNull(entry.getDiskId());
    entry = (DiskEntry) ((LocalRegion) region).basicGetEntry("2");
    assertTrue(entry instanceof AbstractDiskLRURegionEntry);
    assertNull(entry.getDiskId());
  }

  /**
   * Tests immediate creation of DiskID in overflow With Persistence mode
   *
   */
  @Test
  public void testImmediateDiskIdCreationInOverflowWithPersistMode() {
    diskProps.setPersistBackup(true);
    diskProps.setRolling(false);
    diskProps.setMaxOplogSize(1024 * 1024);
    diskProps.setSynchronous(false);
    diskProps.setOverflow(true);
    diskProps.setBytesThreshold(10000);
    diskProps.setTimeInterval(0);
    diskProps.setOverFlowCapacity(1);
    region = DiskRegionHelperFactory.getAsyncOverFlowAndPersistRegion(cache, diskProps);
    final byte[] val = new byte[1000];
    region.put("1", val);
    DiskEntry entry = (DiskEntry) ((LocalRegion) region).basicGetEntry("1");
    assertTrue(entry instanceof AbstractDiskLRURegionEntry);
    assertNotNull(entry.getDiskId());
    region.put("2", val);
    assertNotNull(entry.getDiskId());
    entry = (DiskEntry) ((LocalRegion) region).basicGetEntry("2");
    assertTrue(entry instanceof AbstractDiskLRURegionEntry);
    assertNotNull(entry.getDiskId());
  }

  /**
   * An entry which is evicted to disk will have the flag already written to disk, appropriately set
   *
   */
  @Test
  public void testEntryAlreadyWrittenIsCorrectlyUnmarkedForOverflowOnly() throws Exception {
    diskProps.setPersistBackup(false);
    diskProps.setRolling(false);
    diskProps.setMaxOplogSize(1024 * 1024);
    diskProps.setSynchronous(true);
    diskProps.setOverflow(true);
    diskProps.setBytesThreshold(10000);
    diskProps.setTimeInterval(0);
    diskProps.setOverFlowCapacity(1);
    region = DiskRegionHelperFactory.getSyncOverFlowOnlyRegion(cache, diskProps);
    final byte[] val = new byte[1000];
    region.put("1", val);
    region.put("2", val);
    // "1" should now be on disk
    region.get("1");
    // "2" should now be on disk
    DiskEntry entry1 = (DiskEntry) ((LocalRegion) region).basicGetEntry("1");
    DiskId did1 = entry1.getDiskId();
    DiskId.isInstanceofOverflowIntOplogOffsetDiskId(did1);
    assertTrue(!did1.needsToBeWritten());
    region.put("1", "3");
    assertTrue(did1.needsToBeWritten());
    region.put("2", val);
    DiskEntry entry2 = (DiskEntry) ((LocalRegion) region).basicGetEntry("2");
    DiskId did2 = entry2.getDiskId();
    assertTrue(!did2.needsToBeWritten() || !did1.needsToBeWritten());
    tearDown();
    setUp();
    diskProps.setPersistBackup(false);
    diskProps.setRolling(false);
    long opsize = Integer.MAX_VALUE;
    opsize += 100L;
    diskProps.setMaxOplogSize(opsize);
    diskProps.setSynchronous(true);
    diskProps.setOverflow(true);
    diskProps.setBytesThreshold(10000);
    diskProps.setTimeInterval(0);
    diskProps.setOverFlowCapacity(1);
    region = DiskRegionHelperFactory.getSyncOverFlowOnlyRegion(cache, diskProps);
    region.put("1", val);
    region.put("2", val);
    region.get("1");
    entry1 = (DiskEntry) ((LocalRegion) region).basicGetEntry("1");
    did1 = entry1.getDiskId();
    DiskId.isInstanceofOverflowOnlyWithLongOffset(did1);
    assertTrue(!did1.needsToBeWritten());
    region.put("1", "3");
    assertTrue(did1.needsToBeWritten());
    region.put("2", "3");
    did2 = entry2.getDiskId();
    assertTrue(!did2.needsToBeWritten() || !did1.needsToBeWritten());
  }


  /**
   * An persistent or overflow with persistence entry which is evicted to disk, will have the flag
   * already written to disk, appropriately set
   *
   */
  @Test
  public void testEntryAlreadyWrittenIsCorrectlyUnmarkedForOverflowWithPersistence() {
    diskProps.setPersistBackup(true);
    diskProps.setRolling(false);
    diskProps.setMaxOplogSize(1024 * 1024);
    diskProps.setSynchronous(true);
    diskProps.setOverflow(true);
    diskProps.setBytesThreshold(10000);
    diskProps.setTimeInterval(0);
    diskProps.setOverFlowCapacity(1);
    region = DiskRegionHelperFactory.getSyncOverFlowAndPersistRegion(cache, diskProps);
    final byte[] val = new byte[1000];
    region.put("1", val);
    DiskEntry entry1 = (DiskEntry) ((LocalRegion) region).basicGetEntry("1");
    DiskId did1 = entry1.getDiskId();
    DiskId.isInstanceofPersistIntOplogOffsetDiskId(did1);
    assertTrue(!did1.needsToBeWritten());
    region.put("2", val);
    assertTrue(!did1.needsToBeWritten());
  }

  /**
   * Tests the various DiskEntry.Helper APIs for correctness as there is now delayed creation of
   * DiskId and accessing OplogkeyId will throw UnsupportedException
   */
  @Test
  public void testHelperAPIsForOverflowOnlyRegion() {
    diskProps.setPersistBackup(false);
    diskProps.setRolling(false);
    diskProps.setMaxOplogSize(1024 * 1024);
    diskProps.setSynchronous(true);
    diskProps.setOverflow(true);
    diskProps.setBytesThreshold(10000);
    diskProps.setTimeInterval(0);
    diskProps.setOverFlowCapacity(2);
    region = DiskRegionHelperFactory.getSyncOverFlowOnlyRegion(cache, diskProps);
    final byte[] val = new byte[1000];
    DiskRegion dr = ((LocalRegion) region).getDiskRegion();
    region.put("1", val);
    region.put("2", val);
    region.put("3", val);
    DiskEntry entry1 = (DiskEntry) ((LocalRegion) region).basicGetEntry("1");
    DiskEntry entry2 = (DiskEntry) ((LocalRegion) region).basicGetEntry("2");
    DiskEntry entry3 = (DiskEntry) ((LocalRegion) region).basicGetEntry("3");
    assertNull(entry2.getDiskId());
    assertNull(entry3.getDiskId());
    assertNotNull(entry1.getDiskId());
    assertNull(DiskEntry.Helper.getValueOnDisk(entry3, dr));
    assertNull(DiskEntry.Helper.getValueOnDisk(entry2, dr));
    assertNotNull(DiskEntry.Helper.getValueOnDisk(entry1, dr));
    assertNull(DiskEntry.Helper.getValueOnDisk(entry3, dr));
    assertNull(DiskEntry.Helper.getValueOnDisk(entry2, dr));

    assertNull(entry2.getDiskId());
    assertNull(entry3.getDiskId());
    assertNotNull(entry1.getDiskId());
    assertNull(DiskEntry.Helper.getValueOnDiskOrBuffer(entry3, dr, (LocalRegion) region));
    assertNull(DiskEntry.Helper.getValueOnDiskOrBuffer(entry2, dr, (LocalRegion) region));
    assertNotNull(DiskEntry.Helper.getValueOnDiskOrBuffer(entry1, dr, (LocalRegion) region));
    assertNull(DiskEntry.Helper.getValueOnDisk(entry3, dr));
    assertNull(DiskEntry.Helper.getValueOnDisk(entry2, dr));
  }

  /**
   * Tests the various DiskEntry.Helper APIs for correctness as there is now delayed creation of
   * DiskId and accessing OplogkeyId will throw UnsupportedException
   */
  @Test
  public void testHelperAPIsForOverflowWithPersistenceRegion() {
    helperAPIsForPersistenceWithOrWithoutOverflowRegion(true /* should overflow */);
  }

  /**
   * Tests the various DiskEntry.Helper APIs for correctness as there is now delayed creation of
   * DiskId and accessing OplogkeyId will throw UnsupportedException
   */
  @Test
  public void testHelperAPIsForPersistenceRegion() {
    helperAPIsForPersistenceWithOrWithoutOverflowRegion(false /* should overflow */);
  }

  /**
   * Tests the various DiskEntry.Helper APIs for correctness as there is now delayed creation of
   * DiskId and accessing OplogkeyId will throw UnsupportedException
   */
  private void helperAPIsForPersistenceWithOrWithoutOverflowRegion(boolean overflow) {
    diskProps.setPersistBackup(true);
    diskProps.setRolling(false);
    diskProps.setMaxOplogSize(1024 * 1024);
    diskProps.setSynchronous(true);
    diskProps.setOverflow(overflow);
    diskProps.setBytesThreshold(10000);
    diskProps.setTimeInterval(0);
    diskProps.setOverFlowCapacity(2);
    region = DiskRegionHelperFactory.getSyncOverFlowAndPersistRegion(cache, diskProps);
    final byte[] val = new byte[1000];
    DiskRegion dr = ((LocalRegion) region).getDiskRegion();
    region.put("1", val);
    region.put("2", val);
    region.put("3", val);
    DiskEntry entry1 = (DiskEntry) ((LocalRegion) region).basicGetEntry("1");
    DiskEntry entry2 = (DiskEntry) ((LocalRegion) region).basicGetEntry("2");
    DiskEntry entry3 = (DiskEntry) ((LocalRegion) region).basicGetEntry("3");
    assertNotNull(entry2.getDiskId());
    assertNotNull(entry3.getDiskId());
    assertNotNull(entry1.getDiskId());
    assertNotNull(DiskEntry.Helper.getValueOnDisk(entry3, dr));
    assertNotNull(DiskEntry.Helper.getValueOnDisk(entry2, dr));
    assertNotNull(DiskEntry.Helper.getValueOnDisk(entry1, dr));

    assertNotNull(DiskEntry.Helper.getValueOnDiskOrBuffer(entry3, dr, (LocalRegion) region));
    assertNotNull(DiskEntry.Helper.getValueOnDiskOrBuffer(entry2, dr, (LocalRegion) region));
    assertNotNull(DiskEntry.Helper.getValueOnDiskOrBuffer(entry1, dr, (LocalRegion) region));

    region.close();
    region = DiskRegionHelperFactory.getSyncOverFlowAndPersistRegion(cache, diskProps);
    dr = ((LocalRegion) region).getDiskRegion();
    entry1 = (DiskEntry) ((LocalRegion) region).basicGetEntry("1");
    entry2 = (DiskEntry) ((LocalRegion) region).basicGetEntry("2");
    entry3 = (DiskEntry) ((LocalRegion) region).basicGetEntry("3");

    assertNotNull(entry2.getDiskId());
    assertNotNull(entry3.getDiskId());
    assertNotNull(entry1.getDiskId());

    assertNotNull(DiskEntry.Helper.getValueOnDisk(entry3, dr));
    assertNotNull(DiskEntry.Helper.getValueOnDisk(entry2, dr));
    assertNotNull(DiskEntry.Helper.getValueOnDisk(entry1, dr));

    assertNotNull(DiskEntry.Helper.getValueOnDiskOrBuffer(entry3, dr, (LocalRegion) region));
    assertNotNull(DiskEntry.Helper.getValueOnDiskOrBuffer(entry2, dr, (LocalRegion) region));
    assertNotNull(DiskEntry.Helper.getValueOnDiskOrBuffer(entry1, dr, (LocalRegion) region));
  }

  /**
   * Tests the condition when a 'put' is in progress and concurrent 'clear' and 'put'(on the same
   * key) occur. Thus if after Htree ref was set (in 'put'), the region got cleared (and same key
   * re-'put'), the entry will get recorded in the new Oplog without a corresponding create (
   * because the Oplogs containing create have already been deleted due to the clear operation).
   * This put should not proceed. Also, Region creation after closing should not give an exception.
   */
  @Test
  public void testPutClearPut() throws Exception {
    try {
      // Create a persist only region with rolling true
      diskProps.setPersistBackup(true);
      diskProps.setRolling(true);
      diskProps.setMaxOplogSize(1024);
      diskProps.setSynchronous(true);
      proceed = false;
      region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);
      final Thread clearOp = new Thread(() -> {
        try {
          LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = false;
          region.clear();
          region.put("key1", "value3");
        } catch (Exception e) {
          testFailed = true;
          failureCause = "Encountered Exception=" + e;
        }
      });
      region.getAttributesMutator().setCacheWriter(new CacheWriterAdapter() {
        @Override
        public void beforeUpdate(EntryEvent event) throws CacheWriterException {
          clearOp.start();
        }
      });
      try {
        ThreadUtils.join(clearOp, 30 * 1000);
      } catch (Exception e) {
        testFailed = true;
        failureCause = "Encountered Exception=" + e;
        e.printStackTrace();
      }
      region.create("key1", "value1");
      LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = true;
      region.put("key1", "value2");
      if (!testFailed) {
        region.close();
        region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);
      } else {
        fail(failureCause);
      }
    } finally {
      testFailed = false;
      proceed = false;
      LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = false;
    }
  }

  /**
   * Tests the condition when a 'put' on an alreay created entry and concurrent 'clear' are
   * happening. Thus if after HTree ref was set (in 'put'), the region got cleared (and same key
   * re-'put'), the entry will actually become a create in the VM The new Oplog should record it as
   * a create even though the Htree ref in ThreadLocal will not match with the current Htree Ref.
   * But the operation is valid & should get recorded in Oplog
   *
   */
  @Test
  public void testPutClearCreate() throws Exception {
    failure = false;
    try {
      // Create a persist only region with rolling true
      diskProps.setPersistBackup(true);
      diskProps.setRolling(true);
      diskProps.setMaxOplogSize(1024);
      diskProps.setSynchronous(true);

      region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);

      region.create("key1", "value1");
      LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = true;
      CacheObserverHolder.setInstance(new CacheObserverAdapter() {
        @Override
        public void afterSettingDiskRef() {
          Thread clearTh = new Thread(() -> region.clear());
          clearTh.start();
          try {
            ThreadUtils.join(clearTh, 120 * 1000);
            failure = clearTh.isAlive();
            failureCause = "Clear Thread still running !";
          } catch (Exception e) {
            failure = true;
            failureCause = e.toString();
          }
        }
      });
      region.put("key1", "value2");
      LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = false;
      assertFalse(failureCause, failure);
      assertEquals(1, region.size());
      region.close();
      region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);
      assertEquals(1, region.size());
      assertEquals("value2", region.get("key1"));
    } finally {
      testFailed = false;
      proceed = false;
      LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = false;
      CacheObserverHolder.setInstance(new CacheObserverAdapter());
      failure = false;
    }
  }

  /**
   * Tests if 'destroy' transaction is working correctly for sync-overflow-only disk region entry
   */
  @Test
  public void testOverFlowOnlySyncDestroyTx() {
    diskProps.setMaxOplogSize(20480);
    diskProps.setOverFlowCapacity(1);
    diskProps.setDiskDirs(dirs);
    region = DiskRegionHelperFactory.getSyncOverFlowOnlyRegion(cache, diskProps);
    assertNotNull(region);
    region.put("key", "createValue");
    region.put("key1", "createValue1");
    cache.getCacheTransactionManager().begin();
    region.destroy("key");
    cache.getCacheTransactionManager().commit();
    assertNull("The deleted entry should have been null",
        ((LocalRegion) region).entries.getEntry("key"));
  }

  /**
   * Test to force a recovery to follow the path of switchOutFilesForRecovery and ensuring that
   * IOExceptions do not come as a result. This is also a bug test for bug 37682
   */
  @Test
  public void testSwitchFilesForRecovery() throws Exception {
    region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, null, Scope.LOCAL);
    put100Int();
    ((LocalRegion) region).getDiskRegion().forceRolling();
    Thread.sleep(2000);
    put100Int();
    int sizeOfRegion = region.size();
    region.close();
    region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, null, Scope.LOCAL);

    if (sizeOfRegion != region.size()) {
      fail(" Expected region size to be " + sizeOfRegion + " after recovery but it is "
          + region.size());
    }
  }

  /**
   * tests directory stats are correctly updated in case of single directory (for bug 37531)
   */
  @Test
  public void testPersist1DirStats() {
    final AtomicBoolean freezeRoller = new AtomicBoolean();
    CacheObserver old = CacheObserverHolder.setInstance(new CacheObserverAdapter() {
      private volatile boolean didBeforeCall = false;

      @Override
      public void beforeGoingToCompact() {
        didBeforeCall = true;
        synchronized (freezeRoller) {
          if (!assertDone) {
            try {
              // Here, we are not allowing the Roller thread to roll the old oplog into htree
              while (!freezeRoller.get()) {
                freezeRoller.wait();
              }
              freezeRoller.set(false);
            } catch (InterruptedException e) {
              fail("interrupted");
            }
          }
        }
      }

      @Override
      public void afterHavingCompacted() {
        if (didBeforeCall) {
          didBeforeCall = false;
          LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = false;
          checkDiskStats();
        }
      }
    });
    try {
      final int MAX_OPLOG_SIZE = 500;
      diskProps.setMaxOplogSize(MAX_OPLOG_SIZE);
      diskProps.setPersistBackup(true);
      diskProps.setRolling(true);
      diskProps.setSynchronous(true);
      diskProps.setOverflow(false);
      diskProps.setDiskDirsAndSizes(new File[] {dirs[0]}, new int[] {4000});
      final byte[] val = new byte[200];
      region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);
      LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = true;
      region.put("key1", val);
      checkDiskStats();
      region.put("key2", val);
      checkDiskStats();
      // This put will cause a switch as max-oplog size (500) will be exceeded (600)
      region.put("key3", val);
      synchronized (freezeRoller) {
        checkDiskStats();
        assertDone = true;
        freezeRoller.set(true);
        freezeRoller.notifyAll();
      }

      region.close();
      closeDown();
      // Stop rolling to get accurate estimates:
      diskProps.setRolling(false);

      region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);

      // On recreating the region after closing, old Oplog file gets rolled into htree
      // "Disk space usage zero when region recreated"
      checkDiskStats();
      region.put("key4", val);
      checkDiskStats();
      region.put("key5", val);
      checkDiskStats();
      assertDone = false;
      LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = true;
      region.put("key6", val);
      // again we expect a switch in oplog here
      synchronized (freezeRoller) {
        checkDiskStats();
        assertDone = true;
        freezeRoller.set(true);
        freezeRoller.notifyAll();
      }
      region.close();
    } catch (Exception e) {
      e.printStackTrace();
      fail("Test failed due to exception" + e);
    } finally {
      LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = false;
      CacheObserverHolder.setInstance(old);
      synchronized (freezeRoller) {
        assertDone = true;
        freezeRoller.set(true);
        freezeRoller.notifyAll();
      }
    }
  }

  /**
   * Tests reduction in size of disk stats when the oplog is rolled.
   */
  @Test
  public void testStatsSizeReductionOnRolling() throws Exception {
    final int MAX_OPLOG_SIZE = 500 * 2;
    diskProps.setMaxOplogSize(MAX_OPLOG_SIZE);
    diskProps.setPersistBackup(true);
    diskProps.setRolling(true);
    diskProps.setCompactionThreshold(100);
    diskProps.setSynchronous(true);
    diskProps.setOverflow(false);
    diskProps.setDiskDirsAndSizes(new File[] {dirs[0]}, new int[] {4000});
    final byte[] val = new byte[333];
    region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);
    final DiskRegion dr = ((LocalRegion) region).getDiskRegion();

    // calculate sizes
    final int extra_byte_num_per_entry =
        InternalDataSerializer.calculateBytesForTSandDSID(getDSID((LocalRegion) region));
    final int key3_size =
        CompactOfflineDiskStoreJUnitTest.getSize4Create(extra_byte_num_per_entry, "key3", val);
    final int tombstone_key1 =
        CompactOfflineDiskStoreJUnitTest.getSize4TombstoneWithKey(extra_byte_num_per_entry, "key1");
    final int tombstone_key2 =
        CompactOfflineDiskStoreJUnitTest.getSize4TombstoneWithKey(extra_byte_num_per_entry, "key2");

    CountDownLatch putsCompleted = new CountDownLatch(1);
    // TODO: move static methods from CompactOfflineDiskStoreJUnitTest to shared util class
    StatSizeTestCacheObserverAdapter testObserver = new StatSizeTestCacheObserverAdapter(dr,
        key3_size, tombstone_key1, tombstone_key2, putsCompleted);
    CacheObserver old = CacheObserverHolder.setInstance(testObserver);

    try {

      LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = true;
      cache.getLogger().info("putting key1");
      region.put("key1", val);
      checkDiskStats();
      cache.getLogger().info("putting key2");
      region.put("key2", val);
      checkDiskStats();

      cache.getLogger().info("removing key1");
      region.remove("key1");
      cache.getLogger().info("removing key2");
      region.remove("key2");

      // This put will cause a switch as max-oplog size (900) will be exceeded (999)
      testObserver.setSwitchExpected();
      cache.getLogger().info("putting key3");
      region.put("key3", val);
      putsCompleted.countDown();
      cache.getLogger().info("waiting for compaction");
      await().until(testObserver::hasCompacted);
      assertFalse(testObserver.exceptionOccured());

      region.close();
    } finally {
      LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = false;
      CacheObserverHolder.setInstance(old);
    }
  }

  @Test
  public void testUnPreblowOnRegionCreate() throws Exception {
    final int MAX_OPLOG_SIZE = 20000;
    diskProps.setMaxOplogSize(MAX_OPLOG_SIZE);
    diskProps.setPersistBackup(true);
    diskProps.setRolling(true);
    diskProps.setSynchronous(true);
    diskProps.setOverflow(false);
    diskProps.setDiskDirsAndSizes(new File[] {dirs[0]}, new int[] {40000});
    region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);

    try {
      LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = true;
      for (int i = 0; i < 10; ++i) {
        region.put("key-" + i, "value-");
      }

      assertEquals(18000, getOplogFileSizeSum(dirs[0], ".crf"));
      assertEquals(2000, getOplogFileSizeSum(dirs[0], ".drf"));

      // make a copy of inflated crf. use this to replace compacted crf to
      // simulate incomplete diskStore close
      File[] files = dirs[0].listFiles();
      for (File file : files) {
        if (file.getName().endsWith(".crf") || file.getName().endsWith(".drf")) {
          File inflated = new File(file.getAbsolutePath() + "_inflated");
          FileUtils.copyFile(file, inflated);
        }
      }

      cache.close();
      assertTrue(500 > getOplogFileSizeSum(dirs[0], ".crf"));
      assertTrue(100 > getOplogFileSizeSum(dirs[0], ".drf"));

      // replace compacted crf with inflated crf and remove krf
      files = dirs[0].listFiles();
      for (File file : files) {
        String name = file.getName();
        if (name.endsWith(".krf") || name.endsWith(".crf") || name.endsWith(".drf")) {
          file.delete();
        }
      }
      for (File file : files) {
        String name = file.getName();
        if (name.endsWith("_inflated")) {
          assertTrue(file.renameTo(new File(file.getAbsolutePath().replace("_inflated", ""))));
        }
      }
      assertEquals(18000, getOplogFileSizeSum(dirs[0], ".crf"));
      assertEquals(2000, getOplogFileSizeSum(dirs[0], ".drf"));

      createCache();
      region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);
      for (int i = 10; i < 20; ++i) {
        region.put("key-" + i, "value-");
      }

      int sizeCrf = getOplogFileSizeSum(dirs[0], ".crf");
      assertTrue("crf too big:" + sizeCrf, sizeCrf < 18000 + 500);
      assertTrue("crf too small:" + sizeCrf, sizeCrf > 18000);
      int sizeDrf = getOplogFileSizeSum(dirs[0], ".drf");
      assertTrue("drf too big:" + sizeDrf, sizeDrf < 2000 + 100);
      assertTrue("drf too small:" + sizeDrf, sizeDrf > 2000);

      // test that region recovery does not cause unpreblow
      region.close();
      region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);

      assertEquals(sizeCrf, getOplogFileSizeSum(dirs[0], ".crf"));
      assertEquals(sizeDrf, getOplogFileSizeSum(dirs[0], ".drf"));
    } finally {
      LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = false;
    }
  }

  private int getOplogFileSizeSum(File dir, String type) {
    int sum = 0;
    File[] files = dir.listFiles();
    for (File file : files) {
      String name = file.getName();
      if (name.endsWith(type)) {
        sum += file.length();
      }
    }
    return sum;
  }

  @Test
  public void testMagicSeqPresence() throws Exception {
    final int MAX_OPLOG_SIZE = 200;
    diskProps.setMaxOplogSize(MAX_OPLOG_SIZE);
    diskProps.setPersistBackup(true);
    diskProps.setRolling(true);
    diskProps.setSynchronous(true);
    diskProps.setOverflow(false);
    diskProps.setDiskDirsAndSizes(new File[] {dirs[0]}, new int[] {4000});
    region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);

    // 3 types of oplog files will be verified
    verifyOplogHeader(dirs[0], ".if", ".crf", ".drf");

    try {
      LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = true;
      IntStream.range(0, 20).forEach(i -> region.put("key-" + i, "value-" + i));
      // krf is created, so 4 types of oplog files will be verified
      verifyOplogHeader(dirs[0], ".if", ".crf", ".drf", ".krf");

      region.close();
      region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);

      verifyOplogHeader(dirs[0], ".if", ".crf", ".drf", ".krf");
      region.close();
    } finally {
      LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = false;
    }
  }

  private void verifyOplogHeader(File dir, String... oplogTypes) throws IOException {

    await().until(() -> {
      List<String> types = new ArrayList<>(Arrays.asList(oplogTypes));
      Arrays.stream(dir.listFiles()).map(File::getName).map(f -> f.substring(f.indexOf(".")))
          .forEach(types::remove);
      return types.isEmpty();
    });

    File[] files = dir.listFiles();
    HashSet<String> verified = new HashSet<>();
    for (File file : files) {
      String name = file.getName();
      byte[] expect = new byte[Oplog.OPLOG_MAGIC_SEQ_REC_SIZE];
      if (name.endsWith(".crf")) {
        expect[0] = Oplog.OPLOG_MAGIC_SEQ_ID;
        System.arraycopy(OPLOG_TYPE.CRF.getBytes(), 0, expect, 1, OPLOG_TYPE.getLen());
        verified.add(".crf");
      } else if (name.endsWith(".drf")) {
        expect[0] = Oplog.OPLOG_MAGIC_SEQ_ID;
        System.arraycopy(OPLOG_TYPE.DRF.getBytes(), 0, expect, 1, OPLOG_TYPE.getLen());
        verified.add(".drf");
      } else if (name.endsWith(".krf")) {
        expect[0] = Oplog.OPLOG_MAGIC_SEQ_ID;
        System.arraycopy(OPLOG_TYPE.KRF.getBytes(), 0, expect, 1, OPLOG_TYPE.getLen());
        verified.add(".krf");
      } else if (name.endsWith(".if")) {
        expect[0] = DiskInitFile.OPLOG_MAGIC_SEQ_ID;
        System.arraycopy(OPLOG_TYPE.IF.getBytes(), 0, expect, 1, OPLOG_TYPE.getLen());
        verified.add(".if");
      } else {
        System.out.println("Ignored: " + file);
        continue;
      }
      expect[expect.length - 1] = 21; // EndOfRecord

      byte[] buf = new byte[Oplog.OPLOG_MAGIC_SEQ_REC_SIZE];

      FileInputStream fis = new FileInputStream(file);
      int count = fis.read(buf, 0, 8);
      fis.close();

      System.out.println("Verifying: " + file);
      assertEquals("expected a read to return 8 but it returned " + count + " for file " + file, 8,
          count);
      assertTrue(Arrays.equals(expect, buf));
    }

    assertEquals(oplogTypes.length, verified.size());
  }

  /**
   * Tests if without rolling the region size before close is same as after recreation
   */
  @Test
  public void testSizeStatsAfterRecreationInAsynchMode() throws Exception {
    final int MAX_OPLOG_SIZE = 1000;
    diskProps.setMaxOplogSize(MAX_OPLOG_SIZE);
    diskProps.setPersistBackup(true);
    diskProps.setRolling(false);
    diskProps.setSynchronous(false);
    diskProps.setBytesThreshold(800);
    diskProps.setOverflow(false);
    diskProps.setDiskDirsAndSizes(new File[] {dirs[0], dirs[1]}, new int[] {4000, 4000});
    final byte[] val = new byte[25];
    region = DiskRegionHelperFactory.getAsyncPersistOnlyRegion(cache, diskProps);
    DiskRegion dr = ((LocalRegion) region).getDiskRegion();

    try {
      LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = true;
      for (int i = 0; i < 42; ++i) {
        region.put("key" + i, val);
      }
      // need to wait for writes to happen before getting size
      dr.flushForTesting();
      long size1 = 0;
      for (DirectoryHolder dh : dr.getDirectories()) {
        size1 += dh.getDirStatsDiskSpaceUsage();
      }
      System.out.println("Size before close = " + size1);
      region.close();
      diskProps.setSynchronous(true);
      region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);
      dr = ((LocalRegion) region).getDiskRegion();
      long size2 = 0;
      for (DirectoryHolder dh : dr.getDirectories()) {
        size2 += dh.getDirStatsDiskSpaceUsage();
      }
      System.out.println("Size after recreation= " + size2);
      assertEquals(size1, size2);
      region.close();
    } finally {
      LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = false;
    }
  }

  @Test
  public void testAsynchModeStatsBehaviour() throws Exception {
    final int MAX_OPLOG_SIZE = 1000;
    diskProps.setMaxOplogSize(MAX_OPLOG_SIZE);
    diskProps.setPersistBackup(true);
    diskProps.setRolling(false);
    diskProps.setSynchronous(false);
    diskProps.setBytesThreshold(800);
    diskProps.setTimeInterval(Long.MAX_VALUE);
    diskProps.setOverflow(false);
    diskProps.setDiskDirsAndSizes(new File[] {dirs[0]}, new int[] {4000});
    final byte[] val = new byte[25];
    region = DiskRegionHelperFactory.getAsyncPersistOnlyRegion(cache, diskProps);
    DiskRegion dr = ((LocalRegion) region).getDiskRegion();

    try {
      LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = true;
      for (int i = 0; i < 4; ++i) {
        region.put("key" + i, val);
      }
      // This test now has a race condition in it since
      // async puts no longer increment disk space.
      // It is not until a everything is flushed that we will know the disk size.
      dr.flushForTesting();

      checkDiskStats();
      long size1 = 0;
      for (DirectoryHolder dh : dr.getDirectories()) {
        size1 += dh.getDirStatsDiskSpaceUsage();
      }
      System.out.println("Size before close = " + size1);
      region.close();
      diskProps.setSynchronous(true);
      region = DiskRegionHelperFactory.getSyncPersistOnlyRegion(cache, diskProps, Scope.LOCAL);
      dr = ((LocalRegion) region).getDiskRegion();
      long size2 = 0;
      for (DirectoryHolder dh : dr.getDirectories()) {
        size2 += dh.getDirStatsDiskSpaceUsage();
      }
      System.out.println("Size after recreation= " + size2);
      assertEquals(size1, size2);
      region.close();
    } finally {
      LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = false;
    }
  }

  private long diskSpaceUsageStats() {
    return ((LocalRegion) region).getDiskRegion().getInfoFileDir().getDirStatsDiskSpaceUsage();
  }

  private long calculatedDiskSpaceUsageStats() {
    return oplogSize();
  }

  private void checkDiskStats() {
    long actualDiskStats = diskSpaceUsageStats();
    long computedDiskStats = calculatedDiskSpaceUsageStats();
    int tries = 0;
    while (actualDiskStats != computedDiskStats && tries++ <= 100) {
      // race conditions exist in which the stats change
      try {
        Thread.sleep(100);
      } catch (InterruptedException ignore) {
      }
      actualDiskStats = diskSpaceUsageStats();
      computedDiskStats = calculatedDiskSpaceUsageStats();
    }
    assertEquals(computedDiskStats, actualDiskStats);
  }

  private long oplogSize() {
    long size = ((LocalRegion) region).getDiskRegion().getDiskStore().undeletedOplogSize.get();
    Oplog[] opArray =
        ((LocalRegion) region).getDiskRegion().getDiskStore().getPersistentOplogs().getAllOplogs();
    if (opArray != null) {
      for (Oplog log : opArray) {
        size += log.getOplogSize();
      }
    }
    return size;
  }

  private int getDSID(LocalRegion lr) {
    return lr.getDistributionManager().getDistributedSystemId();
  }

  private class StatSizeTestCacheObserverAdapter extends CacheObserverAdapter {
    private final AtomicBoolean switchExpected = new AtomicBoolean(false);
    private final DiskRegion dr;
    private final AtomicBoolean hasCompacted = new AtomicBoolean(false);
    private final int key3Size;
    private final int tombstoneKey1;
    private final int tombstoneKey2;
    private final AtomicBoolean exceptionOccurred = new AtomicBoolean(true);
    private volatile long spaceUsageBefore = -1;
    private DirectoryHolder dh;
    private final AtomicLong oplogsSize = new AtomicLong();
    private final CountDownLatch putsCompleted;

    StatSizeTestCacheObserverAdapter(DiskRegion dr, int key3Size, int tombstoneKey1,
        int tombstoneKey2, CountDownLatch putsCompleted) {
      this.dr = dr;
      this.key3Size = key3Size;
      this.tombstoneKey1 = tombstoneKey1;
      this.tombstoneKey2 = tombstoneKey2;
      this.putsCompleted = putsCompleted;
    }

    @Override
    public void beforeSwitchingOplog() {
      cache.getLogger().info("beforeSwitchingOplog");
      if (!switchExpected.get()) {
        fail("unexpected oplog switch");
      }
      if (spaceUsageBefore == -1) {
        // only want to call this once; before the 1st oplog destroy
        dh = dr.getNextDir();
        spaceUsageBefore = dh.getDirStatsDiskSpaceUsage();
      }
    }

    @Override
    public void beforeDeletingCompactedOplog(Oplog oplog) {
      cache.getLogger().info("beforeDeletingCompactedOplog");
      oplogsSize.addAndGet(oplog.getOplogSize());
    }

    @Override
    public void afterHavingCompacted() {
      try {
        putsCompleted.await(10, TimeUnit.SECONDS);
      } catch (InterruptedException e) {
        exceptionOccurred.set(true);
        throw new RuntimeException(e);
      }
      cache.getLogger().info("afterHavingCompacted");
      if (spaceUsageBefore > -1) {
        hasCompacted.set(true);
        long after = dh.getDirStatsDiskSpaceUsage();
        // after compaction, in _2.crf, key3 is an create-entry,
        // key1 and key2 are tombstones.
        // _2.drf contained a rvvgc with drMap.size()==1
        int expectedDrfSize = Oplog.OPLOG_DISK_STORE_REC_SIZE + Oplog.OPLOG_MAGIC_SEQ_REC_SIZE
            + Oplog.OPLOG_GEMFIRE_VERSION_REC_SIZE
            + CompactOfflineDiskStoreJUnitTest.getRVVSize(1, new int[] {0}, true);
        int expectedCrfSize = Oplog.OPLOG_DISK_STORE_REC_SIZE + Oplog.OPLOG_MAGIC_SEQ_REC_SIZE
            + Oplog.OPLOG_GEMFIRE_VERSION_REC_SIZE
            + CompactOfflineDiskStoreJUnitTest.getRVVSize(1, new int[] {1}, false)
            + Oplog.OPLOG_NEW_ENTRY_BASE_REC_SIZE + key3Size + tombstoneKey1 + tombstoneKey2;
        int oplog2Size = expectedDrfSize + expectedCrfSize;
        if (after != oplog2Size) {
          cache.getLogger().info("test failed before=" + spaceUsageBefore + " after=" + after
              + " expected=" + oplog2Size);
          exceptionOccurred.set(true);
        } else {
          exceptionOccurred.set(false);
        }
        LocalRegion.ISSUE_CALLBACKS_TO_CACHE_OBSERVER = false;
      }
    }

    boolean hasCompacted() {
      return hasCompacted.get();
    }

    boolean exceptionOccured() {
      return exceptionOccurred.get();
    }

    void setSwitchExpected() {
      switchExpected.set(true);
    }
  }
}
