/**
 * 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.hadoop.hdfs.server.blockmanagement;

import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_NAMENODE_RECONSTRUCTION_PENDING_TIMEOUT_SEC_KEY;
import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_NAMENODE_REDUNDANCY_INTERVAL_SECONDS_KEY;
import static org.apache.hadoop.test.MetricsAsserts.assertCounter;
import static org.apache.hadoop.test.MetricsAsserts.getLongCounter;
import static org.apache.hadoop.test.MetricsAsserts.getMetrics;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.io.IOException;
import java.util.ArrayList;
import java.util.concurrent.TimeoutException;

import com.google.common.base.Supplier;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hdfs.DFSConfigKeys;
import org.apache.hadoop.hdfs.DFSTestUtil;
import org.apache.hadoop.hdfs.DistributedFileSystem;
import org.apache.hadoop.hdfs.HdfsConfiguration;
import org.apache.hadoop.hdfs.MiniDFSCluster;
import org.apache.hadoop.hdfs.protocol.Block;
import org.apache.hadoop.hdfs.protocol.DatanodeInfo;
import org.apache.hadoop.hdfs.protocol.LocatedBlock;
import org.apache.hadoop.hdfs.server.datanode.DataNode;
import org.apache.hadoop.hdfs.server.datanode.DataNodeTestUtils;
import org.apache.hadoop.hdfs.server.namenode.FSNamesystem;
import org.apache.hadoop.hdfs.server.namenode.INodeFile;
import org.apache.hadoop.hdfs.server.namenode.NameNodeAdapter;
import org.apache.hadoop.hdfs.server.protocol.DatanodeRegistration;
import org.apache.hadoop.hdfs.server.protocol.DatanodeStorage;
import org.apache.hadoop.hdfs.server.protocol.ReceivedDeletedBlockInfo;
import org.apache.hadoop.hdfs.server.protocol.ReceivedDeletedBlockInfo.BlockStatus;
import org.apache.hadoop.hdfs.server.protocol.StorageReceivedDeletedBlocks;
import org.apache.hadoop.metrics2.MetricsRecordBuilder;
import org.apache.hadoop.test.GenericTestUtils;
import org.junit.Test;
import org.mockito.Mockito;

/**
 * This class tests the internals of PendingReconstructionBlocks.java, as well
 * as how PendingReconstructionBlocks acts in BlockManager
 */
public class TestPendingReconstruction {
  final static int TIMEOUT = 3;     // 3 seconds
  private static final int DFS_REPLICATION_INTERVAL = 1;
  // Number of datanodes in the cluster
  private static final int DATANODE_COUNT = 5;

  private BlockInfo genBlockInfo(long id, long length, long gs) {
    return new BlockInfoContiguous(new Block(id, length, gs),
        (short) DATANODE_COUNT);
  }

