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

import java.io.File;
import java.io.IOException;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configurable;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.permission.FsPermission;
import org.apache.hadoop.mapred.CleanupQueue.PathDeletionContext;
import org.apache.hadoop.mapred.JvmManager.JvmEnv;
import org.apache.hadoop.mapreduce.MRConfig;
import org.apache.hadoop.util.DiskChecker;
import org.apache.hadoop.util.StringUtils;
import org.apache.hadoop.util.Shell.ShellCommandExecutor;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;

/**
 * Controls initialization, finalization and clean up of tasks, and
 * also the launching and killing of task JVMs.
 * 
 * This class defines the API for initializing, finalizing and cleaning
 * up of tasks, as also the launching and killing task JVMs.
 * Subclasses of this class will implement the logic required for
 * performing the actual actions.
 * 
 * <br/>
 */
@InterfaceAudience.Private
public abstract class TaskController implements Configurable {
  
  private Configuration conf;
  
  public static final Log LOG = LogFactory.getLog(TaskController.class);
  
  public Configuration getConf() {
    return conf;
  }

  // The list of directory paths specified in the variable Configs.LOCAL_DIR
  // This is used to determine which among the list of directories is picked up
  // for storing data for a particular task.
  protected String[] mapredLocalDirs;

  public void setConf(Configuration conf) {
    this.conf = conf;
    mapredLocalDirs = conf.getTrimmedStrings(MRConfig.LOCAL_DIR);
  }

  /**
   * Sets up the permissions of the following directories on all the configured
   * disks:
   * <ul>
   * <li>mapreduce.cluster.local.directories</li>
   * <li>Hadoop log directories</li>
   * </ul>
   */
  public void setup() throws IOException {
    FileSystem localFs = FileSystem.getLocal(conf);

    for (String localDir : this.mapredLocalDirs) {
      // Set up the mapreduce.cluster.local.directories.
      File mapredlocalDir = new File(localDir);
      if (!mapredlocalDir.isDirectory() && !mapredlocalDir.mkdirs()) {
        LOG.warn("Unable to create mapreduce.cluster.local.directory : "
            + mapredlocalDir.getPath());
      } else {
        localFs.setPermission(new Path(mapredlocalDir.getCanonicalPath()),
                              new FsPermission((short)0755));
      }
    }

    // Set up the user log directory
    File taskLog = TaskLog.getUserLogDir();
    if (!taskLog.isDirectory() && !taskLog.mkdirs()) {
      LOG.warn("Unable to create taskLog directory : " + taskLog.getPath());
    } else {
      localFs.setPermission(new Path(taskLog.getCanonicalPath()),
                            new FsPermission((short)0755));
    }
    DiskChecker.checkDir(TaskLog.getUserLogDir());
  }

  /**
   * Take task-controller specific actions to initialize job. This involves
   * setting appropriate permissions to job-files so as to secure the files to
   * be accessible only by the user's tasks.
   * 
   * @throws IOException
   */
  abstract void initializeJob(JobInitializationContext context) throws IOException;

  /**
   * Take task-controller specific actions to initialize the distributed cache
   * file. This involves setting appropriate permissions for these files so as
   * to secure them to be accessible only their owners.
   * 
   * @param context
   * @throws IOException
   */
  public abstract void initializeDistributedCacheFile(DistributedCacheFileContext context)
      throws IOException;

  /**
   * Launch a task JVM
   * 
   * This method defines how a JVM will be launched to run a task. Each
   * task-controller should also do an
   * {@link #initializeTask(TaskControllerContext)} inside this method so as to
   * initialize the task before launching it. This is for reasons of
   * task-controller specific optimizations w.r.t combining initialization and
   * launching of tasks.
   * 
   * @param context the context associated to the task
   */
  abstract void launchTaskJVM(TaskControllerContext context)
                                      throws IOException;

  /**
   * Top level cleanup a task JVM method.
   * <ol>
   * <li>Sends a graceful termiante signal to task JVM to allow subprocesses
   * to cleanup.</li>
   * <li>Sends a forceful kill signal to task JVM, terminating all its
   * sub-processes forcefully.</li>
   * </ol>
   *
   * @param context the task for which kill signal has to be sent.
   */
  final void destroyTaskJVM(TaskControllerContext context) {
    // Send SIGTERM to try to ask for a polite exit.
    terminateTask(context);

    try {
      Thread.sleep(context.sleeptimeBeforeSigkill);
    } catch (InterruptedException e) {
      LOG.warn("Sleep interrupted : " +
          StringUtils.stringifyException(e));
    }

    killTask(context);
  }

