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

import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.apache.commons.io.FileUtils.listFiles;
import static org.apache.commons.io.filefilter.DirectoryFileFilter.DIRECTORY;
import static org.apache.geode.cache.EvictionAction.OVERFLOW_TO_DISK;
import static org.apache.geode.cache.EvictionAttributes.createLIFOEntryAttributes;
import static org.apache.geode.cache.RegionShortcut.PARTITION;
import static org.apache.geode.cache.RegionShortcut.PARTITION_PERSISTENT;
import static org.apache.geode.cache.RegionShortcut.REPLICATE;
import static org.apache.geode.cache.RegionShortcut.REPLICATE_PERSISTENT;
import static org.apache.geode.internal.admin.remote.AdminFailureResponse.create;
import static org.apache.geode.test.dunit.IgnoredException.addIgnoredException;
import static org.apache.geode.test.dunit.VM.getVM;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;

import junitparams.Parameters;
import junitparams.naming.TestCaseName;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.RegexFileFilter;
import org.apache.logging.log4j.Logger;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.apache.geode.cache.CacheClosedException;
import org.apache.geode.cache.DiskStoreFactory;
import org.apache.geode.cache.EvictionAttributes;
import org.apache.geode.cache.PartitionAttributesFactory;
import org.apache.geode.cache.PartitionedRegionStorageException;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.RegionFactory;
import org.apache.geode.cache.control.RebalanceOperation;
import org.apache.geode.cache.control.RebalanceResults;
import org.apache.geode.cache.persistence.PartitionOfflineException;
import org.apache.geode.distributed.DistributedMember;
import org.apache.geode.distributed.internal.ClusterDistributionManager;
import org.apache.geode.distributed.internal.DistributionMessage;
import org.apache.geode.distributed.internal.DistributionMessageObserver;
import org.apache.geode.distributed.internal.ReplyMessage;
import org.apache.geode.distributed.internal.locks.DLockRequestProcessor;
import org.apache.geode.internal.admin.remote.AdminFailureResponse;
import org.apache.geode.internal.cache.DestroyRegionOperation.DestroyRegionMessage;
import org.apache.geode.internal.cache.InternalCache;
import org.apache.geode.internal.lang.SystemUtils;
import org.apache.geode.internal.process.ProcessStreamReader;
import org.apache.geode.internal.process.ProcessStreamReader.ReadingMode;
import org.apache.geode.logging.internal.log4j.api.LogService;
import org.apache.geode.management.BackupStatus;
import org.apache.geode.test.dunit.AsyncInvocation;
import org.apache.geode.test.dunit.VM;
import org.apache.geode.test.dunit.internal.JUnit4DistributedTestCase;
import org.apache.geode.test.dunit.rules.CacheRule;
import org.apache.geode.test.dunit.rules.DistributedDiskDirRule;
import org.apache.geode.test.dunit.rules.DistributedRule;
import org.apache.geode.test.junit.rules.serializable.SerializableTemporaryFolder;
import org.apache.geode.test.junit.rules.serializable.SerializableTestName;
import org.apache.geode.test.junit.runners.GeodeParamsRunner;

/**
 * Additional tests to consider adding:
 * <ul>
 * <li>Test default disk store.
 * <li>Test backing up and recovering while a bucket move is in progress.
 * <li>Test backing up and recovering while ops are in progress?
 * </ul>
 */
@RunWith(GeodeParamsRunner.class)
@SuppressWarnings("serial")
public class BackupDistributedTest extends JUnit4DistributedTestCase implements Serializable {
  private static final Logger logger = LogService.getLogger();

  private static final int NUM_BUCKETS = 15;

  private String regionName1;
  private String regionName2;
  private String regionName3;

  private File backupBaseDir;

  private VM vm0;
  private VM vm1;
  private VM vm2;
  private VM vm3;

  private transient Process process;
  private transient ProcessStreamReader processReader;

  @Rule
  public DistributedRule distributedRule = new DistributedRule();

  @Rule
  public CacheRule cacheRule = new CacheRule();

  @Rule
  public DistributedDiskDirRule diskDirRule = new DistributedDiskDirRule();

