/* * Sun Public License * * The contents of this file are subject to the Sun Public License Version * 1.0 (the "License"). You may not use this file except in compliance with * the License. A copy of the License is available at http://www.sun.com/ * * The Original Code is the SLAMD Distributed Load Generation Engine. * The Initial Developer of the Original Code is Neil A. Wilson. * Portions created by Neil A. Wilson are Copyright (C) 2004-2010. * Some preexisting portions Copyright (C) 2002-2006 Sun Microsystems, Inc. * All Rights Reserved. * * Contributor(s): Neil A. Wilson */ package com.slamd.jobs; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.Socket; import java.text.ParseException; import java.util.ArrayList; import java.util.Date; import java.util.Random; import java.util.StringTokenizer; import com.unboundid.util.FixedRateBarrier; import com.unboundid.util.ValuePattern; import com.slamd.job.JobClass; import com.slamd.job.UnableToRunException; import com.slamd.parameter.IntegerParameter; import com.slamd.parameter.InvalidValueException; import com.slamd.parameter.Parameter; import com.slamd.parameter.ParameterList; import com.slamd.parameter.PasswordParameter; import com.slamd.parameter.PlaceholderParameter; import com.slamd.parameter.StringParameter; import com.slamd.stat.IncrementalTracker; import com.slamd.stat.IntegerValueTracker; import com.slamd.stat.RealTimeStatReporter; import com.slamd.stat.StatTracker; import com.slamd.stat.TimeTracker; /** * This class defines a SLAMD job that interacts with an IMAPv4 mail server. It * does so by performing the following operations: * * <OL> * <LI>Log into the server (<CODE>LOGIN</CODE> <I>{user} {password}</I>).</LI> * <LI>Select the INBOX folder (<CODE>SELECT INBOX</CODE>).</LI> * <LI>Retrieve a list of all messages (<CODE> FETCH * 1:</CODE><I>{num}</I><CODE> (FLAGS)</CODE>).</LI> * <LI>Log out (<CODE>LOGOUT</CODE>).</LI> * </OL> * * * @author Neil A. Wilson */ public class IMAPCheckRateJobClass extends JobClass { /** * The prefix that will be placed before the incrementing message ID for each * request. */ public static final String REQUEST_ID_PREFIX = "imaprate"; /** * The display name of the stat tracker used to count the number of IMAP * sessions established. */ public static final String STAT_TRACKER_IMAP_SESSIONS = "IMAP Sessions"; /** * The display name of the stat tracker used to count the number of failed * IMAP logins. */ public static final String STAT_TRACKER_FAILURE_COUNT = "Failed Logins"; /** * The display name of the stat tracker used to keep track of the number of * messages contained in the inbox for each IMAP session. */ public static final String STAT_TRACKER_MESSAGE_COUNT = "Message Count"; /** * The display name of the stat tracker used to time the process of * authenticating and retrieving the list of messages. */ public static final String STAT_TRACKER_SESSION_DURATION = "Session Duration (ms)"; /** * The display name of the stat tracker used to count the number of successful * IMAP logins. */ public static final String STAT_TRACKER_SUCCESS_COUNT = "Successful Logins"; // The port number of the IMAP server. private IntegerParameter portParameter = new IntegerParameter("imap_port", "IMAP Server Port", "The port number on which the IMAP server is " + "listening for requests.", true, 143, true, 1, true, 65535); // The length of time between initial requests. private IntegerParameter delayParameter = new IntegerParameter("delay", "Time Between IMAP Sessions (ms)", "The length of time in milliseconds between " + "attempts to access the IMAP server.", true, 0, true, 0, false, 0); // The parameter that specifies the maximum request rate. private IntegerParameter maxRateParameter = new IntegerParameter("maxRate", "Max Request Rate (Requests/Second/Client)", "Specifies the maximum request rate (in requests per second per " + "client) to attempt to maintain. If multiple clients are used, " + "then each client will attempt to maintain this rate. A value " + "less than or equal to zero indicates that the client should " + "attempt to perform requests as quickly as possible.", true, -1); // The parameter that specifies the interval over which to enforce the maximum // request rate. private IntegerParameter rateLimitDurationParameter = new IntegerParameter( "maxRateDuration", "Max Rate Enforcement Interval (Seconds)", "Specifies the duration in seconds of the interval over which to " + "attempt to maintain the configured maximum rate. A value of " + "zero indicates that it should be equal to the statistics " + "collection interval. Large values may allow more variation but " + "may be more accurate over time. Small values can better " + "ensure that the rate doesn't exceed the requested level but may " + "be less able to achieve the desired rate.", true, 0, true,0, false, 0); // The password to use when authenticating. private PasswordParameter passwordParameter = new PasswordParameter("user_pw", "User Password", "The password that will be used to authenticate " + "to the IMAP server.", true, ""); // A placeholder parameter that is only used for formatting. private PlaceholderParameter placeholder = new PlaceholderParameter(); // The address of the IMAP server. private StringParameter hostParameter = new StringParameter("imap_host", "IMAP Server Address", "The fully-qualified domain name or IP address of " + "the system running the IMAP server.", true, ""); // The user ID to use when authenticating. private StringParameter userIDParameter = new StringParameter("user_id", "User ID", "The user ID that will be used to authenticate to " + "the IMAP server. It may include a range of " + "numeric values chosen randomly by including " + "that range in brackets with the values separated " + "by a dash (i.e., \"user.[1-1000]\"), or a range " + "of sequentially-incrementing numeric values by " + "including that range in brackets with the values " + "separated by a colon (i.e., \"user.[1:1000]\").", true, ""); // Static variables used to hold parameter values. private static int imapPort; private static int delay; private static String imapAddress; private static String password; private static ValuePattern userIDPattern; // The random number generator for the job. private static Random parentRandom; private Random random; // The rate limiter for this job. private static FixedRateBarrier rateLimiter; // The stat trackers for the job. private IncrementalTracker failureCounter; private IncrementalTracker sessionCounter; private IncrementalTracker successCounter; private IntegerValueTracker messageCountTracker; private TimeTracker sessionTimer; /** * The default constructor used to create a new instance of the job class. * The only thing it should do is to invoke the superclass constructor. All * other initialization should be performed in the <CODE>initialize</CODE> * method. */ public IMAPCheckRateJobClass() { super(); } /** * {@inheritDoc} */ @Override() public String getJobName() { return "IMAP CheckRate"; } /** * {@inheritDoc} */ @Override() public String getShortDescription() { return "Repeatedly retrieve information about messages in an IMAPv4 inbox"; } /** * {@inheritDoc} */ @Override() public String[] getLongDescription() { return new String[] { "This job can be used to repeatedly establish sessions with an IMAPv4 " + "mail server and retrieve information about messages in the user's inbox." }; } /** * {@inheritDoc} */ @Override() public String getJobCategoryName() { return "Mail"; } /** * {@inheritDoc} */ @Override() public ParameterList getParameterStubs() { Parameter[] parameters = { placeholder, hostParameter, portParameter, userIDParameter, passwordParameter, delayParameter, maxRateParameter, rateLimitDurationParameter }; return new ParameterList(parameters); } /** * {@inheritDoc} */ @Override() public StatTracker[] getStatTrackerStubs(String clientID, String threadID, int collectionInterval) { return new StatTracker[] { new IncrementalTracker(clientID, threadID, STAT_TRACKER_IMAP_SESSIONS, collectionInterval), new TimeTracker(clientID, threadID, STAT_TRACKER_SESSION_DURATION, collectionInterval), new IntegerValueTracker(clientID, threadID, STAT_TRACKER_MESSAGE_COUNT, collectionInterval), new IncrementalTracker(clientID, threadID, STAT_TRACKER_SUCCESS_COUNT, collectionInterval), new IncrementalTracker(clientID, threadID, STAT_TRACKER_FAILURE_COUNT, collectionInterval) }; } /** * {@inheritDoc} */ @Override() public StatTracker[] getStatTrackers() { return new StatTracker[] { sessionCounter, sessionTimer, messageCountTracker, successCounter, failureCounter }; } /** * {@inheritDoc} */ @Override() public void validateJobInfo(int numClients, int threadsPerClient, int threadStartupDelay, Date startTime, Date stopTime, int duration, int collectionInterval, ParameterList parameters) throws InvalidValueException { // The user ID parameter must be parseable as a value pattern. StringParameter p = parameters.getStringParameter(userIDParameter.getName()); if ((p != null) && p.hasValue()) { try { new ValuePattern(p.getValue()); } catch (ParseException pe) { throw new InvalidValueException("The value provided for the '" + p.getDisplayName() + "' parameter is not a valid value " + "pattern: " + pe.getMessage(), pe); } } } /** * {@inheritDoc} */ @Override() public boolean providesParameterTest() { return true; } /** * {@inheritDoc} */ @Override() public boolean testJobParameters(ParameterList parameters, ArrayList<String> outputMessages) { // Get the parameters necessary to perform the test. StringParameter hostParam = parameters.getStringParameter(hostParameter.getName()); if ((hostParam == null) || (! hostParam.hasValue())) { outputMessages.add("ERROR: No IMAP server address was provided."); return false; } String host = hostParam.getStringValue(); IntegerParameter portParam = parameters.getIntegerParameter(portParameter.getName()); if ((portParam == null) || (! portParam.hasValue())) { outputMessages.add("ERROR: No IMAP server port was provided."); return false; } int port = portParam.getIntValue(); StringParameter userIDParam = parameters.getStringParameter(userIDParameter.getName()); if ((userIDParam == null) || (! userIDParam.hasValue())) { outputMessages.add("ERROR: No user ID was provided."); return false; } String userID = userIDParam.getStringValue(); PasswordParameter pwParam = parameters.getPasswordParameter(passwordParameter.getName()); if ((pwParam == null) || (! pwParam.hasValue())) { outputMessages.add("ERROR: No user password was provided."); return false; } String userPW = pwParam.getStringValue(); // Try to establish a connection to the IMAP server. Socket socket; BufferedReader reader; BufferedWriter writer; try { outputMessages.add("Trying to establish a connection to IMAP server " + host + ':' + port + "...."); socket = new Socket(host, port); reader = new BufferedReader(new InputStreamReader( socket.getInputStream())); writer = new BufferedWriter(new OutputStreamWriter( socket.getOutputStream())); outputMessages.add("Connected successfully."); outputMessages.add(""); } catch (Exception e) { outputMessages.add("ERROR: Unable to connect: " + stackTraceToString(e)); return false; } // Read the initial response line from the server. try { outputMessages.add("Trying to read the hello string from the server...."); String line = reader.readLine(); outputMessages.add("Hello string was '" + line + "'."); outputMessages.add(""); } catch (Exception e) { outputMessages.add("ERROR: Unable to read the hello string: " + stackTraceToString(e)); try { reader.close(); } catch (Exception e2) {} try { writer.close(); } catch (Exception e2) {} try { socket.close(); } catch (Exception e2) {} return false; } // Send the login request. try { outputMessages.add("Trying to send the LOGIN request to the server...."); writer.write("10 LOGIN " + userID + ' ' + userPW); writer.newLine(); writer.flush(); outputMessages.add("Successfully sent the LOGIN request."); outputMessages.add(""); } catch (Exception e) { outputMessages.add("ERROR: Unable to send the LOGIN request: " + stackTraceToString(e)); try { reader.close(); } catch (Exception e2) {} try { writer.close(); } catch (Exception e2) {} try { socket.close(); } catch (Exception e2) {} return false; } // Read the login response. boolean loginSuccessful = false; try { outputMessages.add("Trying to read the LOGIN response from the " + "server...."); String line = reader.readLine(); if (line.toLowerCase().startsWith("10 ok")) { loginSuccessful = true; } outputMessages.add("Read a LOGIN response of '" + line + "'."); outputMessages.add(""); } catch (Exception e) { outputMessages.add("ERROR: Unable to read the LOGIN response: " + stackTraceToString(e)); try { reader.close(); } catch (Exception e2) {} try { writer.close(); } catch (Exception e2) {} try { socket.close(); } catch (Exception e2) {} return false; } // If we've gotten here, then everything seems to be OK. Close the // connection and return whether the login was successful. try { outputMessages.add("Sending the LOGOUT request to the server."); outputMessages.add(""); writer.write("20 LOGOUT"); writer.newLine(); writer.flush(); } catch (Exception e) {} try { reader.close(); } catch (Exception e) {} try { writer.close(); } catch (Exception e) {} try { socket.close(); } catch (Exception e) {} outputMessages.add("All tests completed."); return loginSuccessful; } /** * {@inheritDoc} */ @Override() public void initializeClient(String clientID, ParameterList parameters) throws UnableToRunException { // Seed the parent random number generator. parentRandom = new Random(); // Get the address of the IMAP server. hostParameter = parameters.getStringParameter(hostParameter.getName()); if (hostParameter != null) { imapAddress = hostParameter.getStringValue(); } // Get the port for the IMAP server. portParameter = parameters.getIntegerParameter(portParameter.getName()); if (portParameter != null) { imapPort = portParameter.getIntValue(); } // Get the user ID. See if it should be a range of values. userIDParameter = parameters.getStringParameter(userIDParameter.getName()); if (userIDParameter != null) { try { userIDPattern = new ValuePattern(userIDParameter.getStringValue()); } catch (Exception e) { throw new UnableToRunException("Unable to parse user ID pattern: " + stackTraceToString(e), e); } } // Get the password. passwordParameter = parameters.getPasswordParameter(passwordParameter.getName()); if (passwordParameter != null) { password = passwordParameter.getStringValue(); } // Get the delay between requests. delayParameter = parameters.getIntegerParameter(delayParameter.getName()); if (delayParameter != null) { delay = delayParameter.getIntValue(); } rateLimiter = null; maxRateParameter = parameters.getIntegerParameter(maxRateParameter.getName()); if ((maxRateParameter != null) && maxRateParameter.hasValue()) { int maxRate = maxRateParameter.getIntValue(); if (maxRate > 0) { int rateIntervalSeconds = 0; rateLimitDurationParameter = parameters.getIntegerParameter( rateLimitDurationParameter.getName()); if ((rateLimitDurationParameter != null) && rateLimitDurationParameter.hasValue()) { rateIntervalSeconds = rateLimitDurationParameter.getIntValue(); } if (rateIntervalSeconds <= 0) { rateIntervalSeconds = getClientSideJob().getCollectionInterval(); } rateLimiter = new FixedRateBarrier(rateIntervalSeconds * 1000L, maxRate * rateIntervalSeconds); } } } /** * {@inheritDoc} */ @Override() public void initializeThread(String clientID, String threadID, int collectionInterval, ParameterList parameters) throws UnableToRunException { // Create the stat trackers for this thread. sessionCounter = new IncrementalTracker(clientID, threadID, STAT_TRACKER_IMAP_SESSIONS, collectionInterval); sessionTimer = new TimeTracker(clientID, threadID, STAT_TRACKER_SESSION_DURATION, collectionInterval); messageCountTracker = new IntegerValueTracker(clientID, threadID, STAT_TRACKER_MESSAGE_COUNT, collectionInterval); successCounter = new IncrementalTracker(clientID, threadID, STAT_TRACKER_SUCCESS_COUNT, collectionInterval); failureCounter = new IncrementalTracker(clientID, threadID, STAT_TRACKER_FAILURE_COUNT, collectionInterval); // Enable real-time reporting of the data for these stat trackers. RealTimeStatReporter statReporter = getStatReporter(); if (statReporter != null) { String jobID = getJobID(); sessionCounter.enableRealTimeStats(statReporter, jobID); sessionTimer.enableRealTimeStats(statReporter, jobID); messageCountTracker.enableRealTimeStats(statReporter, jobID); successCounter.enableRealTimeStats(statReporter, jobID); failureCounter.enableRealTimeStats(statReporter, jobID); } // Seed the random number generator for this thread. random = new Random(parentRandom.nextLong()); } /** * {@inheritDoc} */ @Override() public void runJob() { // Define variables that will be used throughout this method. boolean keepReading; BufferedReader reader; BufferedWriter writer; int highestUID; int idCounter; long lastStartTime = 0; Socket socket; String line; String lowerLine; String request; String userID; // Start the stat trackers. sessionCounter.startTracker(); sessionTimer.startTracker(); messageCountTracker.startTracker(); successCounter.startTracker(); failureCounter.startTracker(); // Loop until it is determined that the job should stop. mainLoop: while (! shouldStop()) { if (rateLimiter != null) { if (rateLimiter.await()) { continue; } } // If we need to sleep, then do so. if (delay > 0) { long now = System.currentTimeMillis(); long prevTestTime = now - lastStartTime; if (prevTestTime < delay) { try { Thread.sleep(delay - prevTestTime); } catch (Exception e) {} } } lastStartTime = System.currentTimeMillis(); // Start the attempt timer and indicate the beginning of a new attempt. sessionCounter.increment(); sessionTimer.startTimer(); // Get the user ID to use in the next request. userID = userIDPattern.nextValue(); idCounter = 1; // Open a connection to the IMAP server. try { socket = new Socket(imapAddress, imapPort); reader = new BufferedReader(new InputStreamReader( socket.getInputStream())); writer = new BufferedWriter(new OutputStreamWriter( socket.getOutputStream())); } catch (IOException ioe) { sessionTimer.stopTimer(); failureCounter.increment(); continue; } // Read the initial hello line from the server. try { line = reader.readLine(); } catch (IOException ioe) { sessionTimer.stopTimer(); failureCounter.increment(); try { reader.close(); writer.close(); socket.close(); } catch (Exception e) {} continue; } // Send the LOGIN request. request = REQUEST_ID_PREFIX + idCounter + " LOGIN " + userID + ' ' + password; try { writer.write(request); writer.newLine(); writer.flush(); } catch (IOException ioe) { sessionTimer.stopTimer(); failureCounter.increment(); try { reader.close(); writer.close(); socket.close(); } catch (Exception e) {} continue; } // Read the LOGIN response. It should be a single line, but we'll make it // a loop just in case. keepReading = true; while (keepReading) { try { line = reader.readLine(); if (line == null) { // The server must have closed the connection. keepReading = false; sessionTimer.stopTimer(); failureCounter.increment(); try { reader.close(); writer.close(); socket.close(); } catch (Exception e) {} continue mainLoop; } else if (line.startsWith(REQUEST_ID_PREFIX + idCounter)) { keepReading = false; lowerLine = line.toLowerCase(); if (! lowerLine.contains(REQUEST_ID_PREFIX + idCounter + " ok")) { sessionTimer.stopTimer(); failureCounter.increment(); try { reader.close(); writer.close(); socket.close(); } catch (Exception e) {} continue mainLoop; } } } catch (IOException ioe) { sessionTimer.stopTimer(); failureCounter.increment(); try { reader.close(); writer.close(); socket.close(); } catch (Exception e) {} continue mainLoop; } } // Specify that we will be working with the INBOX folder. idCounter++; request = REQUEST_ID_PREFIX + idCounter + " SELECT INBOX"; try { writer.write(request); writer.newLine(); writer.flush(); } catch (IOException ioe) { sessionTimer.stopTimer(); failureCounter.increment(); try { reader.close(); writer.close(); socket.close(); } catch (Exception e) {} continue; } // Read the response from the SELECT operation. It will be multiple // lines. In addition to the success line at the end, we will also want // to get the UID of the last message in the inbox so that we can retrieve // the entire list of flags. highestUID = -1; keepReading = true; while (keepReading) { try { line = reader.readLine(); if (line == null) { // The server must have closed the connection. keepReading = false; sessionTimer.stopTimer(); failureCounter.increment(); try { reader.close(); writer.close(); socket.close(); } catch (Exception e) {} continue mainLoop; } lowerLine = line.toLowerCase(); if (line.startsWith(REQUEST_ID_PREFIX + idCounter)) { keepReading = false; lowerLine = line.toLowerCase(); if (! lowerLine.contains(REQUEST_ID_PREFIX + idCounter + " ok")) { sessionTimer.stopTimer(); failureCounter.increment(); try { reader.close(); writer.close(); socket.close(); } catch (Exception e) {} continue mainLoop; } } else if (lowerLine.startsWith("*") && (lowerLine.indexOf("exists") > 0)) { StringTokenizer tokenizer = new StringTokenizer(line, " "); try { // The first will be the asterisk. The second should be the // highest ID in the inbox. tokenizer.nextToken(); highestUID = Integer.parseInt(tokenizer.nextToken()); } catch (Exception e) { sessionTimer.stopTimer(); failureCounter.increment(); try { reader.close(); writer.close(); socket.close(); } catch (Exception e2) {} continue mainLoop; } } } catch (IOException ioe) { sessionTimer.stopTimer(); failureCounter.increment(); try { reader.close(); writer.close(); socket.close(); } catch (Exception e) {} continue mainLoop; } } // If we didn't get a value for the highest message UID, then that's a // failure. if (highestUID < 0) { sessionTimer.stopTimer(); failureCounter.increment(); try { reader.close(); writer.close(); socket.close(); } catch (Exception e) {} continue; } else { messageCountTracker.addValue(highestUID); } // If we got a value of 0 for the highest message UID, then that means // the mailbox is empty. That's not an error, but we shouldn't try to // get the list of messages. if (highestUID > 0) { // Create a request that will retrieve the flags for all messages in the // inbox. idCounter++; request = REQUEST_ID_PREFIX + idCounter + " FETCH 1:" + highestUID + " (FLAGS)"; try { writer.write(request); writer.newLine(); writer.flush(); } catch (IOException ioe) { sessionTimer.stopTimer(); failureCounter.increment(); try { reader.close(); writer.close(); socket.close(); } catch (Exception e) {} continue; } // Read the list of flags for each message. We don't really care what // they are for this test, so just look for the end of the list. keepReading = true; while (keepReading) { try { line = reader.readLine(); if (line == null) { // The server must have closed the connection. keepReading = false; sessionTimer.stopTimer(); failureCounter.increment(); try { reader.close(); writer.close(); socket.close(); } catch (Exception e) {} continue mainLoop; } else if (line.startsWith(REQUEST_ID_PREFIX + idCounter)) { keepReading = false; lowerLine = line.toLowerCase(); if (! lowerLine.contains(REQUEST_ID_PREFIX + idCounter + " ok")) { sessionTimer.stopTimer(); failureCounter.increment(); try { reader.close(); writer.close(); socket.close(); } catch (Exception e) {} continue mainLoop; } } } catch (IOException ioe) { sessionTimer.stopTimer(); failureCounter.increment(); try { reader.close(); writer.close(); socket.close(); } catch (Exception e) {} continue mainLoop; } } } // Send the request to log out from the server and close the connection. idCounter++; request = REQUEST_ID_PREFIX + idCounter + " LOGOUT"; try { writer.write(request); writer.newLine(); writer.flush(); reader.close(); writer.close(); socket.close(); sessionTimer.stopTimer(); successCounter.increment(); } catch (IOException ioe) { sessionTimer.stopTimer(); failureCounter.increment(); try { reader.close(); writer.close(); socket.close(); } catch (Exception e) {} continue; } } // Stop the stat trackers. sessionCounter.stopTracker(); sessionTimer.stopTracker(); messageCountTracker.stopTracker(); successCounter.stopTracker(); failureCounter.stopTracker(); } }