  /** Perform initializing actions required before a task can run.
    * 
    * For instance, this method can be used to setup appropriate
    * access permissions for files and directories that will be
    * used by tasks. Tasks use the job cache, log, and distributed cache
    * directories and files as part of their functioning. Typically,
    * these files are shared between the daemon and the tasks
    * themselves. So, a TaskController that is launching tasks
    * as different users can implement this method to setup
    * appropriate ownership and permissions for these directories
    * and files.
    */
  abstract void initializeTask(TaskControllerContext context)
      throws IOException;

  static class TaskExecContext {
    // task being executed
    Task task;
  }
  /**
   * Contains task information required for the task controller.  
   */
  static class TaskControllerContext extends TaskExecContext {
    ShellCommandExecutor shExec;     // the Shell executor executing the JVM for this task.

    // Information used only when this context is used for launching new tasks.
    JvmEnv env;     // the JVM environment for the task.

    // Information used only when this context is used for destroying a task jvm.
    String pid; // process handle of task JVM.
    long sleeptimeBeforeSigkill; // waiting time before sending SIGKILL to task JVM after sending SIGTERM
  }

  /**
   * Contains info related to the path of the file/dir to be deleted. This info
   * is needed by task-controller to build the full path of the file/dir
   */
  static abstract class TaskControllerPathDeletionContext 
  extends PathDeletionContext {
    TaskController taskController;
    String user;

    /**
     * mapredLocalDir is the base dir under which to-be-deleted jobLocalDir, 
     * taskWorkDir or taskAttemptDir exists. fullPath of jobLocalDir, 
     * taskAttemptDir or taskWorkDir is built using mapredLocalDir, jobId, 
     * taskId, etc.
     */
    Path mapredLocalDir;

    public TaskControllerPathDeletionContext(FileSystem fs, Path mapredLocalDir,
                                             TaskController taskController,
                                             String user) {
      super(fs, null);
      this.taskController = taskController;
      this.mapredLocalDir = mapredLocalDir;
      this.user = user;
    }

    @Override
    protected String getPathForCleanup() {
      if (fullPath == null) {
        fullPath = buildPathForDeletion();
      }
      return fullPath;
    }

    /**
     * Return the component of the path under the {@link #mapredLocalDir} to be 
     * cleaned up. Its the responsibility of the class that extends 
     * {@link TaskControllerPathDeletionContext} to provide the correct 
     * component. For example 
     *  - For task related cleanups, either the task-work-dir or task-local-dir
     *    might be returned depending on jvm reuse.
     *  - For job related cleanup, simply the job-local-dir might be returned.
     */
    abstract protected String getPath();
    
    /**
     * Builds the path of taskAttemptDir OR taskWorkDir based on
     * mapredLocalDir, jobId, taskId, etc
     */
    String buildPathForDeletion() {
      return mapredLocalDir.toUri().getPath() + Path.SEPARATOR + getPath();
    }
  }

  /** Contains info related to the path of the file/dir to be deleted. This info
   * is needed by task-controller to build the full path of the task-work-dir or
   * task-local-dir depending on whether the jvm is reused or not.
   */
  static class TaskControllerTaskPathDeletionContext 
  extends TaskControllerPathDeletionContext {
    final Task task;
    final boolean isWorkDir;
    
    public TaskControllerTaskPathDeletionContext(FileSystem fs, 
        Path mapredLocalDir, Task task, boolean isWorkDir, 
        TaskController taskController) {
      super(fs, mapredLocalDir, taskController, task.getUser());
      this.task = task;
      this.isWorkDir = isWorkDir;
    }
    
    /**
     * Returns the taskWorkDir or taskLocalDir based on whether 
     * {@link TaskControllerTaskPathDeletionContext} is configured to delete
     * the workDir.
     */
    @Override
    protected String getPath() {
      String subDir = (isWorkDir) ? TaskTracker.getTaskWorkDir(task.getUser(),
          task.getJobID().toString(), task.getTaskID().toString(),
          task.isTaskCleanupTask())
        : TaskTracker.getLocalTaskDir(task.getUser(),
          task.getJobID().toString(), task.getTaskID().toString(),
          task.isTaskCleanupTask());
      return subDir;
    }

    /**
     * Makes the path(and its subdirectories recursively) fully deletable by
     * setting proper permissions(770) by task-controller
     */
    @Override
    protected void enablePathForCleanup() throws IOException {
      getPathForCleanup();// allow init of fullPath, if not inited already
      if (fs.exists(new Path(fullPath))) {
        taskController.enableTaskForCleanup(this);
      }
    }
  }