  @Rule
  public SerializableTemporaryFolder temporaryFolder = new SerializableTemporaryFolder();

  @Rule
  public SerializableTestName testName = new SerializableTestName();

  @Before
  public void setUp() throws Exception {
    vm0 = getVM(0);
    vm1 = getVM(1);
    vm2 = getVM(2);
    vm3 = getVM(3);

    String uniqueName = getClass().getSimpleName() + "_" + testName.getMethodName();
    regionName1 = uniqueName + "_region1";
    regionName2 = uniqueName + "_region2";
    regionName3 = uniqueName + "_region3";

    backupBaseDir = temporaryFolder.newFolder("backupDir");
  }

  @After
  public void tearDown() throws Exception {
    vm0.invoke(() -> {
      DistributionMessageObserver.setInstance(null);
      disconnectFromDS();
    });

    if (process != null && process.isAlive()) {
      process.destroyForcibly();
      process.waitFor(2, MINUTES);
    }
    if (processReader != null && processReader.isRunning()) {
      processReader.stop();
    }
  }

  @Test
  public void testBackupPR() throws Exception {
    createPersistentRegions(vm0, vm1);

    long lastModified0 = vm0.invoke(() -> setBackupFiles(getDiskDirFor(vm0)));
    long lastModified1 = vm1.invoke(() -> setBackupFiles(getDiskDirFor(vm1)));

    vm0.invoke(() -> {
      createData(0, 5, "A", regionName1);
      createData(0, 5, "B", regionName2);
    });

    vm0.invoke(() -> {
      assertThat(getCache().getDistributionManager().getNormalDistributionManagerIds()).hasSize(2);
    });

    vm2.invoke(() -> {
      assertThat(getCache().getDistributionManager().getNormalDistributionManagerIds()).hasSize(3);
    });

    BackupStatus status = vm2.invoke(this::backupMember);
    assertThat(status.getBackedUpDiskStores()).hasSize(2);
    assertThat(status.getOfflineDiskStores()).isEmpty();

    Collection<File> files = FileUtils.listFiles(backupBaseDir, new String[] {"txt"}, true);
    assertThat(files).hasSize(4);

    vm0.invoke(() -> deleteOldUserFile(getDiskDirFor(vm0)));
    vm1.invoke(() -> deleteOldUserFile(getDiskDirFor(vm1)));

    validateBackupComplete();

    vm0.invoke(() -> {
      createData(0, 5, "C", regionName1);
      createData(0, 5, "C", regionName2);
    });

    assertThat(status.getBackedUpDiskStores()).hasSize(2);
    assertThat(status.getOfflineDiskStores()).isEmpty();

    vm0.invoke(() -> getCache().close());
    vm1.invoke(() -> getCache().close());

    // destroy the current data
    cleanDiskDirsInEveryVM();

    restoreBackup(2);

    createPersistentRegions(vm0, vm1);

    vm0.invoke(() -> {
      validateData(0, 5, "A", regionName1);
      validateData(0, 5, "B", regionName2);
    });

    vm0.invoke(() -> verifyUserFileRestored(getDiskDirFor(vm0), lastModified0));
    vm1.invoke(() -> verifyUserFileRestored(getDiskDirFor(vm1), lastModified1));
  }