  @Test
  public void testPendingReconstruction() {
    PendingReconstructionBlocks pendingReconstructions;
    pendingReconstructions = new PendingReconstructionBlocks(TIMEOUT * 1000);
    pendingReconstructions.start();
    //
    // Add 10 blocks to pendingReconstruction.
    //
    DatanodeStorageInfo[] storages = DFSTestUtil.createDatanodeStorageInfos(10);
    for (int i = 0; i < storages.length; i++) {
      BlockInfo block = genBlockInfo(i, i, 0);
      DatanodeStorageInfo[] targets = new DatanodeStorageInfo[i];
      System.arraycopy(storages, 0, targets, 0, i);
      pendingReconstructions.increment(block,
          DatanodeStorageInfo.toDatanodeDescriptors(targets));
    }
    assertEquals("Size of pendingReconstruction ",
                 10, pendingReconstructions.size());


    //
    // remove one item
    //
    BlockInfo blk = genBlockInfo(8, 8, 0);
    pendingReconstructions.decrement(blk, storages[7].getDatanodeDescriptor()); // removes one replica
    assertEquals("pendingReconstructions.getNumReplicas ",
                 7, pendingReconstructions.getNumReplicas(blk));

    //
    // insert the same item twice should be counted as once
    //
    pendingReconstructions.increment(blk, storages[0].getDatanodeDescriptor());
    assertEquals("pendingReconstructions.getNumReplicas ",
        7, pendingReconstructions.getNumReplicas(blk));

    for (int i = 0; i < 7; i++) {
      // removes all replicas
      pendingReconstructions.decrement(blk, storages[i].getDatanodeDescriptor());
    }
    assertTrue(pendingReconstructions.size() == 9);
    pendingReconstructions.increment(blk,
        DatanodeStorageInfo.toDatanodeDescriptors(
            DFSTestUtil.createDatanodeStorageInfos(8)));
    assertTrue(pendingReconstructions.size() == 10);

    //
    // verify that the number of replicas returned
    // are sane.
    //
    for (int i = 0; i < 10; i++) {
      BlockInfo block = genBlockInfo(i, i, 0);
      int numReplicas = pendingReconstructions.getNumReplicas(block);
      assertTrue(numReplicas == i);
    }

    //
    // verify that nothing has timed out so far
    //
    assertNull(pendingReconstructions.getTimedOutBlocks());
    assertEquals(0L, pendingReconstructions.getNumTimedOuts());

    //
    // Wait for one second and then insert some more items.
    //
    try {
      Thread.sleep(1000);
    } catch (Exception ignored) {
    }

    for (int i = 10; i < 15; i++) {
      BlockInfo block = genBlockInfo(i, i, 0);
      pendingReconstructions.increment(block,
          DatanodeStorageInfo.toDatanodeDescriptors(
              DFSTestUtil.createDatanodeStorageInfos(i)));
    }
    assertEquals(15, pendingReconstructions.size());
    assertEquals(0L, pendingReconstructions.getNumTimedOuts());

    //
    // Wait for everything to timeout.
    //
    int loop = 0;
    while (pendingReconstructions.size() > 0) {
      try {
        Thread.sleep(1000);
      } catch (Exception e) {
      }
      loop++;
    }
    System.out.println("Had to wait for " + loop +
                       " seconds for the lot to timeout");

    //
    // Verify that everything has timed out.
    //
    assertEquals("Size of pendingReconstructions ", 0, pendingReconstructions.size());
    assertEquals(15L, pendingReconstructions.getNumTimedOuts());
    Block[] timedOut = pendingReconstructions.getTimedOutBlocks();
    assertNotNull(timedOut);
    assertEquals(15, timedOut.length);
    // Verify the number is not reset
    assertEquals(15L, pendingReconstructions.getNumTimedOuts());
    for (Block block : timedOut) {
      assertTrue(block.getBlockId() < 15);
    }
    pendingReconstructions.stop();
  }

/* Test that processpendingReconstructions will use the most recent
 * blockinfo from the blocksmap by placing a larger genstamp into
 * the blocksmap.
 */
  @Test
  public void testProcessPendingReconstructions() throws Exception {
    final Configuration conf = new HdfsConfiguration();
    conf.setLong(
        DFS_NAMENODE_RECONSTRUCTION_PENDING_TIMEOUT_SEC_KEY, TIMEOUT);
    MiniDFSCluster cluster = null;
    Block block;
    BlockInfo blockInfo;
    try {
      cluster =
          new MiniDFSCluster.Builder(conf).numDataNodes(DATANODE_COUNT).build();
      cluster.waitActive();

      FSNamesystem fsn = cluster.getNamesystem();
      BlockManager blkManager = fsn.getBlockManager();

      PendingReconstructionBlocks pendingReconstruction =
          blkManager.pendingReconstruction;
      LowRedundancyBlocks neededReconstruction = blkManager.neededReconstruction;
      BlocksMap blocksMap = blkManager.blocksMap;

      //
      // Add 1 block to pendingReconstructions with GenerationStamp = 0.
      //

      block = new Block(1, 1, 0);
      blockInfo = new BlockInfoContiguous(block, (short) 3);

      pendingReconstruction.increment(blockInfo,
          DatanodeStorageInfo.toDatanodeDescriptors(
              DFSTestUtil.createDatanodeStorageInfos(1)));
      BlockCollection bc = Mockito.mock(BlockCollection.class);
      // Place into blocksmap with GenerationStamp = 1
      blockInfo.setGenerationStamp(1);
      blocksMap.addBlockCollection(blockInfo, bc);
      //Save it for later.
      BlockInfo storedBlock = blockInfo;

      assertEquals("Size of pendingReconstructions ", 1,
          pendingReconstruction.size());

      // Add a second block to pendingReconstructions that has no
      // corresponding entry in blocksmap
      block = new Block(2, 2, 0);
      blockInfo = new BlockInfoContiguous(block, (short) 3);
      pendingReconstruction.increment(blockInfo,
          DatanodeStorageInfo.toDatanodeDescriptors(
              DFSTestUtil.createDatanodeStorageInfos(1)));

      // verify 2 blocks in pendingReconstructions
      assertEquals("Size of pendingReconstructions ", 2,
          pendingReconstruction.size());

      //
      // Wait for everything to timeout.
      //
      while (pendingReconstruction.size() > 0) {
        try {
          Thread.sleep(100);
        } catch (Exception e) {
        }
      }

      //
      // Verify that block moves to neededReconstruction
      //
      while (neededReconstruction.size() == 0) {
        try {
          Thread.sleep(100);
        } catch (Exception e) {
        }
      }

      // Verify that the generation stamp we will try to replicate
      // is now 1
      for (Block b: neededReconstruction) {
        assertEquals("Generation stamp is 1 ", 1,
            b.getGenerationStamp());
      }

      // Verify size of neededReconstruction is exactly 1.
      assertEquals("size of neededReconstruction is 1 ", 1,
          neededReconstruction.size());

      // Verify HDFS-11960
      // Stop the replication/redundancy monitor
      BlockManagerTestUtil.stopRedundancyThread(blkManager);
      pendingReconstruction.clear();
      // Pick a real node
      DatanodeDescriptor desc[] = { blkManager.getDatanodeManager().
          getDatanodes().iterator().next() };

      // Add a stored block to the pendingReconstruction.
      pendingReconstruction.increment(storedBlock, desc);
      assertEquals("Size of pendingReconstructions ", 1,
          pendingReconstruction.size());

      // A received IBR processing calls addBlock(). If the gen stamp in the
      // report is not the same, it should stay in pending.
      fsn.writeLock();
      try {
        // Use a wrong gen stamp.
        blkManager.addBlock(desc[0].getStorageInfos()[0],
            new Block(1, 1, 0), null);
      } finally {
        fsn.writeUnlock();
      }

      // The block should still be pending
      assertEquals("Size of pendingReconstructions ", 1,
          pendingReconstruction.size());

      // A block report with the correct gen stamp should remove the record
      // from the pending queue.
      fsn.writeLock();
      try {
        blkManager.addBlock(desc[0].getStorageInfos()[0],
            new Block(1, 1, 1), null);
      } finally {
        fsn.writeUnlock();
      }

      // The pending queue should be empty.
      assertEquals("Size of pendingReconstructions ", 0,
          pendingReconstruction.size());
    } finally {
      if (cluster != null) {
        cluster.shutdown();
      }
    }
  }