  /** Contains info related to the path of the file/dir to be deleted. This info
   * is needed by task-controller to build the full path of the job-local-dir.
   */
  static class TaskControllerJobPathDeletionContext 
  extends TaskControllerPathDeletionContext {
    final JobID jobId;
    
    public TaskControllerJobPathDeletionContext(FileSystem fs, 
        Path mapredLocalDir, JobID id, String user, 
        TaskController taskController) {
      super(fs, mapredLocalDir, taskController, user);
      this.jobId = id;
    }
    
    /**
     * Returns the jobLocalDir of the job to be cleaned up.
     */
    @Override
    protected String getPath() {
      return TaskTracker.getLocalJobDir(user, jobId.toString());
    }
    
    /**
     * Makes the path(and its sub-directories recursively) fully deletable by
     * setting proper permissions(770) by task-controller
     */
    @Override
    protected void enablePathForCleanup() throws IOException {
      getPathForCleanup();// allow init of fullPath, if not inited already
      if (fs.exists(new Path(fullPath))) {
        taskController.enableJobForCleanup(this);
      }
    }
  }
  
  @InterfaceAudience.Private
  @InterfaceStability.Unstable
  public static class InitializationContext {
    public File workDir;
    public String user;
    
    public InitializationContext() {
    }
    
    public InitializationContext(String user, File workDir) {
      this.user = user;
      this.workDir = workDir;
    }
  }
  
  /**
   * This is used for initializing the private localized files in distributed
   * cache. Initialization would involve changing permission, ownership and etc.
   */
  @InterfaceAudience.Private
  @InterfaceStability.Unstable
  public static class DistributedCacheFileContext extends InitializationContext {
    // base directory under which file has been localized
    Path localizedBaseDir;
    // the unique string used to construct the localized path
    String uniqueString;

    public DistributedCacheFileContext(String user, File workDir,
        Path localizedBaseDir, String uniqueString) {
      super(user, workDir);
      this.localizedBaseDir = localizedBaseDir;
      this.uniqueString = uniqueString;
    }

    public Path getLocalizedUniqueDir() {
      return new Path(localizedBaseDir, new Path(TaskTracker
          .getPrivateDistributedCacheDir(user), uniqueString));
    }
  }

  static class JobInitializationContext extends InitializationContext {
    JobID jobid;
  }
  
  static class DebugScriptContext extends TaskExecContext {
    List<String> args;
    File workDir;
    File stdout;
  }

  /**
   * Sends a graceful terminate signal to taskJVM and it sub-processes. 
   *   
   * @param context task context
   */
  abstract void terminateTask(TaskControllerContext context);
  
  /**
   * Sends a KILL signal to forcefully terminate the taskJVM and its
   * sub-processes.
   * 
   * @param context task context
   */
  abstract void killTask(TaskControllerContext context);


  /**
   * Sends a QUIT signal to direct the task JVM (and sub-processes) to
   * dump their stack to stdout.
   *
   * @param context task context.
   */
  abstract void dumpTaskStack(TaskControllerContext context);

  /**
   * Initialize user on this TaskTracer in a TaskController specific manner.
   * 
   * @param context
   * @throws IOException
   */
  public abstract void initializeUser(InitializationContext context)
      throws IOException;
  
  /**
   * Launch the task debug script
   * 
   * @param context
   * @throws IOException
   */
  abstract void runDebugScript(DebugScriptContext context) 
      throws IOException;
  
  /**
   * Enable the task for cleanup by changing permissions of the path
   * @param context   path deletion context
   * @throws IOException
   */
  abstract void enableTaskForCleanup(PathDeletionContext context)
      throws IOException;
  
  /**
   * Enable the job for cleanup by changing permissions of the path
   * @param context   path deletion context
   * @throws IOException
   */
  abstract void enableJobForCleanup(PathDeletionContext context)
    throws IOException;

  /**
   * Returns the local unix user that a given job will run as.
   */
  String getRunAsUser(JobConf conf) {
    return System.getProperty("user.name");
  }
}