  @Test
  public void testBackupDuringGII() throws Exception {
    getBlackboard().initBlackboard();
    String diskStoreName = getUniqueName();

    vm0.invoke(() -> createReplicatedRegion(regionName1, diskStoreName, getDiskStoreFor(vm0), true,
        false));
    vm0.invoke(() -> createReplicatedRegion(regionName2, diskStoreName, getDiskStoreFor(vm0), true,
        false));

    vm1.invoke(
        () -> createReplicatedRegion(regionName1, diskStoreName, getDiskStoreFor(vm1), true, true));

    vm0.invoke(() -> {
      createData(0, 5, "A", regionName1);
      createData(0, 5, "B", regionName2);
    });

    vm2.invoke(() -> {
      DistributionMessageObserver.setInstance(
          createTestHookToCreateRegionBeforeSendingPrepareBackupRequest());
    });

    AsyncInvocation<Void> asyncVM1CreateRegion = vm1.invokeAsync(() -> {
      getBlackboard().waitForGate("createRegion", 30, SECONDS);
      createReplicatedRegion(regionName2, diskStoreName, getDiskStoreFor(vm1), false, true);
      getBlackboard().signalGate("createdRegion");
    });


    vm2.invoke(this::backupMember);
    validateBackupComplete();
    vm2.invoke(() -> getCache().close());

    asyncVM1CreateRegion.join();

    vm0.invoke(() -> {
      validateData(0, 5, "A", regionName1);
      validateData(0, 5, "B", regionName2);
    });

    vm0.invoke(() -> getCache().close());
    vm1.invoke(() -> getCache().close());

    // destroy the current data
    cleanDiskDirsInEveryVM();

    restoreBackup(2);

    vm1.invoke(() -> {
      getCache().getPartitionedRegionLockService().becomeLockGrantor();
    });

    AsyncInvocation<Void> asyncVM1Region1 = vm1.invokeAsync(() -> {
      createReplicatedRegion(regionName1, diskStoreName, getDiskStoreFor(vm1), true, true);
    });

    AsyncInvocation<Void> asyncVM1Region2 = vm1.invokeAsync(() -> {
      createReplicatedRegion(regionName2, diskStoreName, getDiskStoreFor(vm1), false, true);
      getBlackboard().signalGate("createdRegionAfterRecovery");
    });

    vm0.invoke(() -> createReplicatedRegion(regionName1, diskStoreName, getDiskStoreFor(vm0), true,
        false));

    vm0.invoke(() -> {
      DistributionMessageObserver.setInstance(
          createTestHookToWaitForRegionCreationBeforeSendingDLockRequestMessage());
    });

    vm0.invoke(() -> createReplicatedRegion(regionName2, diskStoreName, getDiskStoreFor(vm0), true,
        false));

    asyncVM1Region1.join();
    asyncVM1Region2.join();

    vm0.invoke(() -> {
      validateData(0, 5, "A", regionName1);
      validateData(0, 5, "B", regionName2);
    });

    vm1.invoke(() -> {
      validateData(0, 5, "A", regionName1);
      validateData(0, 5, "B", regionName2);
    });
  }


  /**
   * Test of bug 42419.
   *
   * <p>
   * TRAC #42419: backed up disk stores map contains null key instead of member; cannot restore
   * backup files
   */
  @Test
  public void testBackupFromMemberWithDiskStore() throws Exception {
    createPersistentRegions(vm0, vm1);

    vm0.invoke(() -> {
      createData(0, 5, "A", regionName1);
      createData(0, 5, "B", regionName2);
    });

    BackupStatus status = vm1.invoke(this::backupMember);
    assertThat(status.getBackedUpDiskStores()).hasSize(2);

    for (DistributedMember key : status.getBackedUpDiskStores().keySet()) {
      assertThat(key).isNotNull();
    }
    assertThat(status.getOfflineDiskStores()).isEmpty();

    validateBackupComplete();

    vm0.invoke(() -> getCache().close());
    vm1.invoke(() -> getCache().close());

    // destroy the current data
    cleanDiskDirsInEveryVM();

    restoreBackup(2);

    createPersistentRegions(vm0, vm1);

    vm0.invoke(() -> {
      validateData(0, 5, "A", regionName1);
      validateData(0, 5, "B", regionName2);
    });
  }

  /**
   * Test for bug 42419
   *
   * <p>
   * TRAC #42419: backed up disk stores map contains null key instead of member; cannot restore
   * backup files
   */
  @Test
  public void testBackupWhileBucketIsCreated() throws Exception {
    vm0.invoke(() -> createRegions(vm0));

    // create a bucket on vm0
    vm0.invoke(() -> createData(0, 1, "A", regionName1));

    // create the pr on vm1, which won't have any buckets
    vm1.invoke(() -> createRegions(vm1));

    CompletableFuture<BackupStatus> backupStatusFuture =
        CompletableFuture.supplyAsync(() -> vm2.invoke(this::backupMember));

    CompletableFuture<Void> createDataFuture =
        CompletableFuture.runAsync(() -> vm0.invoke(() -> createData(1, 5, "A", regionName1)));

    CompletableFuture.allOf(backupStatusFuture, createDataFuture);

    BackupStatus status = backupStatusFuture.get();
    assertThat(status.getBackedUpDiskStores()).hasSize(2);
    assertThat(status.getOfflineDiskStores()).isEmpty();

    validateBackupComplete();

    vm0.invoke(() -> createData(0, 5, "C", regionName1));

    assertThat(status.getBackedUpDiskStores()).hasSize(2);
    assertThat(status.getOfflineDiskStores()).isEmpty();

    vm0.invoke(() -> getCache().close());
    vm1.invoke(() -> getCache().close());

    // destroy the current data
    cleanDiskDirsInEveryVM();

    restoreBackup(2);

    createPersistentRegions(vm0, vm1);

    vm0.invoke(() -> validateData(0, 1, "A", regionName1));

  }

