/*
Copyright (C) 2006 EBI
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the itmplied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.biomart.runner.controller;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import javax.mail.MessagingException;
import org.biomart.common.resources.Log;
import org.biomart.common.resources.Resources;
import org.biomart.common.resources.Settings;
import org.biomart.common.utils.FileUtils;
import org.biomart.common.utils.SendMail;
import org.biomart.runner.controller.JobThreadManager.JobThreadManagerListener;
import org.biomart.runner.exceptions.JobException;
import org.biomart.runner.model.JobList;
import org.biomart.runner.model.JobPlan;
import org.biomart.runner.model.JobStatus;
import org.biomart.runner.model.JobPlan.JobPlanAction;
import org.biomart.runner.model.JobPlan.JobPlanSection;
/**
* Tools for managing job statuses, and manipulating job objects.
*
* @author Richard Holland <holland@ebi.ac.uk>
* @version $Revision: 1.23 $, $Date: 2007-12-20 15:38:46 $, modified by
* $Author: rh4 $
* @since 0.6
*/
public class JobHandler {
private static final Object planDirLock = "__PLANDIR__LOCK__";
private static final int SAVE_INTERVAL = 5; // Seconds.
private static long nextJob = System.currentTimeMillis();
private static final File jobsDir = new File(
Settings.getStorageDirectory(), "jobs");
private static JobList jobList = null;
private static final Map jobManagers = Collections
.synchronizedMap(new HashMap());
private static boolean jobListIsDirty = false;
private static final Timer t = new Timer();
static {
if (!JobHandler.jobsDir.exists())
JobHandler.jobsDir.mkdir();
t.schedule(new TimerTask() {
public void run() {
if (JobHandler.jobListIsDirty)
synchronized (JobHandler.planDirLock) {
Log.debug("Saving list");
// Save (overwrite) file with plan.
FileOutputStream fos = null;
try {
final File jobListFile = JobHandler
.getJobListFile();
fos = new FileOutputStream(jobListFile);
final ObjectOutputStream oos = new ObjectOutputStream(
fos);
oos.writeObject(JobHandler.jobList);
oos.flush();
fos.flush();
} catch (final IOException e) {
// What else to do with it?
Log.error(e);
} finally {
if (fos != null)
try {
fos.close();
} catch (final IOException e) {
// What else to do with it?
Log.error(e);
}
}
}
JobHandler.jobListIsDirty = false;
}
}, 0, JobHandler.SAVE_INTERVAL * 1000);
}
/**
* Request a new job ID. Don't define the job, just request an ID for one
* that could be defined in future.
*
* @return a unique job ID.
* @throws JobException
* if anything went wrong.
*/
public static String nextJobId() throws JobException {
synchronized (JobHandler.planDirLock) {
return "" + JobHandler.nextJob++;
}
}
/**
* Locate any jobs that say they're running and change them to say they're
* STOPPED.
*
* @return the number of jobs changed.
* @throws JobException
* if anything went wrong.
*/
public static int stopCrashedJobs() throws JobException {
final Set stoppedJobs = new HashSet();
// Update job summaries.
final JobList jobList = JobHandler.getJobList();
for (final Iterator i = jobList.getAllJobs().iterator(); i.hasNext();) {
final JobPlan plan = (JobPlan) i.next();
final List sections = new ArrayList();
sections.add(plan.getRoot());
for (int j = 0; j < sections.size(); j++) {
final JobPlanSection section = (JobPlanSection) sections.get(j);
if (!section.getStatus().equals(JobStatus.RUNNING))
continue;
sections.addAll(section.getSubSections());
for (final Iterator l = JobHandler.getActions(plan.getJobId(),
section.getIdentifier()).values().iterator(); l
.hasNext();) {
final JobPlanAction action = (JobPlanAction) l.next();
if (action.getStatus().equals(JobStatus.RUNNING)) {
JobHandler.setStatus(plan.getJobId(), action
.getIdentifier(), JobStatus.STOPPED, null);
stoppedJobs.add(plan);
}
}
}
}
// Send an email when find stopped jobs.
for (final Iterator i = stoppedJobs.iterator(); i.hasNext();) {
final JobPlan plan = (JobPlan) i.next();
final String contactEmail = plan.getContactEmailAddress();
if (contactEmail != null && !"".equals(contactEmail.trim()))
try {
SendMail.sendSMTPMail(new String[] { contactEmail },
Resources.get("jobStoppedSubject", ""
+ plan.getJobId()), "");
} catch (final MessagingException e) {
// We don't really care.
Log.error(e);
}
}
return stoppedJobs.size();
}
/**
* Flag that a job is about to start receiving commands.
*
* @param jobId
* the job ID.
* @param targetSchema
* the schema we will be constructing.
* @param jdbcDriverClassName
* the JDBC driver classname for the server the job will run
* against.
* @param jdbcURL
* the JDBC URL of the server the job will run against.
* @param jdbcUsername
* the JDBC username for the server the job will run against.
* @param jdbcPassword
* the JDBC password for the server the job will run against.
* @throws JobException
* if anything went wrong.
*/
public static void beginJob(final String jobId, final String targetSchema,
final String jdbcDriverClassName, final String jdbcURL,
final String jdbcUsername, final String jdbcPassword)
throws JobException {
try {
// Create a job list entry and a job plan.
final JobPlan jobPlan = JobHandler.getJobList().getJobPlan(jobId);
// Set the JDBC stuff.
jobPlan.setTargetSchema(targetSchema);
jobPlan.setJDBCDriverClassName(jdbcDriverClassName);
jobPlan.setJDBCURL(jdbcURL);
jobPlan.setJDBCUsername(jdbcUsername);
jobPlan.setJDBCPassword(jdbcPassword);
// Save stuff.
JobHandler.saveJobList();
} catch (final IOException e) {
throw new JobException(e);
}
}
/**
* Flag that a job is about to end receiving commands.
*
* @param jobId
* the job ID.
* @throws JobException
* if anything went wrong.
*/
public static void endJob(final String jobId) throws JobException {
// We don't really care.
}
/**
* Change the status of a job plan action object.
*
* @param jobId
* the job.
* @param identifiers
* the identifiers.
* @param status
* the new status.
* @param message
* the new message.
* @throws JobException
* if it can't.
*/
public static void setStatus(final String jobId,
final Collection identifiers, final JobStatus status,
final String message) throws JobException {
JobHandler.setStatus(jobId, identifiers, status, message, true);
}
/**
* Change the status of a job plan action object.
*
* @param jobId
* the job.
* @param identifier
* the job action.
* @param status
* the new status.
* @param message
* any message to assign to the action. Use <tt>null</tt> for
* no message.
* @throws JobException
* if it can't.
*/
public static void setStatus(final String jobId, final String identifier,
final JobStatus status, final String message) throws JobException {
JobHandler.setStatus(jobId, Collections.singletonList(identifier),
status, message);
}
private static void setStatus(final String jobId,
final Collection identifiers, final JobStatus status,
final String message, final boolean saveList) throws JobException {
Map actions = null;
String previousSectionId = null;
boolean sectionHasUpdatedActions = false;
for (final Iterator i = identifiers.iterator(); i.hasNext();) {
final String identifier = (String) i.next();
String sectionId = null;
String actionId = null;
// Convert identifier into either a section
// or an action.
if (identifier.indexOf('#') < 0)
// Convert to a section and process all actions.
sectionId = identifier;
else {
// Locate section and process individual action.
final String[] parts = identifier.split("#");
sectionId = parts[0];
actionId = parts[1];
}
if (!sectionId.equals(previousSectionId)) {
if (previousSectionId != null && sectionHasUpdatedActions)
JobHandler.setActions(jobId, previousSectionId, actions,
false);
sectionHasUpdatedActions = false;
actions = JobHandler.getActions(jobId, sectionId);
}
previousSectionId = sectionId;
if (actionId != null) {
sectionHasUpdatedActions = true;
final JobPlanAction action = (JobPlanAction) actions
.get(identifier);
// Set the status.
action.setStatus(status, actions.values());
action.setMessage(message);
// Update timings.
if (status.equals(JobStatus.RUNNING)) {
action.setStarted(new Date(), actions.values());
action.setEnded(null, actions.values());
} else if (status.equals(JobStatus.FAILED)
|| status.equals(JobStatus.COMPLETED))
action.setEnded(new Date(), actions.values());
else {
action.setStarted(null, actions.values());
action.setEnded(null, actions.values());
}
} else {
// Find all subsections and recurse on them.
final Collection newIdentifiers = new ArrayList();
final JobPlanSection section = JobHandler.getJobPlan(jobId)
.getJobPlanSection(sectionId);
if (section.getActionCount() > 0)
// Recurse on direct actions.
newIdentifiers.addAll(actions.keySet());
// Recurse on subsections too.
for (final Iterator j = section.getSubSections().iterator(); j
.hasNext();)
newIdentifiers.add(((JobPlanSection) j.next())
.getIdentifier());
// Do the recursive call.
JobHandler.setStatus(jobId, newIdentifiers, status, message,
false);
}
}
if (previousSectionId != null && sectionHasUpdatedActions)
JobHandler.setActions(jobId, previousSectionId, actions, false);
// Now save the list if required.
if (saveList)
try {
JobHandler.saveJobList();
} catch (final IOException e) {
throw new JobException(e);
}
}
/**
* Get the numbered section from the job def.
*
* @param jobId
* the job.
* @param sectionId
* the section number.
* @return the section.
* @throws JobException
*/
public static JobPlanSection getSection(final String jobId,
final String sectionId) throws JobException {
return JobHandler.getJobPlan(jobId).getJobPlanSection(sectionId);
}
/**
* Do the funky empty table thang.
*
* @param jobId
* the job.
* @throws JobException
*/
public static void makeEmptyTableJob(final String jobId)
throws JobException {
try {
JobHandler.getJobPlan(jobId).makeEmptyTableJob();
} catch (final SQLException e) {
throw new JobException(e);
}
}
/**
* Queue some set of sections+actions.
*
* @param jobId
* the job ID.
* @param identifiers
* the selected node identifiers.
* @throws JobException
* if it cannot do it.
*/
public static void queue(final String jobId, final Collection identifiers)
throws JobException {
JobHandler.setStatus(jobId, identifiers, JobStatus.QUEUED, null);
}
/**
* Unqueue some set of sections+actions.
*
* @param jobId
* the job ID.
* @param identifiers
* the selected node identifiers.
* @throws JobException
* if it cannot do it.
*/
public static void unqueue(final String jobId, final Collection identifiers)
throws JobException {
JobHandler.setStatus(jobId, identifiers, JobStatus.NOT_QUEUED, null);
}
/**
* Update an action.
*
* @param jobId
* the job ID.
* @param sectionId
* the section ID.
* @param actionId
* the action ID.
* @param action
* the new action.
* @throws JobException
* if it cannot do it.
*/
public static void updateAction(final String jobId, final String sectionId,
final String actionId, final String action) throws JobException {
final Map actions = JobHandler.getActions(jobId, sectionId);
final JobPlanAction jpAction = (JobPlanAction) actions.get(actionId);
jpAction.setAction(action);
jpAction.setStatus(JobStatus.NOT_QUEUED, actions.values());
JobHandler.setActions(jobId, sectionId, actions, true);
}
/**
* Flag that an action is to be set to the job.
*
* @param jobId
* the job ID.
* @param sectionPath
* the section this applies to.
* @param actions
* the actions to add.
* @throws JobException
* if anything went wrong.
*/
public static void setActions(final String jobId,
final String[] sectionPath, final Collection actions)
throws JobException {
final JobPlan jobPlan = JobHandler.getJobPlan(jobId);
// Add the action to the job.
jobPlan.setActionCount(sectionPath, actions.size());
// Get the section ID.
JobPlanSection section = jobPlan.getRoot();
for (int i = 0; i < sectionPath.length; i++)
section = section.getSubSection(sectionPath[i]);
// Convert each action into an action object and create
// a map.
final Map actionMap = new LinkedHashMap();
for (final Iterator i = actions.iterator(); i.hasNext();) {
final JobPlanAction action = new JobPlanAction(jobId, (String) i
.next(), section.getIdentifier());
actionMap.put(action.getIdentifier(), action);
}
// Do the work.
JobHandler.setActions(jobId, section.getIdentifier(), actionMap, false);
// Update the status to QUEUED (for external requests only).
JobHandler.setStatus(jobId, actionMap.keySet(), JobStatus.QUEUED, null);
}
private static void setActions(final String jobId, final String sectionId,
final Map actionMap, final boolean saveList) throws JobException {
Log.debug("Saving actions for job " + jobId + " section " + sectionId);
FileOutputStream fos = null;
try {
// Write the actions to a file.
fos = new FileOutputStream(JobHandler.getActionsFile(jobId,
sectionId));
final ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(actionMap);
oos.flush();
fos.flush();
// Save the list.
if (saveList)
JobHandler.saveJobList();
} catch (final IOException e) {
throw new JobException(e);
} finally {
if (fos != null)
try {
fos.close();
} catch (final IOException ie) {
// Ignore.
}
}
}
/**
* Get the actions for the specified job section. Keys are identifiers,
* values are actions.
*
* @param jobId
* the job ID.
* @param sectionId
* the section ID.
* @return the actions.
* @throws JobException
* if they could not be read.
*/
public static Map getActions(final String jobId, final String sectionId)
throws JobException {
Log.debug("Loading actions for job " + jobId + " section " + sectionId);
// Load actions from file.
synchronized (JobHandler.planDirLock) {
// Load existing job plan.
FileInputStream fis = null;
Map actions = null;
try {
final File actionsFile = JobHandler.getActionsFile(jobId,
sectionId);
// Doesn't exist? Return a default empty list.
if (!actionsFile.exists())
actions = Collections.EMPTY_MAP;
else {
fis = new FileInputStream(actionsFile);
final ObjectInputStream ois = new ObjectInputStream(fis);
actions = (Map) ois.readObject();
}
} catch (final Throwable t) {
// This is horrible. Make up a default one.
Log.error(t);
actions = Collections.EMPTY_MAP;
throw new JobException(t);
} finally {
if (fis != null)
try {
fis.close();
} catch (final IOException ie) {
// We don't care.
}
}
return actions;
}
}
private static File getActionsFile(final String jobId,
final String sectionId) throws IOException {
synchronized (JobHandler.planDirLock) {
final String firstDir = sectionId.length() <= 3 ? sectionId
: sectionId.substring(0, 3);
final String secondDir = sectionId.length() <= 6 ? sectionId
: sectionId.substring(0, 6);
final File sectionDir = new File(new File(new File(
JobHandler.jobsDir, jobId), firstDir), secondDir);
if (!sectionDir.exists())
sectionDir.mkdirs();
return new File(sectionDir, sectionId);
}
}
/**
* Flag that an action is to be set to the job.
*
* @param jobId
* the job ID.
* @param sectionId
* the section this applies to.
* @param newPredecessorSectionId
* the section to go after, or <tt>null</tt> if to be moved to
* the top of current siblings.
* @throws JobException
* if anything went wrong.
*/
public static void moveSection(final String jobId, final String sectionId,
final String newPredecessorSectionId) throws JobException {
// Get the sections.
JobPlanSection section = JobHandler.getSection(jobId, sectionId);
JobPlanSection newPredecessorSection = newPredecessorSectionId != null ? JobHandler
.getSection(jobId, newPredecessorSectionId)
: null;
// Swap sections.
section.getParent().moveSubSection(section, newPredecessorSection);
// Save modified structure.
try {
JobHandler.saveJobList();
} catch (final IOException e) {
throw new JobException(e);
}
}
/**
* Gets the plan for a job.
*
* @param jobId
* the job ID.
* @return the plan.
* @throws JobException
* if anything went wrong.
*/
public static JobPlan getJobPlan(final String jobId) throws JobException {
return JobHandler.getJobList().getJobPlan(jobId);
}
/**
* Obtain a list of the jobs that MartRunner is currently managing.
*
* @return a list of jobs.
* @throws JobException
* if anything went wrong.
*/
public static JobList getJobList() throws JobException {
try {
return JobHandler.loadJobList();
} catch (final IOException e) {
throw new JobException(e);
}
}
/**
* Makes MartRunner forget about a job.
*
* @param jobId
* the job to forget.
* @throws JobException
* if it couldn't lose its memory.
*/
public static void removeJob(final String jobId) throws JobException {
try {
// Stop job first if currently running.
JobHandler.stopJob(jobId);
// Remove the job list entry.
final JobList jobList = JobHandler.getJobList();
jobList.removeJob(jobId);
JobHandler.saveJobList();
// Recursively delete the job directory.
FileUtils.delete(new File(JobHandler.jobsDir, jobId));
} catch (final IOException e) {
throw new JobException(e);
}
}
/**
* Flag that a job skip drop status has changed.
*
* @param jobId
* the job ID.
* @param skipDropTable
* the new value - <tt>true</tt> to turn it on.
* @throws JobException
* if anything went wrong.
*/
public static void setSkipDropTable(final String jobId,
final boolean skipDropTable) throws JobException {
try {
// Create a job list entry.
final JobPlan jobPlan = JobHandler.getJobPlan(jobId);
// Set the stuff.
jobPlan.setSkipDropTable(skipDropTable);
JobHandler.saveJobList();
} catch (final IOException e) {
throw new JobException(e);
}
}
/**
* Flag that a job email address has changed.
*
* @param jobId
* the job ID.
* @param email
* the new email address to use as a contact address.
* @throws JobException
* if anything went wrong.
*/
public static void setEmailAddress(final String jobId, final String email)
throws JobException {
try {
// Create a job list entry.
final JobPlan jobPlan = JobHandler.getJobPlan(jobId);
// Set the email stuff.
final String trimmedEmail = email.trim().length() == 0 ? null
: email.trim();
if (jobPlan.getContactEmailAddress() == null
&& trimmedEmail != null
|| jobPlan.getContactEmailAddress() != null
&& !jobPlan.getContactEmailAddress().equals(trimmedEmail)) {
jobPlan.setContactEmailAddress(trimmedEmail);
JobHandler.saveJobList();
}
} catch (final IOException e) {
throw new JobException(e);
}
}
/**
* Flag that a job thread count has changed.
*
* @param jobId
* the job ID.
* @param threadCount
* the new thread count to use.
* @throws JobException
* if anything went wrong.
*/
public static void setThreadCount(final String jobId, final int threadCount)
throws JobException {
try {
// Create a job list entry.
final JobPlan jobPlan = JobHandler.getJobPlan(jobId);
// Set the thread count.
if (threadCount != jobPlan.getThreadCount()) {
jobPlan.setThreadCount(threadCount);
JobHandler.saveJobList();
}
} catch (final IOException e) {
throw new JobException(e);
}
}
/**
* Starts a job.
*
* @param jobId
* the job ID.
* @throws JobException
* if anything went wrong.
*/
public static void startJob(final String jobId) throws JobException {
if (JobHandler.jobManagers.containsKey(jobId))
return; // Ignore if already running.
final JobThreadManager manager = new JobThreadManager(jobId,
new JobThreadManagerListener() {
public void jobStopped(final String jobId) {
JobHandler.jobManagers.remove(jobId);
Log.info("Thread manager stopped for " + jobId);
}
});
JobHandler.jobManagers.put(jobId, manager);
manager.startThreadManager();
Log.info("Thread manager started for " + jobId);
}
/**
* Stops a job.
*
* @param jobId
* the job ID.
* @throws JobException
* if anything went wrong.
*/
public static void stopJob(final String jobId) throws JobException {
if (!JobHandler.jobManagers.containsKey(jobId))
return; // Ignore if already stopped.
final JobThreadManager manager = (JobThreadManager) JobHandler.jobManagers
.get(jobId);
manager.stopThreadManager();
// Don't remove it. The callback will do that.
Log.info("Stopped thread manager " + jobId);
}
private static File getJobListFile() throws IOException {
return new File(JobHandler.jobsDir, "list");
}
private static JobList loadJobList() throws IOException {
synchronized (JobHandler.planDirLock) {
if (JobHandler.jobList != null)
return JobHandler.jobList;
Log.debug("Loading list");
final File jobListFile = JobHandler.getJobListFile();
// Load existing job plan.
FileInputStream fis = null;
JobList jobList = null;
// Doesn't exist? Return a default new list.
if (!jobListFile.exists())
jobList = new JobList();
else
try {
fis = new FileInputStream(jobListFile);
final ObjectInputStream ois = new ObjectInputStream(fis);
jobList = (JobList) ois.readObject();
} catch (final IOException e) {
throw e;
} catch (final Throwable t) {
// This is horrible. Make up a default one.
Log.error(t);
jobList = new JobList();
} finally {
if (fis != null)
fis.close();
}
JobHandler.jobList = jobList;
return jobList;
}
}
private static void saveJobList() throws IOException {
synchronized (JobHandler.planDirLock) {
JobHandler.jobListIsDirty = true;
}
}
// Tools are static and cannot be instantiated.
private JobHandler() {
}
}