  /**
   * Test if DatanodeProtocol#blockReceivedAndDeleted can correctly update the
   * pending reconstruction. Also make sure the blockReceivedAndDeleted call is
   * idempotent to the pending reconstruction.
   */
  @Test
  public void testBlockReceived() throws Exception {
    final Configuration conf = new HdfsConfiguration();
    conf.setLong(DFSConfigKeys.DFS_BLOCK_SIZE_KEY, 1024);
    MiniDFSCluster cluster = null;
    try {
      cluster = new MiniDFSCluster.Builder(conf).numDataNodes(
          DATANODE_COUNT).build();
      cluster.waitActive();

      DistributedFileSystem hdfs = cluster.getFileSystem();
      FSNamesystem fsn = cluster.getNamesystem();
      BlockManager blkManager = fsn.getBlockManager();

      final String file = "/tmp.txt";
      final Path filePath = new Path(file);
      short replFactor = 1;
      DFSTestUtil.createFile(hdfs, filePath, 1024L, replFactor, 0);

      // temporarily stop the heartbeat
      ArrayList<DataNode> datanodes = cluster.getDataNodes();
      for (int i = 0; i < DATANODE_COUNT; i++) {
        DataNodeTestUtils.setHeartbeatsDisabledForTests(datanodes.get(i), true);
      }

      hdfs.setReplication(filePath, (short) DATANODE_COUNT);
      BlockManagerTestUtil.computeAllPendingWork(blkManager);

      assertEquals(1, blkManager.pendingReconstruction.size());
      INodeFile fileNode = fsn.getFSDirectory().getINode4Write(file).asFile();
      BlockInfo[] blocks = fileNode.getBlocks();
      assertEquals(DATANODE_COUNT - 1,
          blkManager.pendingReconstruction.getNumReplicas(blocks[0]));

      LocatedBlock locatedBlock = hdfs.getClient().getLocatedBlocks(file, 0)
          .get(0);
      DatanodeInfo existingDn = (locatedBlock.getLocations())[0];
      int reportDnNum = 0;
      String poolId = cluster.getNamesystem().getBlockPoolId();
      // let two datanodes (other than the one that already has the data) to
      // report to NN
      for (int i = 0; i < DATANODE_COUNT && reportDnNum < 2; i++) {
        if (!datanodes.get(i).getDatanodeId().equals(existingDn)) {
          DatanodeRegistration dnR = datanodes.get(i).getDNRegistrationForBP(
              poolId);
          StorageReceivedDeletedBlocks[] report = {
              new StorageReceivedDeletedBlocks(
                  new DatanodeStorage("Fake-storage-ID-Ignored"),
              new ReceivedDeletedBlockInfo[] { new ReceivedDeletedBlockInfo(
                  blocks[0], BlockStatus.RECEIVED_BLOCK, "") }) };
          cluster.getNameNodeRpc().blockReceivedAndDeleted(dnR, poolId, report);
          reportDnNum++;
        }
      }
      // IBRs are async, make sure the NN processes all of them.
      cluster.getNamesystem().getBlockManager().flushBlockOps();
      assertEquals(DATANODE_COUNT - 3,
          blkManager.pendingReconstruction.getNumReplicas(blocks[0]));

      // let the same datanodes report again
      for (int i = 0; i < DATANODE_COUNT && reportDnNum < 2; i++) {
        if (!datanodes.get(i).getDatanodeId().equals(existingDn)) {
          DatanodeRegistration dnR = datanodes.get(i).getDNRegistrationForBP(
              poolId);
          StorageReceivedDeletedBlocks[] report =
            { new StorageReceivedDeletedBlocks(
                new DatanodeStorage("Fake-storage-ID-Ignored"),
                new ReceivedDeletedBlockInfo[] {
                  new ReceivedDeletedBlockInfo(
                      blocks[0], BlockStatus.RECEIVED_BLOCK, "")}) };
          cluster.getNameNodeRpc().blockReceivedAndDeleted(dnR, poolId, report);
          reportDnNum++;
        }
      }

      cluster.getNamesystem().getBlockManager().flushBlockOps();
      assertEquals(DATANODE_COUNT - 3,
          blkManager.pendingReconstruction.getNumReplicas(blocks[0]));

      // re-enable heartbeat for the datanode that has data
      for (int i = 0; i < DATANODE_COUNT; i++) {
        DataNodeTestUtils
            .setHeartbeatsDisabledForTests(datanodes.get(i), false);
        DataNodeTestUtils.triggerHeartbeat(datanodes.get(i));
      }

      Thread.sleep(5000);
      assertEquals(0, blkManager.pendingReconstruction.size());
    } finally {
      if (cluster != null) {
        cluster.shutdown();
      }
    }
  }