  /**
   * Test for bug 42420. Invoke a backup when a bucket is in the middle of being moved.
   *
   * <p>
   * TRAC #42420: Online backup files sometimes cannot be restored
   */
  @Test
  @Parameters({"BEFORE_SENDING_DESTROYREGIONMESSAGE", "BEFORE_PROCESSING_REPLYMESSAGE"})
  @TestCaseName("{method}({params})")
  public void testWhileBucketIsMovedBackup(final WhenToInvokeBackup whenToInvokeBackup)
      throws Exception {
    vm0.invoke("Add listener to invoke backup", () -> {
      disconnectFromDS();

      // This listener will wait for a response to the
      // destroy region message, and then trigger a backup.
      // That will backup before this member has finished destroying
      // a bucket, but after the peer has removed the bucket.
      DistributionMessageObserver.setInstance(createTestHookToBackup(whenToInvokeBackup));
    });

    vm0.invoke(() -> createRegions(vm0));

    // create twos bucket on vm0
    vm0.invoke(() -> createData(0, 2, "A", regionName1));

    // create the pr on vm1, which won't have any buckets
    vm1.invoke(() -> createRegions(vm1));

    // Perform a rebalance. This will trigger the backup in the middle of the bucket move.
    vm0.invoke("Do rebalance", () -> {
      RebalanceOperation op = getCache().getResourceManager().createRebalanceFactory().start();
      RebalanceResults results = op.getResults();
      assertThat(results.getTotalBucketTransfersCompleted()).isEqualTo(1);
    });

    validateBackupComplete();

    vm0.invoke(() -> createData(0, 5, "C", regionName1));

    vm0.invoke(() -> getCache().close());
    vm1.invoke(() -> getCache().close());

    // Destroy the current data
    cleanDiskDirsInEveryVM();

    restoreBackup(2);

    createPersistentRegions(vm0, vm1);

    vm0.invoke(() -> validateData(0, 2, "A", regionName1));
  }

  @Test
  public void testBackupStatusCleanedUpAfterFailureOnOneMember() throws Exception {
    addIgnoredException("Uncaught exception");
    addIgnoredException("Stop processing");

    String exceptionMessage = "Backup in progress";

    vm0.invoke(() -> {
      disconnectFromDS();
      // create an observer that will fail a backup when this member receives a prepare
      DistributionMessageObserver.setInstance(
          createTestHookToThrowIOExceptionBeforeProcessingPrepareBackupRequest(exceptionMessage));
    });

    createPersistentRegions(vm0, vm1);

    vm0.invoke(() -> {
      createData(0, 5, "A", regionName1);
      createData(0, 5, "B", regionName2);
    });

    assertThatThrownBy(() -> vm2.invoke(this::backupMember))
        .hasRootCauseInstanceOf(IOException.class);

    // second backup should succeed because the observer and backup state has been cleared
    BackupStatus status = vm2.invoke(this::backupMember);
    assertThat(status.getBackedUpDiskStores()).hasSize(2);
    assertThat(status.getOfflineDiskStores()).isEmpty();
  }

