Java tutorial
/* * * Copyright 2015 Netflix, Inc. * * Licensed 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 com.netflix.genie.server.jobmanager.impl; import com.netflix.genie.common.exceptions.GenieException; import com.netflix.genie.common.exceptions.GeniePreconditionException; import com.netflix.genie.server.jobmanager.JobMonitor; import com.netflix.config.ConfigurationManager; import com.netflix.genie.common.model.Job; import com.netflix.genie.common.model.JobStatus; import com.netflix.genie.server.jobmanager.JobManager; import com.netflix.genie.server.metrics.GenieNodeStatistics; import com.netflix.genie.server.services.ExecutionService; import java.io.File; import java.util.Properties; import javax.mail.Authenticator; import javax.mail.Message; import javax.mail.MessagingException; import javax.mail.PasswordAuthentication; import javax.mail.Session; import javax.mail.Transport; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeMessage; import com.netflix.genie.server.services.JobService; import org.apache.commons.configuration.AbstractConfiguration; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The monitor thread that gets launched for each job. * * @author skrishnan * @author amsharma * @author tgianos */ public class JobMonitorImpl implements JobMonitor { private static final Logger LOG = LoggerFactory.getLogger(JobMonitorImpl.class); // interval to check status, and update in database if needed private static final int JOB_UPDATE_TIME_MS = 60000; // stdout filename private static final String STDOUT_FILENAME = "stdout"; // stderr filename private static final String STDERR_FILENAME = "stderr"; private final GenieNodeStatistics genieNodeStatistics; private final ExecutionService xs; private final JobService jobService; // max specified stdout size private final Long maxStdoutSize; // max specified stdout size private final Long maxStderrSize; // Config Instance to get all properties private final AbstractConfiguration config; private String jobId; private JobManager jobManager; // last updated time in DB private long lastUpdatedTimeMS; // the handle to the process for the running job private Process process; // the working directory for this job private String workingDir; // the stdout for this job private File stdOutFile; // the stderr for this job private File stdErrFile; // whether this job has been terminated by the monitor thread private boolean terminated; private int threadSleepTime = 5000; /** * Constructor. * * @param xs The job execution service. * @param jobService The job service API's to use. * @param genieNodeStatistics The statistics object to use */ public JobMonitorImpl(final ExecutionService xs, final JobService jobService, final GenieNodeStatistics genieNodeStatistics) { this.xs = xs; this.jobService = jobService; this.genieNodeStatistics = genieNodeStatistics; this.config = ConfigurationManager.getConfigInstance(); this.maxStdoutSize = this.config.getLong("com.netflix.genie.job.max.stdout.size", null); this.maxStderrSize = this.config.getLong("com.netflix.genie.job.max.stderr.size", null); this.workingDir = null; this.process = null; this.stdOutFile = null; this.stdErrFile = null; } /** * {@inheritDoc} */ @Override public void setJob(final Job job) throws GenieException { if (job == null || StringUtils.isBlank(job.getId())) { throw new GeniePreconditionException("No job entered."); } this.jobId = job.getId(); } /** * {@inheritDoc} */ @Override public void setWorkingDir(final String workingDir) { this.workingDir = workingDir; if (this.workingDir != null) { this.stdOutFile = new File(this.workingDir + File.separator + "stdout.log"); this.stdErrFile = new File(this.workingDir + File.separator + "stderr.log"); } } /** * {@inheritDoc} */ @Override public void setProcess(final Process process) throws GenieException { if (process == null) { throw new GeniePreconditionException("No process entered."); } this.process = process; } /** * {@inheritDoc} */ @Override public void setJobManager(final JobManager jobManager) throws GenieException { if (jobManager == null) { throw new GeniePreconditionException("No job manager entered."); } this.jobManager = jobManager; } /** * {@inheritDoc} */ @Override public void setThreadSleepTime(final int threadSleepTime) throws GenieException { if (threadSleepTime < 1) { throw new GeniePreconditionException("Sleep time was less than 1. Unable to sleep that little."); } this.threadSleepTime = threadSleepTime; } /** * The main run method for this thread - wait till it finishes, and manage * job state in DB. */ @Override public void run() { try { // wait for process to complete final boolean killed = this.xs.finalizeJob(this.jobId, waitForExit()) == JobStatus.KILLED; // Check if user email address is specified. If so // send an email to user about job completion. final String emailTo = this.jobService.getJob(this.jobId).getEmail(); if (emailTo != null) { LOG.info("User email address: " + emailTo); if (sendEmail(emailTo, killed)) { // Email sent successfully. Update success email counter this.genieNodeStatistics.incrSuccessfulEmailCount(); } else { // Failed to send email. Update email failed counter LOG.warn("Failed to send email."); this.genieNodeStatistics.incrFailedEmailCount(); } } } catch (final GenieException ge) { //TODO: Some sort of better handling. LOG.error(ge.getMessage(), ge); } } /** * Is the job running? * * @return true if job is running, false otherwise */ private boolean isRunning() { try { this.process.exitValue(); } catch (final IllegalThreadStateException e) { return true; } return false; } /** * Check if it is time to update the job status. * * @return true if job hasn't been updated for configured time, false * otherwise */ private boolean shouldUpdateJob() { final long curTimeMS = System.currentTimeMillis(); final long timeSinceStartMS = curTimeMS - this.lastUpdatedTimeMS; return timeSinceStartMS >= JOB_UPDATE_TIME_MS; } /** * Wait until the job finishes, and then return exit code. Also ensure that * stdout is within the limit (if specified), and update DB status * periodically (as RUNNING). * * @return exit code for the job after it finishes * @throws GenieException on issue */ private int waitForExit() throws GenieException { this.lastUpdatedTimeMS = System.currentTimeMillis(); while (this.isRunning()) { try { Thread.sleep(this.threadSleepTime); } catch (final InterruptedException e) { LOG.error("Exception while waiting for job " + this.jobId + " to finish", e); // move on } // update status only in JOB_UPDATE_TIME_MS intervals if (shouldUpdateJob()) { this.lastUpdatedTimeMS = this.jobService.setUpdateTime(this.jobId); // kill the job if it is writing out more than the max stdout/stderr limit // if it has been terminated already, move on and wait for it to clean up after itself String issueFile = null; if (!this.terminated) { if (this.stdOutFile != null && this.stdOutFile.exists() && this.maxStdoutSize != null && this.stdOutFile.length() > this.maxStdoutSize) { issueFile = STDOUT_FILENAME; } else if (this.stdErrFile != null && this.stdErrFile.exists() && this.maxStderrSize != null && this.stdErrFile.length() > this.maxStderrSize) { issueFile = STDERR_FILENAME; } } if (issueFile != null) { LOG.warn("Killing job " + this.jobId + " as its " + issueFile + " is greater than limit"); // kill the job - no need to update status, as it will be updated during next iteration try { this.jobManager.kill(); this.terminated = true; } catch (final GenieException e) { LOG.error("Can't kill job " + this.jobId + " after exceeding " + issueFile + " limit", e); // continue - hoping that it can get cleaned up during next iteration } } } } return this.process.exitValue(); } /** * Check the properties file to figure out if an email needs to be sent at * the end of the job. If yes, get mail properties and try and send email * about Job Status. * * @return 0 for success, -1 for failure * @throws GenieException on issue */ private boolean sendEmail(final String emailTo, final boolean killed) throws GenieException { LOG.debug("called"); final Job job = this.jobService.getJob(this.jobId); if (!this.config.getBoolean("com.netflix.genie.server.mail.enable", false)) { LOG.warn("Email is disabled but user has specified an email address."); return false; } // Sender's email ID final String fromEmail = this.config.getString("com.netflix.genie.server.mail.smpt.from", "no-reply-genie@geniehost.com"); LOG.info("From email address to use to send email: " + fromEmail); // Set the smtp server hostname. Use localhost as default final String smtpHost = this.config.getString("com.netflix.genie.server.mail.smtp.host", "localhost"); LOG.debug("Email smtp server: " + smtpHost); // Get system properties final Properties properties = new Properties(); // Setup mail server properties.setProperty("mail.smtp.host", smtpHost); // check whether authentication should be turned on Authenticator auth = null; if (this.config.getBoolean("com.netflix.genie.server.mail.smtp.auth", false)) { LOG.debug("Email Authentication Enabled"); properties.put("mail.smtp.starttls.enable", "true"); properties.put("mail.smtp.auth", "true"); final String userName = config.getString("com.netflix.genie.server.mail.smtp.user"); final String password = config.getString("com.netflix.genie.server.mail.smtp.password"); if (userName == null || password == null) { LOG.error("Authentication is enabled and username/password for smtp server is null"); return false; } LOG.debug("Constructing authenticator object with username" + userName + " and password " + password); auth = new SMTPAuthenticator(userName, password); } else { LOG.debug("Email authentication not enabled."); } // Get the default Session object. final Session session = Session.getInstance(properties, auth); try { // Create a default MimeMessage object. final MimeMessage message = new MimeMessage(session); // Set From: header field of the header. message.setFrom(new InternetAddress(fromEmail)); // Set To: header field of the header. message.addRecipient(Message.RecipientType.TO, new InternetAddress(emailTo)); JobStatus jobStatus; if (killed) { jobStatus = JobStatus.KILLED; } else { jobStatus = job.getStatus(); } // Set Subject: header field message.setSubject("Genie Job " + job.getName() + " completed with Status: " + jobStatus); // Now set the actual message final String body = "Your Genie Job is complete\n\n" + "Job ID: " + job.getId() + "\n" + "Job Name: " + job.getName() + "\n" + "Status: " + job.getStatus() + "\n" + "Status Message: " + job.getStatusMsg() + "\n" + "Output Base URL: " + job.getOutputURI() + "\n"; message.setText(body); // Send message Transport.send(message); LOG.info("Sent email message successfully...."); return true; } catch (final MessagingException mex) { LOG.error("Got exception while sending email", mex); return false; } } private static class SMTPAuthenticator extends Authenticator { private final String username; private final String password; /** * Default constructor. */ SMTPAuthenticator(final String username, final String password) { this.username = username; this.password = password; } /** * Return a PasswordAuthentication object based on username/password. */ @Override public PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(this.username, this.password); } } }