package io.urmia.job;
/**
*
* Copyright 2014 by Amin Abbaspour
*
* This file is part of Urmia.io
*
* Urmia.io is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Urmia.io is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Urmia.io. If not, see <http://www.gnu.org/licenses/>.
*/
import io.urmia.md.model.job.JobDefinition;
import io.urmia.md.model.job.JobExec;
import io.urmia.md.model.job.JobInput;
import io.urmia.md.model.storage.ObjectName;
import io.urmia.util.UnixPathUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import static io.urmia.job.JobExecutor.Status.*;
import static io.urmia.md.model.job.JobDefinition.Phase.Type.MAP;
import static io.urmia.md.model.job.JobDefinition.Phase.Type.REDUCE;
import static io.urmia.md.model.job.JobInput.END;
public class XargsJobExecutor implements JobExecutor {
private static final Logger log = LoggerFactory.getLogger(XargsJobExecutor.class);
private Status status = INIT;
private Process process = null;
private final String[] commands;
private final String[] envp;
private static final String RUNNER_BASE = "src/main/bash";
private static final String MODULE_NAME = "jail-docker";
private static final String SCRIPT_RUNNER = "job-runner.sh";
private static final String SCRIPT_COLLECT = "collect-outputs.sh";
private final String hostId;
private final String directory;
private final String inputDirectory;
private final String mountPoint;
private final JobDefinition.Phase.Type type;
private RandomAccessFile jobInput = null;
public XargsJobExecutor(String hostId, JobDefinition jobDef, String jobId, String mountPoint,
JobDefinition.Phase.Type type) {
this.directory = UnixPathUtils.appendTrailingSlash(mountPoint) + jobDef.getOwner() + '/' + ObjectName.Namespace.JOBS.path + '/' + jobId;
this.inputDirectory = directory + "/input";
this.mountPoint = UnixPathUtils.trimSlash(mountPoint);
this.hostId = hostId;
this.type = type;
List<String> commands = new LinkedList<String>();
final String parent = UnixPathUtils.normalize(mountPoint + "/" + jobDef.getOwner());
log.info("job {} parent folder: {}", jobId + parent);
commands.add(getScriptPath(SCRIPT_RUNNER));
commands.add("-p");
commands.add(parent);
commands.add("-i");
commands.add(type == MAP ? hostId : hostId + "-reduce");
commands.add("-j");
commands.add(jobId);
List<JobDefinition.Phase> selectedPhases = jobDef.getPhases(type);
log.info("number of phases type: {} in job {} is: {}/{}", type, jobId, selectedPhases.size(), jobDef.getPhases().size());
// todo: for reduce job, add collect-outputs.sh as the first phase.
if(type == REDUCE) {
log.info("job is reduce. adding collect phase first");
commands.add("-c");
commands.add(getScriptPath(SCRIPT_COLLECT));
}
for (JobDefinition.Phase p : selectedPhases) {
log.info("number of exec in phase {} is: {}", p, p.exec.size());
for (JobExec exec : p.exec) {
commands.add("-c");
commands.add(exec.getCommand());
}
}
this.commands = commands.toArray(new String[commands.size()]);
envp = new String[]{
"PATH=/bin:/usr/bin:/usr/local/bin"
};
boolean mkDirResult = new File(directory).mkdirs();
log.info("mkdirs to {} was: {}", directory, mkDirResult);
}
protected String getScriptPath(String SCRIPT_NAME) {
String pwd = System.getProperty("user.dir");
String scriptPath = pwd + File.separatorChar + "source" + File.separatorChar + RUNNER_BASE + File.separatorChar + SCRIPT_NAME;
File f = new File(scriptPath);
if (f.exists()) {
if (f.canExecute())
return scriptPath;
else throw new RuntimeException("script exists but not executable: " + scriptPath);
}
String scriptPath2 = pwd + File.separatorChar + "source" + File.separatorChar + MODULE_NAME + File.separatorChar + RUNNER_BASE + File.separatorChar + SCRIPT_NAME;
File f2 = new File(scriptPath2);
if (f2.exists()) {
if (f2.canExecute())
return scriptPath2;
else throw new RuntimeException("script exists but not executable: " + scriptPath2);
}
String scriptPath3 = pwd + File.separatorChar + ".." + File.separatorChar + MODULE_NAME + File.separatorChar + RUNNER_BASE + File.separatorChar + SCRIPT_NAME;
File f3 = new File(scriptPath3);
if (f3.exists()) {
if (f3.canExecute())
return scriptPath3;
else throw new RuntimeException("script exists but not executable: " + scriptPath3);
}
String scriptPath4 = pwd + File.separatorChar + "../.." + File.separatorChar + MODULE_NAME + File.separatorChar + RUNNER_BASE + File.separatorChar + SCRIPT_NAME;
File f4 = new File(scriptPath4);
if (f4.exists()) {
if (f4.canExecute())
return scriptPath4;
else throw new RuntimeException("script exists but not executable: " + scriptPath4);
}
throw new RuntimeException("script (" + SCRIPT_NAME + ") not found in:\n" + scriptPath + "\n" + scriptPath2 + "\n" + scriptPath3 + "\n" + scriptPath4);
}
@Override
public void addInput(JobInput input) throws IOException {
if (jobInput == null)
throw new RuntimeException("stream not available yet");
if (status != SUCCESSFUL)
throw new IllegalArgumentException("job not in successful mode: " + status);
if (input.isEod()) {
log.info("closing input. file: {}", inputDirectory);
jobInput.close();
jobInput = null;
return;
}
log.info("passing job input to stream: {}", input);
for (String on : input) {
String line = normalizeInput(on);
log.info("flushing input: {}", line);
jobInput.writeBytes(line + "\n");
}
}
/**
* the format of input is different for map and reduce.
* map is ON, reduce is URL (http://ODS:port/ON) for remote and disk path for local
* @return normalized path (disk or remote http)
* /var/tmp/urmia/tck/ods/1[/tck/jobs/e5d8714d-882c-41fe-89e3-788b74bd2bb0/stor/tck/jobs/e5d8714d-882c-41fe-89e3-788b74bd2bb0/2c8a34b9-6bcb-4e8d-982c-35bdb9931ff1]
*/
private String normalizeInput(String on) {
if(type == REDUCE && on.startsWith("http://"))
return on;
return mountPoint + on;
}
@Override
public void terminateInput() throws IOException {
addInput(END);
}
@Override
public String getOutputPath() {
return directory + "/" + hostId;
}
@Override
public int exitCode() throws InterruptedException {
log.info("waiting for '{}' process to exit", type );
return process.waitFor();
}
@Override
public void run() {
log.info("running command: {}", Arrays.toString(commands));
log.info("running env: {}", Arrays.toString(envp));
try {
process = Runtime.getRuntime().exec(commands, envp, new File(directory));
} catch (IOException e) {
log.error("error running job.", e);
status = FAILED;
return;
}
try {
Thread.sleep(100); // todo: why
} catch (InterruptedException ignored) {
}
try {
log.info("opening up jobInput to path: {}", inputDirectory);
File f = new File(inputDirectory);
if(!f.exists())
log.warn("input file does not exist: {}", inputDirectory);
else
log.warn("input file exist: {}", inputDirectory);
jobInput = new RandomAccessFile(inputDirectory, "rwd");
} catch (FileNotFoundException e) {
log.error("error opening input job.", e);
status = FAILED;
process.destroy();
return;
}
status = SUCCESSFUL;
log.info("opened up jobInput to path: {}", inputDirectory);
}
public static class Factory extends JobExecutorFactory {
@Override
public JobExecutor newInstance(String hostId, JobDefinition jobDef, String jobId, String mountPoint,
JobDefinition.Phase.Type type) {
return new XargsJobExecutor(hostId, jobDef, jobId, mountPoint, type);
}
}
}