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

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.mapreduce.server.jobtracker.JTConfig;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.AccessControlException;
import org.apache.hadoop.fs.permission.FsAction;
import org.apache.hadoop.fs.permission.FsPermission;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.util.DiskChecker.DiskErrorException;

/**
 * Persists and retrieves the Job info of a job into/from DFS.
 * <p/>
 * If the retain time is zero jobs are not persisted.
 * <p/>
 * A daemon thread cleans up job info files older than the retain time
 * <p/>
 * The retain time can be set with the 'persist.jobstatus.hours'
 * configuration variable (it is in hours).
 */
class CompletedJobStatusStore implements Runnable {
  private boolean active;
  private String jobInfoDir;
  private long retainTime;
  private FileSystem fs;
  private static final String JOB_INFO_STORE_DIR = "/jobtracker/jobsInfo";

  private ACLsManager aclsManager;

  public static final Log LOG =
          LogFactory.getLog(CompletedJobStatusStore.class);

  private static long HOUR = 1000 * 60 * 60;
  private static long SLEEP_TIME = 1 * HOUR;
  final static FsPermission JOB_STATUS_STORE_DIR_PERMISSION = FsPermission
      .createImmutable((short) 0750); // rwxr-x--


  CompletedJobStatusStore(Configuration conf, ACLsManager aclsManager)
      throws IOException {
    active =
      conf.getBoolean(JTConfig.JT_PERSIST_JOBSTATUS, true);

    if (active) {
      retainTime =
        conf.getInt(JTConfig.JT_PERSIST_JOBSTATUS_HOURS, 1) * HOUR;

      jobInfoDir =
        conf.get(JTConfig.JT_PERSIST_JOBSTATUS_DIR, JOB_INFO_STORE_DIR);

      Path path = new Path(jobInfoDir);
      
      // set the fs
      this.fs = path.getFileSystem(conf);
      if (!fs.exists(path)) {
        if (!fs.mkdirs(path, new FsPermission(JOB_STATUS_STORE_DIR_PERMISSION))) {
          throw new IOException(
              "CompletedJobStatusStore mkdirs failed to create "
                  + path.toString());
        }
      } else {
        FileStatus stat = fs.getFileStatus(path);
        FsPermission actual = stat.getPermission();
        if (!stat.isDir())
          throw new DiskErrorException("not a directory: " 
                                   + path.toString());
        FsAction user = actual.getUserAction();
        if (!user.implies(FsAction.READ))
          throw new DiskErrorException("directory is not readable: "
                                   + path.toString());
        if (!user.implies(FsAction.WRITE))
          throw new DiskErrorException("directory is not writable: "
                                   + path.toString());
      }

      if (retainTime == 0) {
        // as retain time is zero, all stored jobstatuses are deleted.
        deleteJobStatusDirs();
      }

      this.aclsManager = aclsManager;

      LOG.info("Completed job store activated/configured with retain-time : " 
               + retainTime + " , job-info-dir : " + jobInfoDir);
    } else {
      LOG.info("Completed job store is inactive");
    }
  }

  /**
   * Indicates if job status persistency is active or not.
   *
   * @return TRUE if active, FALSE otherwise.
   */
  public boolean isActive() {
    return active;
  }

  public void run() {
    if (retainTime > 0) {
      while (true) {
        deleteJobStatusDirs();
        try {
          Thread.sleep(SLEEP_TIME);
        }
        catch (InterruptedException ex) {
          break;
        }
      }
    }
  }

  private void deleteJobStatusDirs() {
    try {
      long currentTime = JobTracker.getClock().getTime();
      //noinspection ForLoopReplaceableByForEach
      for (FileStatus jobInfo : fs.listStatus(new Path(jobInfoDir))) {
        try {
          if ((currentTime - jobInfo.getModificationTime()) > retainTime) {
            LOG.info("Retiring job status from the store [" + jobInfo.getPath() 
                     + "]");
            fs.delete(jobInfo.getPath(), true);
          }
        }
        catch (IOException ie) {
          LOG.warn("Could not do housekeeping for [ " +
                  jobInfo.getPath() + "] job info : " + ie.getMessage(), ie);
        }
      }
    }
    catch (IOException ie) {
      LOG.warn("Could not obtain job info files : " + ie.getMessage(), ie);
    }
  }

  private Path getInfoFilePath(JobID jobId) {
    return new Path(jobInfoDir, jobId + ".info");
  }
  
  /**
   * Persists a job in DFS.
   *
   * @param job the job about to be 'retired'
   */
  public void store(JobInProgress job) {
    if (active && retainTime > 0) {
      JobID jobId = job.getStatus().getJobID();
      Path jobStatusFile = getInfoFilePath(jobId);
      FSDataOutputStream dataOut = null;
      try {
        dataOut = fs.create(jobStatusFile);

        job.getStatus().write(dataOut);

        job.getProfile().write(dataOut);

        job.getCounters().write(dataOut);

        TaskCompletionEvent[] events = 
                job.getTaskCompletionEvents(0, Integer.MAX_VALUE);
        dataOut.writeInt(events.length);
        for (TaskCompletionEvent event : events) {
          event.write(dataOut);
        }

        dataOut.close();
        dataOut = null; // set dataOut to null explicitly so that close in finally will not be executed again.
      } catch (IOException ex) {
        LOG.warn("Could not store [" + jobId + "] job info : " +
                 ex.getMessage(), ex);
        try {
          fs.delete(jobStatusFile, true);
        }
        catch (IOException ex1) {
          //ignore
        }
      } finally {
        IOUtils.cleanup(LOG, dataOut);
      }
    }
  }