  /**
   * Make sure we don't report members without persistent data as backed up.
   */
  @Test
  public void testBackupOverflow() {
    vm0.invoke(() -> createRegions(vm0));
    vm1.invoke(() -> createRegionWithOverflow(getDiskStoreFor(vm1)));

    vm0.invoke(() -> {
      createData(0, 5, "A", regionName1);
      createData(0, 5, "B", regionName2);
    });

    BackupStatus status = vm2.invoke(this::backupMember);
    assertThat(status.getBackedUpDiskStores()).hasSize(1);
    assertThat(status.getBackedUpDiskStores().values().iterator().next()).hasSize(2);
    assertThat(status.getOfflineDiskStores()).isEmpty();

    validateBackupComplete();
  }

  @Test
  public void testBackupPRWithOfflineMembers() {
    vm0.invoke(() -> createRegions(vm0));
    vm1.invoke(() -> createRegions(vm1));
    vm2.invoke(() -> createRegions(vm2));

    vm0.invoke(() -> {
      createData(0, 5, "A", regionName1);
      createData(0, 5, "B", regionName2);
    });

    vm1.invoke(() -> getCache().close());

    BackupStatus status = vm3.invoke(this::backupMember);
    assertThat(status.getBackedUpDiskStores()).hasSize(2);
    assertThat(status.getOfflineDiskStores()).hasSize(2);
  }

  /**
   * Test what happens when we restart persistent members while there is an accessor concurrently
   * performing puts.
   */
  @Test
  public void testRecoverySystemWithConcurrentPutter() throws Exception {
    vm1.invoke(() -> createColocatedRegions(vm1));
    vm2.invoke(() -> createColocatedRegions(vm2));

    vm0.invoke(this::createAccessor);

    vm0.invoke(() -> {
      createData(0, NUM_BUCKETS, "a", regionName1);
      createData(0, NUM_BUCKETS, "a", regionName2);
    });

    // backup the system. We use this to get a snapshot of vm1 and vm2
    // when they both are online. Recovering from this backup simulates
    // a simultaneous kill and recovery.
    vm3.invoke(this::backupMember);

    vm1.invoke(() -> getCache().close());
    vm2.invoke(() -> getCache().close());

    cleanDiskDirsInEveryVM();
    restoreBackup(2);

    // in vm0, start doing a bunch of concurrent puts.
    AsyncInvocation<Void> putsInVM0 = vm0.invokeAsync(() -> {
      Region region = getCache().getRegion(regionName1);
      try {
        for (int i = 0;; i++) {
          try {
            region.get(i % NUM_BUCKETS);
          } catch (PartitionOfflineException | PartitionedRegionStorageException expected) {
            // do nothing.
          }
        }
      } catch (CacheClosedException expected) {
        // ok, we're done.
      }
    });

    AsyncInvocation<Void> createRegionsInVM1 = vm1.invokeAsync(() -> createColocatedRegions(vm1));
    AsyncInvocation<Void> createRegionsInVM2 = vm2.invokeAsync(() -> createColocatedRegions(vm2));

    createRegionsInVM1.await();
    createRegionsInVM2.await();

    // close the cache in vm0 to stop the async puts.
    vm0.invoke(() -> getCache().close());

    // make sure we didn't get an exception
    putsInVM0.await();
  }

  private DistributionMessageObserver createTestHookToBackup(
      WhenToInvokeBackup backupInvocationTestHook) {
    switch (backupInvocationTestHook) {
      case BEFORE_SENDING_DESTROYREGIONMESSAGE:
        return createTestHookToBackupBeforeSendingDestroyRegionMessage(
            () -> vm2.invoke(this::backupMember));
      case BEFORE_PROCESSING_REPLYMESSAGE:
        return createTestHookToBackupBeforeProcessingReplyMessage(
            () -> vm2.invoke(this::backupMember));
      default:
        throw new RuntimeException("Invalid backupInvocationTestHook " + backupInvocationTestHook);
    }
  }

  private DistributionMessageObserver createTestHookToBackupBeforeProcessingReplyMessage(
      final Runnable task) {
    return new DistributionMessageObserver() {

      private final AtomicInteger count = new AtomicInteger();
      private volatile int replyId = -0xBAD;
      private volatile boolean done;

      @Override
      public void beforeSendMessage(ClusterDistributionManager dm, DistributionMessage message) {
        // the bucket move will send a destroy region message.
        if (message instanceof DestroyRegionMessage && !done) {
          replyId = message.getProcessorId();
        }
      }

      @Override
      public void beforeProcessMessage(ClusterDistributionManager dm, DistributionMessage message) {
        if (message instanceof ReplyMessage && replyId != -0xBAD
            && replyId == message.getProcessorId() && !done && count.incrementAndGet() == 2) {
          task.run();
          done = true;
        }
      }
    };
  }