  /**
   * Test if BlockManager can correctly remove corresponding pending records
   * when a file is deleted
   *
   * @throws Exception
   */
  @Test
  public void testPendingAndInvalidate() throws Exception {
    final Configuration CONF = new HdfsConfiguration();
    CONF.setLong(DFSConfigKeys.DFS_BLOCK_SIZE_KEY, 1024);
    CONF.setLong(DFSConfigKeys.DFS_HEARTBEAT_INTERVAL_KEY,
        DFS_REPLICATION_INTERVAL);
    CONF.setInt(DFS_NAMENODE_REDUNDANCY_INTERVAL_SECONDS_KEY,
        DFS_REPLICATION_INTERVAL);
    MiniDFSCluster cluster = new MiniDFSCluster.Builder(CONF).numDataNodes(
        DATANODE_COUNT).build();
    cluster.waitActive();

    FSNamesystem namesystem = cluster.getNamesystem();
    BlockManager bm = namesystem.getBlockManager();
    DistributedFileSystem fs = cluster.getFileSystem();
    try {
      // 1. create a file
      Path filePath = new Path("/tmp.txt");
      DFSTestUtil.createFile(fs, filePath, 1024, (short) 3, 0L);

      // 2. disable the heartbeats
      for (DataNode dn : cluster.getDataNodes()) {
        DataNodeTestUtils.setHeartbeatsDisabledForTests(dn, true);
      }

      // 3. mark a couple of blocks as corrupt
      LocatedBlock block = NameNodeAdapter.getBlockLocations(
          cluster.getNameNode(), filePath.toString(), 0, 1).get(0);
      cluster.getNamesystem().writeLock();
      try {
        bm.findAndMarkBlockAsCorrupt(block.getBlock(), block.getLocations()[0],
            "STORAGE_ID", "TEST");
        bm.findAndMarkBlockAsCorrupt(block.getBlock(), block.getLocations()[1],
            "STORAGE_ID", "TEST");
        BlockManagerTestUtil.computeAllPendingWork(bm);
        BlockManagerTestUtil.updateState(bm);
        assertEquals(bm.getPendingReconstructionBlocksCount(), 1L);
        BlockInfo storedBlock = bm.getStoredBlock(block.getBlock().getLocalBlock());
        assertEquals(bm.pendingReconstruction.getNumReplicas(storedBlock), 2);
      } finally {
        cluster.getNamesystem().writeUnlock();
      }

      // 4. delete the file
      fs.delete(filePath, true);
      // retry at most 10 times, each time sleep for 1s. Note that 10s is much
      // less than the default pending record timeout (5~10min)
      int retries = 10;
      long pendingNum = bm.getPendingReconstructionBlocksCount();
      while (pendingNum != 0 && retries-- > 0) {
        Thread.sleep(1000);  // let NN do the deletion
        BlockManagerTestUtil.updateState(bm);
        pendingNum = bm.getPendingReconstructionBlocksCount();
      }
      assertEquals(pendingNum, 0L);
    } finally {
      cluster.shutdown();
    }
  }

