/* * 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.job; import java.util.ArrayList; import java.util.Date; import com.slamd.client.ClientSideJob; import com.slamd.common.Constants; import com.slamd.parameter.InvalidValueException; import com.slamd.parameter.ParameterList; import com.slamd.stat.RealTimeStatReporter; import com.slamd.stat.StatTracker; import static com.slamd.admin.AdminServlet.slamdServer; /** * This class implements a thread that may be executed as a SLAMD job. * Depending on the job configuration, multiple instances of this thread may * operate concurrently on the same machine or across multiple machines. * Anyone that wishes to implement their own custom job for SLAMD should only * need to extend this class. * * * @author Neil A. Wilson */ public abstract class JobClass extends Thread { // Indicates whether the job is currently running private boolean isRunning; // Indicates whether a request has been made to stop the job private boolean stopRequested; // The client number associated with this job class. private int clientNumber; // The collection interval that should be used when gathering statistics. private int collectionInterval; // The length of time in seconds that this job should be allowed to run private int duration; // The job state that should be assigned to the job if shouldStop returns // true. private int shouldStopReason; // The thread number associated with this job thread. private int threadNumber; // Indicates the time at which the job actually started running private long actualStartTime; // Indicates the time at which the job actually stopped running. private long actualStopTime; // Indicates the time at which the job should stop running private long shouldStopTime; // The time the job should start running in milliseconds. private long startTimeMillis; // The list of parameters that customize the way that this job is to work private ParameterList parameters; // The parent job that is controlling this thread private ClientSideJob job; // The client ID that indicates the client running this job. private String clientID; // The thread ID that will be used to identify this thread to the parent job private String threadID; /** * Creates a new job thread that is not running and that has not been * requested to stop. This should be invoked by the constructor of all * subclasses. */ protected JobClass() { // No implementation is required. All real work is done in the // initializeJobThread and initialize methods. } /** * Retrieves the name of the job performed by this job thread. * * @return The name of the job performed by this job thread. */ public abstract String getJobName(); /** * Retrieves a short description of the job performed by this job thread. * * @return A short description of the job performed by this job thread. */ public abstract String getShortDescription(); /** * Retrieves a long description of the job performed by this job thread. Each * element of the array returned will be treated as a separate paragraph in * the administrative interface. * * @return A long description of the job performed by this job thread. */ public String[] getLongDescription() { return new String[] { getShortDescription() }; } /** * Retrieves the name of the category in which this job class exists. This is * used to help arrange the job classes in the administrative interface. * * @return The name of the category in which this job class exists. */ public String getJobCategoryName() { return null; } /** * Provides a means for job classes to have a level of control over the * number of clients that will be used to run a job. If a job class * implements this method and returns a value greater than 0, then that number * of clients will always be used to run the job, and the form allowing the * user to schedule a job will not show the "Number of Clients" field. By * default, the user will be allowed to choose the number of clients. * * @return The number of clients that should be used to run this job, or -1 * if the user should be allowed to specify the number of clients. */ public int overrideNumClients() { return -1; } /** * Provides a means for job classes to have a level of control over the * number of threads per client that will be used to run a job. If a job * class implements this method and returns a value greater than 0, then that * number of threads per client will always be used to run the job, and the * form allowing the user to schedule a job will not show the "Threads per * Client" field. By default, the user will be allowed to choose the number * of threads per client. * * @return The number of threads per client that should be used to run this * job, or -1 if the user should be allowed to specify the number * of threads per client. */ public int overrideThreadsPerClient() { return -1; } /** * Provides a means for job classes to have a level of control over the * statistics collection interval that will be used for a job. If a job class * implements this method and returns a value greater than 0, then that * collection interval will always be used to run the job, and the form * that allows the user to schedule a job will not show the "Statistics * Collection Interval" field. By default, the user will be allowed to choose * the statistics collection interval. * * @return The collection interval that should be used for this job, or -1 if * the user should be allowed to specify the collection interval. */ public int overrideCollectionInterval() { return -1; } /** * Retrieves a list of parameter "stubs" that should be used to indicate * which parameters should be passed to the initialize method. These stubs * do not need to have values (although they can be given default values). * * @return A list of the parameter stubs that can be used to determine which * parameters must/may be provided to the initialize method. */ public abstract ParameterList getParameterStubs(); /** * Retrieves a list of parameter "stubs" that specify all the parameters that * should be available to clients. This will generally be exactly the same * as the list returned by the <CODE>getParameterStubs</CODE> method, but in * some cases the list of parameters scheduled in the configuration directory * may differ a little (e.g., to be able to handle dynamically-generated * parameters created at the time the job is scheduled). * * @return A list of parameter "stubs" that specify all the parameters that * should be available to clients. */ public ParameterList getClientSideParameterStubs() { return getParameterStubs(); } /** * Retrieves the set of stat trackers that will be maintained by this job * class. The stat trackers returned by this method do not have to actually * contain any statistics -- the display name and stat tracker class should * be the only information that callers of this method should rely upon. Note * that this list can be different from the list of statistics actually * collected by the job in some cases (e.g., if the job may not return all the * stat trackers it advertises in all cases, or if the job may return stat * trackers that it did not advertise), but it is a possibility that only the * stat trackers returned by this method will be accessible for some features * in the SLAMD server. * * @param clientID The client ID that should be used for the * returned stat trackers. * @param threadID The thread ID that should be used for the * returned stat trackers. * @param collectionInterval The collection interval that should be used for * the returned stat trackers. * * @return The set of stat trackers that will be maintained by this job * class. */ public abstract StatTracker[] getStatTrackerStubs(String clientID, String threadID, int collectionInterval); /** * Retrieves the stat trackers that are maintained for this job thread. * * @return The stat trackers that are maintained for this job thread. */ public abstract StatTracker[] getStatTrackers(); /** * Retrieves the client number associated with this job thread. This provides * a rudimentary capability for a job to have different behaviors on different * clients. The first client that receives a job request will be client 0, * the next will be client 1, etc. * * @return The client number associated with this job thread. */ public int getClientNumber() { return clientNumber; } /** * Specifies the client number to use for this job thread. * * @param clientNumber The client number to use for this job thread. */ public void setClientNumber(int clientNumber) { this.clientNumber = clientNumber; } /** * Retrieves the thread number associated with this job thread. Note that * this value is the thread number for this client. Other clients will have * a thread with the same thread number. * * @return The thread number associated with this job thread. */ public int getThreadNumber() { return threadNumber; } /** * Specifies the thread number for this job thread. * * @param threadNumber The thread number to use for this job thread. */ public void setThreadNumber(int threadNumber) { this.threadNumber = threadNumber; } /** * Provides a means of validating the information used to schedule the job, * including the scheduling information and list of parameters. * * @param numClients The number of clients that should be used to * run the job. * @param threadsPerClient The number of threads that should be created on * each client to run the job. * @param threadStartupDelay The delay in milliseconds that should be used * when starting the client threads. * @param startTime The time that the job should start running. * @param stopTime The time that the job should stop running. * @param duration The maximum length of time in seconds that the * job should be allowed to run. * @param collectionInterval The collection interval that should be used * when gathering statistics for the job. * @param parameters The set of parameters provided to this job that * can be used to customize its behavior. * * @throws InvalidValueException If the provided information is not * appropriate for running this job. */ public void validateJobInfo(int numClients, int threadsPerClient, int threadStartupDelay, Date startTime, Date stopTime, int duration, int collectionInterval, ParameterList parameters) throws InvalidValueException { // No implementation required by default. } /** * Indicates whether this job class implements logic that makes it possible to * test the validity of job parameters before scheduling the job for execution * (e.g., to see if the server is reachable using the information provided). * By default, this method returns <CODE>false</CODE> to indicate that this is * not provided, but actual implementations may override this method to * return <CODE>true</CODE> and should also override the * <CODE>testJobParameters</CODE> method to implement the actual test. * * @return <CODE>true</CODE> if this job provides a means of testing the job * parameters, or <CODE>false</CODE> if not. */ public boolean providesParameterTest() { return false; } /** * Provides a means of testing the provided job parameters to determine * whether they are valid (e.g., to see if the server is reachable) before * scheduling the job for execution. This method will be executed by the * SLAMD server system itself and not by any of the clients. * * @param parameters The job parameters to be tested. * @param outputMessages The lines of output that were generated as part of * the testing process. Each line of output should * be added to this list as a separate string, and * empty strings (but not <CODE>null</CODE> values) * are allowed to provide separation between * different messages. No formatting should be * provided for these messages, however, since they * may be displayed in either an HTML or plain text * interface. * * @return <CODE>true</CODE> if the test completed successfully, or * <CODE>false</CODE> if not. Note that even if the test did not * complete successfully, the user will be presented with a warning * but will still be allowed to schedule the job using the provided * parameters. This is necessary because the parameters may still be * valid even if the server couldn't validate them at the time the * job was scheduled (e.g., if the server wasn't running or could not * be reached by the SLAMD server even though it could be by the * clients). */ public boolean testJobParameters(ParameterList parameters, ArrayList<String> outputMessages) { outputMessages.add("No parameter tests have been defined for " + getJobName() + " jobs."); return false; } /** * Indicates whether this job class is deprecated. If it is deprecated, then * a warning will be displayed whenever the user attempts to schedule the job. * A job that is deprecated will not behave any differently, but merely serves * as a warning that a better option may exist for the same type of test. * * @param message The buffer to which a message may be appended that * provides additional information about the deprecation. * * @return <CODE>true</CODE> if this job class has been deprecated, or * <CODE>false</CODE> if not. */ public boolean isDeprecated(StringBuilder message) { return false; } /** * Performs a one-time initialization for this job. This initialization is * performed on the SLAMD server immediately before the job is sent to the * clients for processing. Note that the work performed by this method should * only be in the form of interaction with something external. Changes to * instance variables or other items internal to this method will not be * reflected on clients. * * @param parameters The set of parameters provided to this job that can be * used to customize its behavior. * * @throws UnableToRunException If the job initialization could not be * completed successfully and the job is unable * to run. */ public void initializeJob(ParameterList parameters) throws UnableToRunException { // No implementation required by default. } /** * Performs initialization for this job on each client immediately before each * thread is created to actually run the job. Note that if any changes are to * be made to variables that should be available to the individual threads, * then those variables should be declared as static. * * @param clientID The ID assigned to the client running this job. * @param parameters The set of parameters provided to this job that can be * used to customize its behavior. * * @throws UnableToRunException If the client initialization could not be * completed successfully and the job is unable * to run. */ public void initializeClient(String clientID, ParameterList parameters) throws UnableToRunException { // No implementation required by default. } /** * Initializes this job thread to be used to actually run the job on the * client. The provided parameter list should be processed to customize the * behavior of this job thread, and any other initialization that needs to be * done in order for the job to run should be performed here as well (e.g., * creating and initializing the status counters). * * @param clientID The client ID for this job thread. * @param threadID The thread ID for this job thread. * @param collectionInterval The length of time in seconds to use as the * statistics collection interval. * @param parameters The set of parameters provided to this job that * can be used to customize its behavior. * * @throws UnableToRunException If the thread initialization could not be * completed successfully and the job is unable * to run. */ public abstract void initializeThread(String clientID, String threadID, int collectionInterval, ParameterList parameters) throws UnableToRunException; /** * The method that does all the real work for this job. It performs the task * associated with the job thread based on the information provided through * the parameter list passed into the <CODE>initialize</CODE> method. */ public abstract void runJob(); /** * Retrieves the number of threads that are currently active for this job. * * @return The number of threads that are currently active for this job. */ public int getActiveThreadCount() { return job.getActiveThreadCount(); } /** * Performs any per-thread finalization that should be done for this job. By * default, no action is performed. */ public void finalizeThread() { // No implementation required by default. } /** * Performs any per-client finalization that should be done for this job. By * default, no action is performed. */ public void finalizeClient() { // No implementation required by default. } /** * Performs any per-job finalization that should be done for this job. By * default, no action is performed. */ public void finalizeJob() { // No implementation required by default. } /** * Provides the job thread with all of the information that it needs to run. * This should be extended by actual job thread implementations, but the * version in this method should always be invoked first using * <CODE>super.initialize(threadID, job, parameters)</CODE> before any * custom code. * * @param clientID The client ID that will be used to identify the * client to the parent job. * @param threadID The thread ID that will be used to identify * this thread to the parent job. * @param collectionInterval The interval in seconds that should be used * when gathering statistics during job * processing. * @param job The parent job that is controlling this thread. * @param duration The length of time in seconds that the job * should be allowed to run. A value of -1 * indicates an unlimited duration. * @param stopTime The time at which the job should stop running. * A value of <CODE>null</CODE> indicates no * specified stop time. * @param startTimeMillis The time this thread should start running in * milliseconds since January 1, 1970. * @param parameters The list of parameters (containing values) that * should be used to control the operation of this * job. * * @throws UnableToRunException If a problem occurs during initialization. */ public final void initializeJobThread(String clientID, String threadID, int collectionInterval, ClientSideJob job, int duration, Date stopTime, long startTimeMillis, ParameterList parameters) throws UnableToRunException { this.clientID = clientID; this.threadID = threadID; this.collectionInterval = collectionInterval; this.job = job; this.duration = duration; this.startTimeMillis = startTimeMillis; this.parameters = parameters; this.shouldStopReason = Constants.JOB_STATE_STOPPED_BY_USER; if (stopTime == null) { shouldStopTime = -1; } else { shouldStopTime = stopTime.getTime(); shouldStopReason = Constants.JOB_STATE_STOPPED_DUE_TO_STOP_TIME; } isRunning = false; stopRequested = false; try { this.initializeThread(clientID, threadID, collectionInterval, parameters); } catch (Exception e) { isRunning = false; stopRequested = true; shouldStopReason = Constants.JOB_STATE_STOPPED_DUE_TO_ERROR; logMessage("Job thread initialization failed: " + stackTraceToString(e)); throw new UnableToRunException("Job thread initialization failed: " + stackTraceToString(e)); } } /** * Retrieves the client side job associated with this job class. * * @return The client side job associated with this job class. */ public final ClientSideJob getClientSideJob() { return job; } /** * Sets the client-side job that should be associated with this job class. * * @param job The client-side job that should be associated with this job * class. */ public final void setClientSideJob(ClientSideJob job) { this.job = job; } /** * Retrieves the job ID of the job with which this job thread is currently * associated. * * @return The job ID of the job with which this job thread is currently * associated, or <CODE>null</CODE> if this thread is not associated * with any job. */ public String getJobID() { if (job == null) { return null; } return job.getJobID(); } /** * Retrieves the client ID for this job thread. * * @return The client ID for this job thread. */ public final String getClientID() { return clientID; } /** * Retrieves the thread ID for this job thread. * * @return The thread ID for this job thread. */ public final String getThreadID() { return threadID; } /** * Retrieves the collection interval for this job thread. * * @return The collection interval for this job thread. */ public final int getCollectionInterval() { return collectionInterval; } /** * Retrieves the scheduled duration for this job thread. * * @return The scheduled duration for this job thread. */ public final int getScheduledDuration() { return duration; } /** * Retrieves the time at which this job should stop, either because of the * duration or the stop time. * * @return The time at which this job should stop, either because of the * duration or the stop time. */ public final long getShouldStopTime() { return shouldStopTime; } /** * Indicates that the job should start running. It performs some necessary * administrative work and then starts the job thread. This may not be * overridden by subclasses, so any work that is intended to be done before * the job actually starts should be done in the <CODE>initialize</CODE> * method. * * @throws AlreadyRunningException If this job thread is already running. */ public final void startJob() throws AlreadyRunningException { // Make sure that the thread is not already running if (isRunning) { throw new AlreadyRunningException("Thread " + threadID + " is already running"); } else { // If the initialization failed, then the stop reason will indicate that // it was stopped because of an error. If that's the case, then don't // try to run. if (shouldStopReason != Constants.JOB_STATE_STOPPED_DUE_TO_ERROR) { start(); } } } /** * Requests that the job stop running at the earliest convenient time. There * is no guarantee that the job has actually stopped running by the time that * this method returns. * * @param stopReason The reason that the job should stop running. */ public final void stopJob(int stopReason) { stopRequested = true; shouldStopReason = stopReason; } /** * Requests that the job stop running at the earliest convenient time, and * then waits until the job has stopped completely. When this method returns, * it is safe to assume that the job is no longer running. * * @param stopReason The reason that the job should stop running. */ public final void stopAndWait(int stopReason) { stopRequested = true; shouldStopReason = stopReason; while (isRunning) { try { Thread.sleep(Constants.THREAD_BLOCK_SLEEP_TIME); } catch (InterruptedException ie) {} } } /** * Indicates whether the job is currently running. * * @return <CODE>true</CODE> if the job is running, or <CODE>false</CODE> if * it is not. */ public final boolean isRunning() { return isRunning; } /** * Indicates whether a request has been made to stop the job from an external * source (e.g., from a call to the <CODE>stopJob</CODE> method). Note that * there may be other reasons that the job should stop running (for example, * if a time limit has been set and has been reached). Therefore, the * <CODE>shouldStop</CODE> method should be used to determine whether the job * should stop processing altogether. * * @return <CODE>true</CODE> if a request has been made to stop this job, or * <CODE>false</CODE> if not. */ public final boolean stopRequested() { return stopRequested; } /** * Indicates whether the job should stop processing for any reason. This * should be checked periodically from the <CODE>runJob</CODE> method to * determine whether it is time to stop running. * * @return <CODE>true</CODE> if the job should stop running, or * <CODE>false</CODE> if there is no reason for it to stop. */ public final boolean shouldStop() { if (stopRequested) { if (shouldStopReason == Constants.JOB_STATE_STOPPED_BY_SHUTDOWN) { job.setJobState(shouldStopReason); } else { job.setJobState(Constants.JOB_STATE_STOPPED_BY_USER); } return true; } if ((shouldStopTime > 0) && (System.currentTimeMillis() > shouldStopTime)) { job.setJobState(shouldStopReason); return true; } return false; } /** * Indicates whether real-time statistics collection has been enabled in the * client with which this job is associated. * * @return <CODE>true</CODE> if the associated client has enabled real-time * statistics collection, or <CODE>false</CODE> if not. */ public final boolean enableRealTimeStats() { if (job == null) { return false; } else { return job.enableRealTimeStats(); } } /** * Retrieves the real-time stat reporter that should be used for this job. * * @return The real-time stat reporter that should be used for this job, or * <CODE>null</CODE> if none should be used. */ public final RealTimeStatReporter getStatReporter() { if (job == null) { return null; } else { return job.getStatReporter(); } } /** * Prevent the deprecated destroy method from being used by subclasses. */ @Override() @SuppressWarnings("deprecation") public final void destroy() { // No implementation required. } /** * Provides a means for destroying this job thread if it does not respond * to a graceful shutdown request. The default implementation does nothing, * but an actual job class may wish to do something like close a connection to * a remote server that could cause the thread to terminate. */ public void destroyThread() { // No implementation provided by default. } /** * Indicates that this job thread has completed, but there were errors that * may impact how the results should be interpreted. */ public final void indicateCompletedWithErrors() { job.setJobState(Constants.JOB_STATE_COMPLETED_WITH_ERRORS); } /** * Indicates that this job thread has been stopped because of an unrecoverable * error. */ public final void indicateStoppedDueToError() { job.setJobState(Constants.JOB_STATE_STOPPED_DUE_TO_ERROR); } /** * Writes a message to the client message writer so that if it is running in * verbose mode this information will be available to that client. This * message will not be displayed if the client is not operating in verbose * mode, nor will it be sent back to the SLAMD server to be included in the * log. * * @param message The message to be written. */ public final void writeVerbose(String message) { if (job == null) { // If the SLAMD server is available, then use it to log the message. // Otherwise, just print it to standard output. try { slamdServer.logMessage(Constants.LOG_LEVEL_JOB_DEBUG, message); } catch (Exception e) { System.err.println(message); } } else { job.writeVerbose(message); } } /** * Sends a status message regarding the progress of this job thread to the * parent job so that it can be recorded in the SLAMD logger. It will be * logged using a job status log level (which is enabled by default), so only * significant events (e.g., exceptions) should be announced using this * method. Less significant events should be logged using the * <CODE>logVerboseStatusMessage</CODE> method. * * @param message The message to be logged. */ public final void logMessage(String message) { if (job == null) { // If the SLAMD server is available, then use it to log the message. // Otherwise, just print it to standard error. try { slamdServer.logMessage(Constants.LOG_LEVEL_JOB_PROCESSING, message); } catch (Exception e) { System.err.println(message); } } else { job.logMessage(threadID, message); } } /** * A wrapper for the <CODE>runJob</CODE> method (which actually does the real * work for this job thread). It takes care of setting a flag used to * determine if the job is currently running or not, and will properly unset * it when the job is no longer running, even if it stopped running because of * an uncaught exception. This method may not be overridden by subclasses. */ @Override() public final void run() { // Sleep until it is time for the job to actually start. while ((! shouldStop()) && (System.currentTimeMillis() < startTimeMillis)) { try { Thread.sleep(10); } catch (InterruptedException ie) {} } writeVerbose("Starting thread " + threadID); // Get the current time and mark it as the start time. actualStartTime = System.currentTimeMillis(); // Figure out when the job should stop. If a duration was specified but // no stop time, then make the stop time (now + duration). If a stop // time was specified but no duration, then just use the stop time. If // both a stop time and a duration have been specified, then pick the // one that will come first. if ((duration > 0) && ((shouldStopTime <= 0) || (shouldStopTime > (actualStartTime + duration)))) { shouldStopTime = actualStartTime + (1000 * duration); shouldStopReason = Constants.JOB_STATE_STOPPED_DUE_TO_DURATION; } // Indicate that the job is actually running. isRunning = true; try { // Run the job. This will not complete until the job is done or has // crashed and burned. if (! shouldStop()) { runJob(); if (job.getJobState() == Constants.JOB_STATE_RUNNING) { job.setJobState(Constants.JOB_STATE_COMPLETED_SUCCESSFULLY); } } // Run the per-thread finalization. finalizeThread(); } catch (Exception e) { // If a problem occurred with the job, then this should take care of it // rather than passing the exception up the line and causing all kinds of // problems. job.logMessage(threadID, "Uncaught exception \"" + e + "\" -- Stack Trace: " + stackTraceToString(e)); e.printStackTrace(); job.setJobState(Constants.JOB_STATE_STOPPED_DUE_TO_ERROR); } finally { // Indicate that the job isn't running anymore. actualStopTime = System.currentTimeMillis(); isRunning = false; job.threadDone(this); } } /** * Retrieves a string representation of the stack trace contained in the * provided exception. * * @param t The exception for which to retrieve the stack trace. * * @return The string representation */ public static String stackTraceToString(Throwable t) { if (t == null) { return null; } String EOL = System.getProperty("line.separator"); StringBuilder buffer = new StringBuilder(); buffer.append(t.toString()); buffer.append(EOL); String separator = " "; StackTraceElement[] elements = t.getStackTrace(); for (int i=0; i < elements.length; i++) { buffer.append(separator); buffer.append(elements[i].getClassName()); buffer.append('.'); buffer.append(elements[i].getMethodName()); buffer.append('('); buffer.append(elements[i].getFileName()); buffer.append(':'); buffer.append(elements[i].getLineNumber()); buffer.append(')'); separator = EOL + " "; } Throwable cause = t.getCause(); if (cause != null) { buffer.append(EOL); buffer.append("Caused By: "); buffer.append(cause.toString()); elements = cause.getStackTrace(); for (int i=0; i < elements.length; i++) { buffer.append(separator); buffer.append(elements[i].getClassName()); buffer.append('.'); buffer.append(elements[i].getMethodName()); buffer.append('('); buffer.append(elements[i].getFileName()); buffer.append(':'); buffer.append(elements[i].getLineNumber()); buffer.append(')'); separator = EOL + " "; } } return buffer.toString(); } }