  private DistributionMessageObserver createTestHookToBackupBeforeSendingDestroyRegionMessage(
      final Runnable task) {
    return new DistributionMessageObserver() {

      private volatile boolean done;

      @Override
      public void beforeSendMessage(ClusterDistributionManager dm, DistributionMessage message) {
        // the bucket move will send a destroy region message.
        if (message instanceof DestroyRegionMessage && !done) {
          task.run();
          done = true;
        }
      }
    };
  }

  private void cleanDiskDirsInEveryVM() throws IOException {
    FileUtils.deleteDirectory(getDiskDirFor(vm0));
    FileUtils.deleteDirectory(getDiskDirFor(vm1));
    FileUtils.deleteDirectory(getDiskDirFor(vm2));
    FileUtils.deleteDirectory(getDiskDirFor(vm3));
  }

  private DistributionMessageObserver createTestHookToThrowIOExceptionBeforeProcessingPrepareBackupRequest(
      final String exceptionMessage) {
    return new DistributionMessageObserver() {

      @Override
      public void beforeProcessMessage(ClusterDistributionManager dm, DistributionMessage message) {
        if (message instanceof PrepareBackupRequest) {
          DistributionMessageObserver.setInstance(null);
          IOException exception = new IOException(exceptionMessage);
          AdminFailureResponse response = create(message.getSender(), exception);
          response.setMsgId(((PrepareBackupRequest) message).getMsgId());
          dm.putOutgoing(response);
          throw new RuntimeException("Stop processing");
        }
      }
    };
  }

  private DistributionMessageObserver createTestHookToCreateRegionBeforeSendingPrepareBackupRequest() {
    return new DistributionMessageObserver() {

      @Override
      public void beforeSendMessage(ClusterDistributionManager dm, DistributionMessage message) {
        if (message instanceof PrepareBackupRequest) {
          DistributionMessageObserver.setInstance(null);
          logger.info("#### After getting into observer to create regionName2 in vm1");
          getBlackboard().signalGate("createRegion");
          logger.info("#### After signalling create region");
          try {
            getBlackboard().waitForGate("createdRegion", 30, SECONDS);
          } catch (InterruptedException | TimeoutException ex) {
            throw new RuntimeException("never was notified region was created", ex);
          }
        }
      }
    };
  }

  private DistributionMessageObserver createTestHookToWaitForRegionCreationBeforeSendingDLockRequestMessage() {
    return new DistributionMessageObserver() {

      @Override
      public void beforeSendMessage(ClusterDistributionManager dm, DistributionMessage message) {
        if (message instanceof DLockRequestProcessor.DLockRequestMessage) {
          DistributionMessageObserver.setInstance(null);
          try {
            logger.info("#### before waiting for createdRegionAfterRecovery");
            getBlackboard().waitForGate("createdRegionAfterRecovery", 30, SECONDS);
            logger.info("#### after waiting for createdRegionAfterRecovery");
          } catch (InterruptedException | TimeoutException ex) {
            throw new RuntimeException("never was notified region was created", ex);
          }
        }
      }
    };
  }

  private void createPersistentRegions(final VM... vms)
      throws ExecutionException, InterruptedException {
    Set<AsyncInvocation<Void>> createRegionAsyncs = new HashSet<>();
    for (VM vm : vms) {
      createRegionAsyncs.add(vm.invokeAsync(() -> createRegions(vm)));
    }
    for (AsyncInvocation<?> createRegion : createRegionAsyncs) {
      createRegion.await();
    }
  }

  private void validateBackupComplete() {
    Pattern pattern = Pattern.compile(".*INCOMPLETE.*");
    File[] files = backupBaseDir.listFiles((dir, name) -> pattern.matcher(name).matches());

    assertThat(files).isNotNull().hasSize(0);
  }