  /**
   * Test the metric counters of the re-replication process.
   * @throws IOException
   * @throws InterruptedException
   * @throws TimeoutException
   */
  @Test (timeout = 300000)
  public void testReplicationCounter() throws IOException,
      InterruptedException, TimeoutException {
    HdfsConfiguration conf = new HdfsConfiguration();
    conf.setInt(DFS_NAMENODE_REDUNDANCY_INTERVAL_SECONDS_KEY, 1);
    conf.setInt(DFS_NAMENODE_RECONSTRUCTION_PENDING_TIMEOUT_SEC_KEY, 1);
    MiniDFSCluster tmpCluster = new MiniDFSCluster.Builder(conf).numDataNodes(
        DATANODE_COUNT).build();
    tmpCluster.waitActive();
    FSNamesystem fsn = tmpCluster.getNamesystem(0);
    fsn.writeLock();

    try {
      BlockManager bm = fsn.getBlockManager();
      BlocksMap blocksMap = bm.blocksMap;

      // create three blockInfo below, blockInfo0 will success, blockInfo1 will
      // time out, blockInfo2 will fail the replication.
      BlockCollection bc0 = Mockito.mock(BlockCollection.class);
      BlockInfo blockInfo0 = new BlockInfoContiguous((short) 3);
      blockInfo0.setBlockId(0);

      BlockCollection bc1 = Mockito.mock(BlockCollection.class);
      BlockInfo blockInfo1 = new BlockInfoContiguous((short) 3);
      blockInfo1.setBlockId(1);

      BlockCollection bc2 = Mockito.mock(BlockCollection.class);
      BlockInfo blockInfo2 = new BlockInfoContiguous((short) 3);
      blockInfo2.setBlockId(2);

      blocksMap.addBlockCollection(blockInfo0, bc0);
      blocksMap.addBlockCollection(blockInfo1, bc1);
      blocksMap.addBlockCollection(blockInfo2, bc2);

      PendingReconstructionBlocks pending = bm.pendingReconstruction;

      MetricsRecordBuilder rb = getMetrics("NameNodeActivity");
      assertCounter("SuccessfulReReplications", 0L, rb);
      assertCounter("NumTimesReReplicationNotScheduled", 0L, rb);
      assertCounter("TimeoutReReplications", 0L, rb);

      // add block0 and block1 to pending queue.
      pending.increment(blockInfo0);
      pending.increment(blockInfo1);

      // call addBlock on block0 will make it successfully replicated.
      // not calling addBlock on block1 will make it timeout later.
      DatanodeStorageInfo[] storageInfos =
          DFSTestUtil.createDatanodeStorageInfos(1);
      bm.addBlock(storageInfos[0], blockInfo0, null);

      // call schedule replication on blockInfo2 will fail the re-replication.
      // because there is no source data to replicate from.
      bm.scheduleReconstruction(blockInfo2, 0);

      GenericTestUtils.waitFor(new Supplier<Boolean>() {
        @Override
        public Boolean get() {
          MetricsRecordBuilder rb = getMetrics("NameNodeActivity");
          return getLongCounter("SuccessfulReReplications", rb) == 1 &&
              getLongCounter("NumTimesReReplicationNotScheduled", rb) == 1 &&
              getLongCounter("TimeoutReReplications", rb) == 1;
        }
      }, 100, 60000);
    } finally {
      tmpCluster.shutdown();
      fsn.writeUnlock();
    }
  }
}