  private FSDataInputStream getJobInfoFile(JobID jobId) throws IOException {
    Path jobStatusFile = getInfoFilePath(jobId);
    return (fs.exists(jobStatusFile)) ? fs.open(jobStatusFile) : null;
  }

  private JobStatus readJobStatus(FSDataInputStream dataIn) throws IOException {
    JobStatus jobStatus = new JobStatus();
    jobStatus.readFields(dataIn);
    return jobStatus;
  }

  private JobProfile readJobProfile(FSDataInputStream dataIn)
          throws IOException {
    JobProfile jobProfile = new JobProfile();
    jobProfile.readFields(dataIn);
    return jobProfile;
  }

  private Counters readCounters(FSDataInputStream dataIn) throws IOException {
    Counters counters = new Counters();
    counters.readFields(dataIn);
    return counters;
  }

  private TaskCompletionEvent[] readEvents(FSDataInputStream dataIn,
                                           int offset, int len)
          throws IOException {
    int size = dataIn.readInt();
    if (offset > size) {
      return TaskCompletionEvent.EMPTY_ARRAY;
    }
    if (offset + len > size) {
      len = size - offset;
    }
    TaskCompletionEvent[] events = new TaskCompletionEvent[len];
    for (int i = 0; i < (offset + len); i++) {
      TaskCompletionEvent event = new TaskCompletionEvent();
      event.readFields(dataIn);
      if (i >= offset) {
        events[i - offset] = event;
      }
    }
    return events;
  }

  /**
   * This method retrieves JobStatus information from DFS stored using
   * store method.
   *
   * @param jobId the jobId for which jobStatus is queried
   * @return JobStatus object, null if not able to retrieve
   */
  public JobStatus readJobStatus(JobID jobId) {
    JobStatus jobStatus = null;
    
    if (null == jobId) {
      LOG.warn("Could not read job status for null jobId");
      return null;
    }
    
    if (active) {
      try {
        FSDataInputStream dataIn = getJobInfoFile(jobId);
        if (dataIn != null) {
          jobStatus = readJobStatus(dataIn);
          dataIn.close();
        }
      } catch (IOException ex) {
        LOG.warn("Could not read [" + jobId + "] job status : " + ex, ex);
      }
    }
    return jobStatus;
  }

  /**
   * This method retrieves JobProfile information from DFS stored using
   * store method.
   *
   * @param jobId the jobId for which jobProfile is queried
   * @return JobProfile object, null if not able to retrieve
   */
  public JobProfile readJobProfile(JobID jobId) {
    JobProfile jobProfile = null;
    if (active) {
      try {
        FSDataInputStream dataIn = getJobInfoFile(jobId);
        if (dataIn != null) {
          readJobStatus(dataIn);
          jobProfile = readJobProfile(dataIn);
          dataIn.close();
        }
      } catch (IOException ex) {
        LOG.warn("Could not read [" + jobId + "] job profile : " + ex, ex);
      }
    }
    return jobProfile;
  }

  /**
   * This method retrieves Counters information from file stored using
   * store method.
   *
   * @param jobId the jobId for which Counters is queried
   * @return Counters object, null if not able to retrieve
   * @throws AccessControlException 
   */
  public Counters readCounters(JobID jobId) throws AccessControlException {
    Counters counters = null;
    if (active) {
      try {
        FSDataInputStream dataIn = getJobInfoFile(jobId);
        if (dataIn != null) {
          JobStatus jobStatus = readJobStatus(dataIn);
          JobProfile profile = readJobProfile(dataIn);
          String queue = profile.getQueueName();
          // authorize the user for job view access
          aclsManager.checkAccess(jobStatus,
              UserGroupInformation.getCurrentUser(), queue,
              Operation.VIEW_JOB_COUNTERS);

          counters = readCounters(dataIn);
          dataIn.close();
        }
      } catch (AccessControlException ace) {
        throw ace;
      } catch (IOException ex) {
        LOG.warn("Could not read [" + jobId + "] job counters : " + ex, ex);
      }
    }
    return counters;
  }

  /**
   * This method retrieves TaskCompletionEvents information from DFS stored
   * using store method.
   *
   * @param jobId       the jobId for which TaskCompletionEvents is queried
   * @param fromEventId events offset
   * @param maxEvents   max number of events
   * @return TaskCompletionEvent[], empty array if not able to retrieve
   */
  public TaskCompletionEvent[] readJobTaskCompletionEvents(JobID jobId,
                                                               int fromEventId,
                                                               int maxEvents) {
    TaskCompletionEvent[] events = TaskCompletionEvent.EMPTY_ARRAY;
    if (active) {
      try {
        FSDataInputStream dataIn = getJobInfoFile(jobId);
        if (dataIn != null) {
          readJobStatus(dataIn);
          readJobProfile(dataIn);
          readCounters(dataIn);
          events = readEvents(dataIn, fromEventId, maxEvents);
          dataIn.close();
        }
      } catch (IOException ex) {
        LOG.warn("Could not read [" + jobId + "] job events : " + ex, ex);
      }
    }
    return events;
  }

}