  private void deleteOldUserFile(final File dir) throws IOException {
    File userDir = new File(dir, "userbackup-");
    FileUtils.deleteDirectory(userDir);
  }

  private long setBackupFiles(final File dir) throws IOException {
    File dir1 = new File(dir, "test1");
    File dir2 = new File(dir1, "test2");
    dir2.mkdirs();

    File textFile = new File(dir2, "my.txt");
    Files.createFile(textFile.toPath());

    List<File> backupList = new ArrayList<>();
    backupList.add(dir2);
    getCache().setBackupFiles(backupList);

    return textFile.lastModified();
  }

  private void verifyUserFileRestored(final File dir, final long expectedLastModified) {
    File dir1 = new File(dir, "test1");
    File dir2 = new File(dir1, "test2");
    File textFile = new File(dir2, "my.txt");

    assertThat(textFile).exists();
    assertThat(textFile.lastModified()).isEqualTo(expectedLastModified);
  }

  private void createRegions(final VM vm) {
    createRegion(regionName1, getUniqueName() + "-1", getDiskStoreFor(vm, 1));
    createRegion(regionName2, getUniqueName() + "-2", getDiskStoreFor(vm, 2));
  }

  private void createRegion(final String regionName, final String diskStoreName,
      final File diskDir) {
    DiskStoreFactory diskStoreFactory = getCache().createDiskStoreFactory();
    diskStoreFactory.setDiskDirs(toArray(diskDir));
    diskStoreFactory.setMaxOplogSize(1);

    PartitionAttributesFactory partitionAttributesFactory = new PartitionAttributesFactory();
    partitionAttributesFactory.setRedundantCopies(0);

    RegionFactory regionFactory = getCache().createRegionFactory(PARTITION_PERSISTENT);
    regionFactory.setDiskStoreName(diskStoreFactory.create(diskStoreName).getName());
    regionFactory.setDiskSynchronous(true);
    regionFactory.setPartitionAttributes(partitionAttributesFactory.create());
    regionFactory.create(regionName);
  }

  private void createReplicatedRegion(final String regionName, final String diskStoreName,
      final File diskDir, boolean sync, boolean delayDiskStoreFlush) {
    DiskStoreFactory diskStoreFactory = getCache().createDiskStoreFactory();
    diskStoreFactory.setDiskDirs(toArray(diskDir));
    diskStoreFactory.setMaxOplogSize(1);
    if (delayDiskStoreFlush) {
      diskStoreFactory.setTimeInterval(Integer.MAX_VALUE);
    }

    RegionFactory regionFactory = getCache().createRegionFactory(REPLICATE_PERSISTENT);
    regionFactory.setDiskStoreName(diskStoreFactory.create(diskStoreName).getName());
    regionFactory.setDiskSynchronous(sync);
    regionFactory.create(regionName);
  }

  private File[] toArray(final File file) {
    return new File[] {file};
  }

  private void createColocatedRegions(final VM vm) {
    DiskStoreFactory diskStoreFactory1 = getCache().createDiskStoreFactory();
    diskStoreFactory1.setDiskDirs(toArray(getDiskStoreFor(vm, 1)));
    diskStoreFactory1.setMaxOplogSize(1);

    DiskStoreFactory diskStoreFactory2 = getCache().createDiskStoreFactory();
    diskStoreFactory2.setDiskDirs(toArray(getDiskStoreFor(vm, 2)));
    diskStoreFactory2.setMaxOplogSize(1);

    PartitionAttributesFactory partitionAttributesFactory1 = new PartitionAttributesFactory();
    partitionAttributesFactory1.setRedundantCopies(0);

    RegionFactory regionFactory1 = getCache().createRegionFactory(PARTITION_PERSISTENT);
    regionFactory1.setPartitionAttributes(partitionAttributesFactory1.create());
    regionFactory1.setDiskStoreName(diskStoreFactory1.create(getUniqueName() + "-1").getName());
    regionFactory1.setDiskSynchronous(true);
    regionFactory1.create(regionName1);

    PartitionAttributesFactory partitionAttributesFactory2 = new PartitionAttributesFactory();
    partitionAttributesFactory2.setColocatedWith(regionName1);
    partitionAttributesFactory2.setRedundantCopies(0);

    RegionFactory regionFactory2 = getCache().createRegionFactory(PARTITION_PERSISTENT);
    regionFactory2.setDiskStoreName(diskStoreFactory2.create(getUniqueName() + "-2").getName());
    regionFactory2.setDiskSynchronous(true);
    regionFactory2.setPartitionAttributes(partitionAttributesFactory2.create());
    regionFactory2.create(regionName2);
  }

