/**
   * notifyMasterOfReduceTaskCompletion: Get the master's communicator and call reduceTaskCompleted
   * on it
   *
   * @param finished: the reduce task that just completed
   */
  private void notifyMasterOfReduceTaskCompletion(ReduceTask finished) {
    Registry registry;
    String masterNode = communicator.getMasterHostName();

    try {
      registry = LocateRegistry.getRegistry(masterNode, communicator.getREGISTRY_PORT());
      CommunicatorInterface communicator =
          (CommunicatorInterface) registry.lookup("communicator_" + masterNode);
      communicator.reduceTaskCompleted(finished);
    } catch (RemoteException e) {
      e.printStackTrace();
    } catch (NotBoundException e) {
      e.printStackTrace();
    }
  }
  /**
   * notifyMasterOfReduceTaskFailure: Get master's communicator and call reduceTaskFailed on it with
   * the failed reduceTask and the exception
   *
   * @param task
   * @param e
   */
  public void notifyMasterOfReduceTaskFailure(ReduceTask task, Exception e) {
    Registry registry;
    String masterNode = communicator.getMasterHostName();

    try {
      registry = LocateRegistry.getRegistry(masterNode, communicator.getREGISTRY_PORT());
      CommunicatorInterface communicator =
          (CommunicatorInterface) registry.lookup("communicator_" + masterNode);
      communicator.reduceTaskFailed(task, e);
    } catch (RemoteException e1) {
      e1.printStackTrace();
    } catch (NotBoundException e1) {
      e1.printStackTrace();
    }
  }
  /**
   * notifyOfMapperThreadCompletion: We come in here when a mapper task is successfully completed.
   * At this point the mapper has generated intermediate files. In this function we run the
   * partition function on the keys and decide which reducers we need to send the intermediate files
   * to.
   *
   * <p>The sending of intermediate files as soon as a map task completes is better than sending all
   * at the end, especially in situations where the network bandwidth can be a bottleneck.
   *
   * @param mapRunner
   */
  @Override
  public synchronized void notifyOfMapperThreadCompletion(MapRunner mapRunner) {
    MapTask finished = mapRunner.getMapTask();
    JobConfiguration jobConf = finished.getJobConf();
    final int chunkID = finished.getChunkID();
    File folder = new File(jobConf.getJobDir() + "-intermediate/");

    File[] intermediateFiles =
        folder.listFiles(
            new FilenameFilter() {
              @Override
              public boolean accept(File dir, String name) {
                return name.endsWith(Integer.toString(chunkID));
              }
            });

    /** Iterate through all the intermediate files created for a particular chunkID */
    for (File file : intermediateFiles) {
      try {
        /** The intermediate filename is key-chunkID. Extract the key from the filename */
        String[] arr = file.getName().split("-");
        String key = "";

        for (int i = 0; i < arr.length - 1; i++) {
          key += arr[i];
        }

        // create new partitioner object
        Partitioner partitioner = new Partitioner(jobConf.getNumberOfReducers(), key);
        /**
         * run a partition function which returns a reducer to which this intermediate file should
         * be sent to
         */
        int dataNodeIndex = partitioner.partition();
        /**
         * get the list of all the data nodes. Sort them. Use the dataNodeIndex returned by the
         * partition function to get the actual reducer node
         */
        communicator.acquireDataNodesLock();
        List<String> allDataNodes = communicator.getAllDataNodes();
        Collections.sort(allDataNodes);
        String reducerNode = allDataNodes.get(dataNodeIndex);

        /**
         * Get the communicator object of the reducer node, Read the intermediate file into the
         * memory and call receiveIntermediateFile() on the communicator of the reducer node.
         */
        String intermediateFilePath =
            jobConf.getJobDir() + "-intermediate/" + key.toString() + "-" + chunkID;
        Registry registry;
        registry = LocateRegistry.getRegistry(reducerNode, communicator.getREGISTRY_PORT());
        CommunicatorInterface communicator =
            (CommunicatorInterface) registry.lookup("communicator_" + reducerNode);
        communicator.receiveIntermediateFile(
            jobConf, Files.readAllBytes(Paths.get(intermediateFilePath)), file.getName());

        /**
         * At the end of the task completion we send to master a list of reducer nodes to which
         * intermediate files were sent to. Hence store this reducer node to the list
         */
        finished.addReducer(reducerNode);
      } catch (RemoteException e) {
        e.printStackTrace();
      } catch (IOException e) {
        e.printStackTrace();
      } catch (NotBoundException e) {
        e.printStackTrace();
      } catch (IndexOutOfBoundsException e) {
        System.out.println("This job is about to be terminated");
      }
      communicator.releaseDataNodesLock();
    }

    // notify the master that map task has successfully completed
    notifyMasterOfMapTaskCompletion(finished);

    // remove this task from local data structure
    mapLock.lock();
    mapTasks.remove(finished);
    mapLock.unlock();
  }