/**
* 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.mapreduce.test.system;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.util.HashMap;
import java.util.StringTokenizer;
import junit.framework.Assert;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.mapred.JobClient;
import org.apache.hadoop.mapred.JobConf;
import org.apache.hadoop.mapred.JobStatus;
import org.apache.hadoop.mapred.JobTracker;
import org.apache.hadoop.mapred.RunningJob;
import org.apache.hadoop.mapred.TaskStatus;
import org.apache.hadoop.mapred.UtilsForTests;
import org.apache.hadoop.mapreduce.JobID;
import org.apache.hadoop.test.system.process.RemoteProcess;
/**
* JobTracker client for system tests.
*/
public class JTClient extends MRDaemonClient<JTProtocol> {
static final Log LOG = LogFactory.getLog(JTClient.class);
private JobClient client;
/**
* Create JobTracker client to talk to {@link JobTracker} specified in the
* configuration. <br/>
*
* @param conf
* configuration used to create a client.
* @param daemon
* the process management instance for the {@link JobTracker}
* @throws IOException
*/
public JTClient(Configuration conf, RemoteProcess daemon) throws
IOException {
super(conf, daemon);
}
@Override
public synchronized void connect() throws IOException {
if (isConnected()) {
return;
}
client = new JobClient(new JobConf(getConf()));
setConnected(true);
}
@Override
public synchronized void disconnect() throws IOException {
client.close();
}
@Override
public synchronized JTProtocol getProxy() {
return (JTProtocol) client.getProtocol();
}
/**
* Gets the {@link JobClient} which can be used for job submission. JobClient
* which is returned would not contain the decorated API's. To be used for
* submitting of the job.
*
* @return client handle to the JobTracker
*/
public JobClient getClient() {
return client;
}
/**
* Gets the configuration which the JobTracker is currently running.<br/>
*
* @return configuration of JobTracker.
*
* @throws IOException
*/
public Configuration getJobTrackerConfig() throws IOException {
return getProxy().getDaemonConf();
}
/**
* Kills the job. <br/>
* @param id of the job to be killed.
* @throws IOException
*/
public void killJob(JobID id) throws IOException {
getClient().killJob(id);
}
/**
* Verification API to check running jobs and running job states.
* users have to ensure that their jobs remain running state while
* verification is called. <br/>
*
* @param id
* of the job to be verified.
*
* @throws Exception
*/
public void verifyRunningJob(JobID jobId) throws Exception {
}
private JobInfo getJobInfo(JobID jobId) throws IOException {
JobInfo info = getProxy().getJobInfo(jobId);
if (info == null && !getProxy().isJobRetired(jobId)) {
Assert.fail("Job id : " + jobId + " has never been submitted to JT");
}
return info;
}
/**
* Verification API to wait till job retires and verify all the retired state
* is correct.
* <br/>
* @param conf of the job used for completion
* @return job handle
* @throws Exception
*/
public RunningJob submitAndVerifyJob(Configuration conf) throws Exception {
JobConf jconf = new JobConf(conf);
RunningJob rJob = getClient().submitJob(jconf);
JobID jobId = rJob.getID();
verifyRunningJob(jobId);
verifyCompletedJob(jobId);
return rJob;
}
/**
* Verification API to check if the job completion state is correct. <br/>
*
* @param id id of the job to be verified.
*/
public void verifyCompletedJob(JobID id) throws Exception{
RunningJob rJob = getClient().getJob(
org.apache.hadoop.mapred.JobID.downgrade(id));
while(!rJob.isComplete()) {
LOG.info("waiting for job :" + id + " to retire");
Thread.sleep(1000);
rJob = getClient().getJob(
org.apache.hadoop.mapred.JobID.downgrade(id));
}
verifyJobDetails(id);
verifyJobHistory(id);
}
/**
* Verification API to check if the job details are semantically correct.<br/>
*
* @param jobId
* jobID of the job
* @param jconf
* configuration object of the job
* @return true if all the job verifications are verified to be true
* @throws Exception
*/
public void verifyJobDetails(JobID jobId) throws Exception {
// wait till the setup is launched and finished.
JobInfo jobInfo = getJobInfo(jobId);
if(jobInfo == null){
return;
}
LOG.info("waiting for the setup to be finished");
while (!jobInfo.isSetupFinished()) {
Thread.sleep(2000);
jobInfo = getJobInfo(jobId);
if(jobInfo == null) {
break;
}
}
// verify job id.
assertTrue(jobId.toString().startsWith("job_"));
LOG.info("verified job id and is : " + jobId.toString());
// verify the number of map/reduce tasks.
verifyNumTasks(jobId);
// should verify job progress.
verifyJobProgress(jobId);
jobInfo = getJobInfo(jobId);
if(jobInfo == null) {
return;
}
if (jobInfo.getStatus().getRunState() == JobStatus.SUCCEEDED) {
// verify if map/reduce progress reached 1.
jobInfo = getJobInfo(jobId);
if (jobInfo == null) {
return;
}
assertEquals(1.0, jobInfo.getStatus().mapProgress(), 0.001);
assertEquals(1.0, jobInfo.getStatus().reduceProgress(), 0.001);
// verify successful finish of tasks.
verifyAllTasksSuccess(jobId);
}
if (jobInfo.getStatus().isJobComplete()) {
// verify if the cleanup is launched.
jobInfo = getJobInfo(jobId);
if (jobInfo == null) {
return;
}
assertTrue(jobInfo.isCleanupLaunched());
LOG.info("Verified launching of cleanup");
}
}
public void verifyAllTasksSuccess(JobID jobId) throws IOException {
JobInfo jobInfo = getJobInfo(jobId);
if (jobInfo == null) {
return;
}
TaskInfo[] taskInfos = getProxy().getTaskInfo(jobId);
if(taskInfos.length == 0 && getProxy().isJobRetired(jobId)) {
LOG.info("Job has been retired from JT memory : " + jobId);
return;
}
for (TaskInfo taskInfo : taskInfos) {
TaskStatus[] taskStatus = taskInfo.getTaskStatus();
if (taskStatus != null && taskStatus.length > 0) {
int i;
for (i = 0; i < taskStatus.length; i++) {
if (TaskStatus.State.SUCCEEDED.equals(taskStatus[i].getRunState())) {
break;
}
}
assertFalse(i == taskStatus.length);
}
}
LOG.info("verified that none of the tasks failed.");
}
public void verifyJobProgress(JobID jobId) throws IOException {
JobInfo jobInfo;
jobInfo = getJobInfo(jobId);
if (jobInfo == null) {
return;
}
assertTrue(jobInfo.getStatus().mapProgress() >= 0 && jobInfo.getStatus()
.mapProgress() <= 1);
LOG.info("verified map progress and is "
+ jobInfo.getStatus().mapProgress());
assertTrue(jobInfo.getStatus().reduceProgress() >= 0 && jobInfo.getStatus()
.reduceProgress() <= 1);
LOG.info("verified reduce progress and is "
+ jobInfo.getStatus().reduceProgress());
}
public void verifyNumTasks(JobID jobId) throws IOException {
JobInfo jobInfo;
jobInfo = getJobInfo(jobId);
if (jobInfo == null) {
return;
}
assertEquals(jobInfo.numMaps(), (jobInfo.runningMaps()
+ jobInfo.waitingMaps() + jobInfo.finishedMaps()));
LOG.info("verified number of map tasks and is " + jobInfo.numMaps());
assertEquals(jobInfo.numReduces(), (jobInfo.runningReduces()
+ jobInfo.waitingReduces() + jobInfo.finishedReduces()));
LOG.info("verified number of reduce tasks and is "
+ jobInfo.numReduces());
}
/**
* Verification API to check if the job history file is semantically correct.
* <br/>
*
*
* @param id
* of the job to be verified.
* @throws IOException
*/
public void verifyJobHistory(JobID jobId) throws IOException {
JobInfo info = getJobInfo(jobId);
String url ="";
if(info == null) {
LOG.info("Job has been retired from JT memory : " + jobId);
url = getProxy().getJobHistoryLocationForRetiredJob(jobId);
} else {
url = info.getHistoryUrl();
}
Path p = new Path(url);
if (p.toUri().getScheme().equals("file:/")) {
FileStatus st = getFileStatus(url, true);
Assert.assertNotNull("Job History file for " + jobId + " not present " +
"when job is completed" , st);
} else {
FileStatus st = getFileStatus(url, false);
Assert.assertNotNull("Job History file for " + jobId + " not present " +
"when job is completed" , st);
}
LOG.info("Verified the job history for the jobId : " + jobId);
}
/**
* The method provides the information on the job has stopped or not
* @return indicates true if the job has stopped false otherwise.
* @param job id has the information of the running job.
* @throw IOException is thrown if the job info cannot be fetched.
*/
public boolean isJobStopped(JobID id) throws IOException{
int counter = 0;
JobInfo jInfo = getProxy().getJobInfo(id);
if(jInfo != null ) {
while (counter < 60) {
if (jInfo.getStatus().isJobComplete()) {
break;
}
UtilsForTests.waitFor(1000);
jInfo = getProxy().getJobInfo(id);
counter ++;
}
}
return (counter != 60)? true : false;
}
/**
* It uses to check whether job is started or not.
* @param id job id
* @return true if job is running.
* @throws IOException if an I/O error occurs.
*/
public boolean isJobStarted(JobID id) throws IOException {
JobInfo jInfo = getJobInfo(id);
int counter = 0;
while (counter < 60) {
if (jInfo.getStatus().getRunState() == JobStatus.RUNNING) {
break;
} else {
UtilsForTests.waitFor(1000);
jInfo = getJobInfo(jInfo.getID());
Assert.assertNotNull("Job information is null",jInfo);
}
counter++;
}
return (counter != 60)? true : false ;
}
/**
* It uses to check whether task is started or not.
* @param taskInfo task information
* @return true if task is running.
* @throws IOException if an I/O error occurs.
*/
public boolean isTaskStarted(TaskInfo taskInfo) throws IOException {
JTProtocol wovenClient = getProxy();
int counter = 0;
while (counter < 60) {
if (taskInfo.getTaskStatus().length > 0) {
if (taskInfo.getTaskStatus()[0].getRunState() ==
TaskStatus.State.RUNNING) {
break;
}
}
UtilsForTests.waitFor(1000);
taskInfo = wovenClient.getTaskInfo(taskInfo.getTaskID());
counter++;
}
return (counter != 60)? true : false;
}
/**
* Get the jobtracker log files as pattern.
* @return String - Jobtracker log file pattern.
* @throws IOException - if I/O error occurs.
*/
public String getJobTrackerLogFilePattern() throws IOException {
return getProxy().getFilePattern();
}
/**
* It uses to get the job summary details of given job id. .
* @param jobID - job id
* @return HashMap -the job summary details as map.
* @throws IOException if any I/O error occurs.
*/
public HashMap<String,String> getJobSummary(JobID jobID)
throws IOException {
String output = getProxy().getJobSummaryInfo(jobID);
StringTokenizer strToken = new StringTokenizer(output,",");
HashMap<String,String> mapcollect = new HashMap<String,String>();
while(strToken.hasMoreTokens()) {
String keypair = strToken.nextToken();
mapcollect.put(keypair.split("=")[0], keypair.split("=")[1]);
}
return mapcollect;
}
/**
* Concrete implementation of abstract super class method
* @param attributeName name of the attribute to be retrieved
* @return Object value of the given attribute
* @throws IOException is thrown in case of communication errors
*/
@Override
public Object getDaemonAttribute (String attributeName) throws IOException {
return getJmxAttribute("JobTracker", "JobTrackerInfo", attributeName);
}
}