  private void createRegionWithOverflow(final File diskDir) {
    DiskStoreFactory diskStoreFactory = getCache().createDiskStoreFactory();
    diskStoreFactory.setDiskDirs(toArray(diskDir));

    EvictionAttributes evictionAttributes = createLIFOEntryAttributes(1, OVERFLOW_TO_DISK);

    RegionFactory regionFactory = getCache().createRegionFactory(REPLICATE);
    regionFactory.setDiskStoreName(diskStoreFactory.create(getUniqueName()).getName());
    regionFactory.setDiskSynchronous(true);
    regionFactory.setEvictionAttributes(evictionAttributes);
    regionFactory.create(regionName3);
  }

  private void createData(final int startKey, final int endKey, final String value,
      final String regionName) {
    Region<Integer, String> region = getCache().getRegion(regionName);

    for (int i = startKey; i < endKey; i++) {
      region.put(i, value);
    }
  }

  private void validateData(final int startKey, final int endKey, final String value,
      final String regionName) {
    Region region = getCache().getRegion(regionName);

    for (int i = startKey; i < endKey; i++) {
      assertThat(region.get(i)).isEqualTo(value);
    }
  }

  private BackupStatus backupMember() {
    return new BackupOperation(getCache().getDistributionManager(), getCache())
        .backupAllMembers(backupBaseDir.toString(), null);
  }

  private void restoreBackup(final int expectedScriptCount)
      throws IOException, InterruptedException {
    Collection<File> restoreScripts =
        listFiles(backupBaseDir, new RegexFileFilter(".*restore.*"), DIRECTORY);

    assertThat(restoreScripts).hasSize(expectedScriptCount);

    for (File script : restoreScripts) {
      executeScript(script);
    }
  }

  private void executeScript(final File script) throws IOException, InterruptedException {
    process = new ProcessBuilder(script.getAbsolutePath()).redirectErrorStream(true).start();

    processReader = new ProcessStreamReader.Builder(process).inputStream(process.getInputStream())
        .inputListener(line -> logger.info("OUTPUT: {}", line))
        .readingMode(getReadingMode()).continueReadingMillis(2 * 1000).build().start();

    assertThat(process.waitFor(5, MINUTES)).isTrue();
    assertThat(process.exitValue()).isEqualTo(0);
  }

  private void createAccessor() {
    PartitionAttributesFactory partitionAttributesFactory1 = new PartitionAttributesFactory();
    partitionAttributesFactory1.setLocalMaxMemory(0);
    partitionAttributesFactory1.setRedundantCopies(0);

    RegionFactory regionFactory1 = getCache().createRegionFactory(PARTITION);
    regionFactory1.setPartitionAttributes(partitionAttributesFactory1.create());
    regionFactory1.create(regionName1);

    PartitionAttributesFactory partitionAttributesFactory2 = new PartitionAttributesFactory();
    partitionAttributesFactory2.setLocalMaxMemory(0);
    partitionAttributesFactory2.setRedundantCopies(0);
    partitionAttributesFactory2.setColocatedWith(regionName1);

    RegionFactory regionFactory2 = getCache().createRegionFactory(PARTITION);
    regionFactory2.setPartitionAttributes(partitionAttributesFactory2.create());
    regionFactory2.create(regionName2);
  }

  private InternalCache getCache() {
    return cacheRule.getOrCreateCache();
  }

  private File getDiskStoreFor(final VM vm) {
    return new File(getDiskDirFor(vm), getUniqueName());
  }

  private File getDiskStoreFor(final VM vm, final int which) {
    return new File(getDiskDirFor(vm), "vm-" + vm.getId() + "-diskstores-" + which);
  }

  private File getDiskDirFor(final VM vm) {
