/* * 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.client; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.Iterator; import java.util.LinkedHashMap; import com.slamd.job.AlreadyRunningException; import com.slamd.job.JobClass; import com.slamd.job.UnableToRunException; import com.slamd.common.Constants; import com.slamd.common.JobClassLoader; import com.slamd.common.SLAMDException; import com.slamd.message.ClassTransferRequestMessage; import com.slamd.parameter.ParameterList; import com.slamd.stat.RealTimeStatReporter; import com.slamd.stat.StatPersistenceThread; import com.slamd.stat.StatTracker; /** * This class defines a "client-side" job. It is a representation of a job that * is held by the client, having no knowledge of anything on the server side). * All methods implemented in this class operate on the local version of the * job only and have no direct impact on the execution of the job that may be * occurring concurrently on other clients. * * * @author Neil A. Wilson */ public class ClientSideJob { /** * The date formatter used to generate the timestamps in messages to be * logged. */ static SimpleDateFormat dateFormat = new SimpleDateFormat(Constants.DISPLAY_DATE_FORMAT); // The set of active job threads. ArrayList<JobClass> activeThreads; // The set of messages that have been logged during the execution of the job. ArrayList<String> logMessages; // Indicates whether real-time statistics collection should be used. boolean enableRealTimeStats; // Indicates whether the job is done processing for some reason boolean isDone; // Indicates whether the custom job class loader should be used. boolean useCustomClassLoader; // The client message writer that will be used to write messages to the // client. ClientMessageWriter messageWriter; // The time at which this job should start running. Date startTime; // The time at which this job should stop running. Date stopTime; // The client number associated with this job. int clientNumber; // The length of time in seconds to use as the statistics collection interval. int collectionInterval; // The maximum length of time in seconds that the job should be allowed to // run. int duration; // The state of the job as it currently exists. int jobState; // The number of concurrent job threads that should run on each client. int threadsPerClient; // The delay in milliseconds between the time that each thread should be // started. int threadStartupDelay; // The time that the job actually started running. long actualStartTime; // The time that the job actually stopped running. long actualStopTime; // The time (ms since January 1, 1970) that the job was scheduled to start. long scheduledStartTime; // A mutex used to protect multithreaded access to job threads. private final Object jobThreadMutex; // A mutex used to protect multithreaded access to the log list. private final Object logMutex; // The set of parameters that can customize the behavior of the job. ParameterList parameters; // The stat reporter that should be used for collecting statistics RealTimeStatReporter statReporter; // The client with which this job is associated. Client client; // The set of job threads that are associated with this job. JobClass[] jobThreads; // The location in which job class files may be found. String classPath; // The ID of the client that is being used to run this job. String clientID; // The unique ID assigned to this job String jobID; // The name of the Java class file that serves as the job thread String jobClass; /** * Creates a new client-side job with the provided information. * * @param client The client with which this job is associated. * @param jobID The job ID for this job. * @param jobClass The name of the Java class that should be * executed to run this job. * @param threadsPerClient The number of threads that should be used to * run this job. * @param startTime The time at which this job is supposed to * start. * @param stopTime The time at which this job is supposed to * stop. * @param clientNumber The client number associated with this job. * @param duration The maximum length of time in seconds that * this job should run. * @param collectionInterval The length of time in seconds that should be * used for the statistics collection interval. * @param threadStartupDelay The delay in milliseconds that should be used * when creating the individual client threads. * @param parameters The list of job-specific parameters that * control how this job should operate. * @param useCustomClassLoader Indicates whether the custom job class loader * should be used to load job classes. * @param enableRealTimeStats Indicates whether this job should report * statistics in real-time. * @param statReporter The stat reporter that should be used to * report real-time statistics. */ public ClientSideJob(Client client, String jobID, String jobClass, int threadsPerClient, Date startTime, Date stopTime, int clientNumber, int duration, int collectionInterval, int threadStartupDelay, ParameterList parameters, boolean useCustomClassLoader, boolean enableRealTimeStats, RealTimeStatReporter statReporter) { this.client = client; this.classPath = client.getClassPath(); this.clientID = client.getClientID(); this.messageWriter = client.getMessageWriter(); this.jobID = jobID; this.jobClass = jobClass; this.threadsPerClient = threadsPerClient; this.startTime = startTime; this.stopTime = stopTime; this.clientNumber = clientNumber; this.duration = duration; this.collectionInterval = collectionInterval; this.threadStartupDelay = threadStartupDelay; this.parameters = parameters; this.useCustomClassLoader = useCustomClassLoader; this.enableRealTimeStats = enableRealTimeStats; this.statReporter = statReporter; scheduledStartTime = startTime.getTime(); isDone = false; jobState = Constants.JOB_STATE_NOT_YET_STARTED; jobThreadMutex = new Object(); logMutex = new Object(); jobThreads = new JobClass[threadsPerClient]; activeThreads = new ArrayList<JobClass>(threadsPerClient); logMessages = new ArrayList<String>(); checkForJobClass(); } /** * Creates a new client-side job that is only to be used as a standalone job * (i.e., to run without a server). * * @param messageWriter The message writer that will be used to write * messages regarding the progress of job * execution. * @param classPath The directory on the filesystem in which job * class files may be found. * @param jobClass The name of the Java class that should be * used to run this job. * @param threadsPerClient The number of threads to use to run the job. * @param duration The maximum length of time in seconds that * the job should be allowed to run. * @param collectionInterval The length of time in seconds that should be * used for the statistics collection interval. * @param parameters The list of job-specific parameters that * control how this job should operate. * @param useCustomClassLoader Indicates whether the custom job class loader * should be used to load job classes. * @param enableRealTimeStats Indicates whether this job should report * statistics in real-time. * @param statReporter The stat reporter that should be used to * report real-time statistics. */ public ClientSideJob(ClientMessageWriter messageWriter, String classPath, String jobClass, int threadsPerClient, int duration, int collectionInterval, ParameterList parameters, boolean useCustomClassLoader, boolean enableRealTimeStats, RealTimeStatReporter statReporter) { this.client = null; this.classPath = classPath; this.clientID = null; this.jobID = null; this.messageWriter = messageWriter; this.jobClass = jobClass; this.threadsPerClient = threadsPerClient; this.startTime = null; this.stopTime = null; this.clientNumber = 0; this.duration = duration; this.collectionInterval = collectionInterval; this.parameters = parameters; this.useCustomClassLoader = useCustomClassLoader; this.enableRealTimeStats = enableRealTimeStats; this.statReporter = statReporter; scheduledStartTime = System.currentTimeMillis(); isDone = false; jobState = Constants.JOB_STATE_NOT_YET_STARTED; jobThreadMutex = new Object(); logMutex = new Object(); jobThreads = new JobClass[threadsPerClient]; activeThreads = new ArrayList<JobClass>(threadsPerClient); logMessages = new ArrayList<String>(); } /** * Checks to see if the client has the specified class in its classpath. If * not, then request it from the server. */ private void checkForJobClass() { // See if we can load the specified job class. If so, then return // "success". Otherwise, request it from the server. try { if (useCustomClassLoader) { JobClassLoader jobClassLoader = new JobClassLoader(getClass().getClassLoader(), classPath); jobClassLoader.getJobClass(jobClass); return; } else { Class<?> jobClass = Constants.classForName(this.jobClass); JobClass instance = (JobClass) jobClass.newInstance(); return; } } catch (Exception e) { // We couldn't find the job class, so request it from the server. ClassTransferRequestMessage request = new ClassTransferRequestMessage(client.getMessageID(), jobClass); try { client.sendMessage(request); } catch (IOException ioe) { writeVerbose("Unable to send class transfer request for " + jobClass + ": " + ioe); } } } /** * Retrieves the job ID for this job. * * @return The job ID for this job. */ public String getJobID() { return jobID; } /** * Retrieves the name of the Java class that should be invoked to run this * job. * * @return The name of the Java class that should be invoked to run this job. */ public String getJobClass() { return jobClass; } /** * Retrieves the number of threads that should be started on each client * running this job. * * @return The number of threads that should be started on each client * running this job. */ public int getThreadsPerClient() { return threadsPerClient; } /** * Retrieves the time at which this job should start running. * * @return The time at which this job should start running. */ public Date getStartTime() { return startTime; } /** * Retrieves the time at which this job has been scheduled to start. * * @return The time at which this job has been scheduled to start. */ public long getScheduledStartTime() { return scheduledStartTime; } /** * Retrieves the time at which this job actually started running. * * @return The time at which this job actually started running. */ public long getActualStartTime() { return actualStartTime; } /** * Retrieves the time at which this job should stop running. * * @return The time at which this job should stop running. */ public Date getStopTime() { return stopTime; } /** * Retrieves the time at which this job actually stopped running. * * @return The time at which this job actually stopped running. */ public long getActualStopTime() { return actualStopTime; } /** * Retrieves the maximum length of time in seconds that this job should be * allowed to run. * * @return The maximum length of time in seconds that this job should be * allowed to run. */ public int getDuration() { return duration; } /** * Retrieves the total length of time in seconds that the job was running. * * @return The total length of time in seconds that the job was running. */ public int getActualDuration() { return (int) ((actualStopTime - actualStartTime) / 1000); } /** * Retrieves the length of time in seconds that should be used as the * statistics collection interval. * * @return The length of time in seconds that should be used as the * statistics collection interval. */ public int getCollectionInterval() { return collectionInterval; } /** * Indicates whether this job should collect statistical data in real time. * * @return <CODE>true</CODE> if the client should collect statistical data in * real time, or <CODE>false</CODE> if not. */ public boolean enableRealTimeStats() { return enableRealTimeStats; } /** * Retrieves the stat reporter that should be used to report real-time * statistical data. * * @return The stat reporter that should be used to report real-time * statistical data, or <CODE>null</CODE> if no reporting should be * done. */ public RealTimeStatReporter getStatReporter() { return statReporter; } /** * Retrieves the list of job-specific parameters associated with this job. * * @return The list of job-specific parameters associated with this job. */ public ParameterList getParameters() { return parameters; } /** * Starts processing on the job and waits until that processing is complete. * This method is intended for use by standalone clients that do not * actually communicate with a SLAMD server. * * @return The response code from the start operation. */ public int startAndWait() { int startResult = start(); if (startResult != Constants.MESSAGE_RESPONSE_SUCCESS) { return startResult; } while (! isDone) { try { Thread.sleep(100); } catch (InterruptedException ie) {} } return startResult; } /** * Attempts to start processing on the job. * * @return The result code from the attempted start operation. */ public synchronized int start() { // First, make sure that the job has not yet been started. If it has, then // exit. if (jobState != Constants.JOB_STATE_NOT_YET_STARTED) { return Constants.MESSAGE_RESPONSE_JOB_ALREADY_STARTED; } // Set the job state value to indicate that the job has stopped due to an // error. This isn't true, but if we abort the startup for some reason, // then it will be. If the startup succeeds, then this will be changed to // the appropriate value. jobState = Constants.JOB_STATE_STOPPED_DUE_TO_ERROR; // Get an instance of the job class. JobClass jobInstance; if (useCustomClassLoader) { JobClassLoader jobClassLoader = new JobClassLoader(getClass().getClassLoader(), classPath); try { jobInstance = jobClassLoader.getJobClass(jobClass); } catch (SLAMDException se) { logMessage(se.getMessage()); return Constants.MESSAGE_RESPONSE_CLASS_NOT_FOUND; } } else { try { Class<?> jobClass = Constants.classForName(this.jobClass); jobInstance = (JobClass) jobClass.newInstance(); } catch (Exception e) { logMessage(e.getMessage()); return Constants.MESSAGE_RESPONSE_CLASS_NOT_FOUND; } } // Perform client-level initialization for the job class. try { String clientID = ""; if (client != null) { clientID = client.getClientID(); } jobInstance.setClientNumber(clientNumber); jobInstance.setClientSideJob(this); StatPersistenceThread statPersistenceThread = Client.getStatPersistenceThread(); if (statPersistenceThread != null) { statPersistenceThread.setJob(this); } jobInstance.initializeClient(clientID, parameters); } catch (Exception e) { logMessage("Client-level initialization failed: " + JobClass.stackTraceToString(e)); return Constants.MESSAGE_RESPONSE_JOB_CREATION_FAILURE; } // The job is ready to be started, so create the appropriate number of // threads and start them. It is safe to assume that if an illegal access // or instantiation exception occurs as a result of this, it will be on the // first one and therefore there will be no cleanup necessary. jobThreads = new JobClass[threadsPerClient]; long threadStartTime = scheduledStartTime; for (int i=0; i < threadsPerClient; i++) { try { JobClass jobThread = jobInstance.getClass().newInstance(); String threadID = null; if (client != null) { threadID = client.getClientID() + '-' + i; } else { threadID = String.valueOf(i); } jobThread.setName("Job Thread " + jobID + ':' + threadID); jobThread.setClientNumber(clientNumber); jobThread.setThreadNumber(i); jobThread.initializeJobThread(clientID, threadID, collectionInterval, this, duration, stopTime, threadStartTime, parameters); jobThreads[i] = jobThread; activeThreads.add(jobThreads[i]); threadStartTime += threadStartupDelay; } catch (UnableToRunException utre) { logMessage("Unable to run exception while initializing job thread: " + JobClass.stackTraceToString(utre)); return Constants.MESSAGE_RESPONSE_JOB_CREATION_FAILURE; } catch (IllegalAccessException iae) { logMessage("Illegal access exception while initializing job thread: " + JobClass.stackTraceToString(iae)); return Constants.MESSAGE_RESPONSE_JOB_CREATION_FAILURE; } catch (InstantiationException ie) { logMessage("Instantiation exception while initializing job thread: " + JobClass.stackTraceToString(ie)); return Constants.MESSAGE_RESPONSE_JOB_CREATION_FAILURE; } catch (Exception e) { logMessage("Uncaught exception while initializing job thread: " + JobClass.stackTraceToString(e)); return Constants.MESSAGE_RESPONSE_JOB_CREATION_FAILURE; } } // Update the job state to indicate that it is running. Technically, it // isn't running yet, but this is close enough, and we'll change the state // if a failure occurs later in this method. jobState = Constants.JOB_STATE_RUNNING; actualStartTime = new Date().getTime(); // Iterate through all of the job threads and signal them to start for (int i=0; i < jobThreads.length; i++) { try { messageWriter.writeVerbose("Adding job thread " + jobThreads[i].getThreadID() + " to active list"); jobThreads[i].startJob(); } catch (AlreadyRunningException sare) { // This should never happen, but we still need to catch it. logMessage("Caught an already running exception: " + JobClass.stackTraceToString(sare)); jobState = Constants.JOB_STATE_STOPPED_DUE_TO_ERROR; return Constants.MESSAGE_RESPONSE_JOB_CREATION_FAILURE; } if (threadStartupDelay > 0) { try { Thread.sleep(threadStartupDelay); } catch (InterruptedException ie) {} } } // No problems encountered -- the jobs are starting up return Constants.MESSAGE_RESPONSE_SUCCESS; } /** * Attempts to stop processing on the job. * * @param stopReason The reason that the job is to be stopped. * * @return The result code from the attempted stop operation. */ public int stop(int stopReason) { synchronized (jobThreadMutex) { // If the job hasn't been started yet, then cancel it. This will prevent // it from being started. if (jobState == Constants.JOB_STATE_NOT_YET_STARTED) { jobState = Constants.JOB_STATE_CANCELLED; } // Is the job running? If so, then try to stop it. if (jobState == Constants.JOB_STATE_RUNNING) { for (int i=0; i < activeThreads.size(); i++) { JobClass jobThread = activeThreads.get(i); jobThread.stopJob(stopReason); jobState = stopReason; } } } return Constants.MESSAGE_RESPONSE_SUCCESS; } /** * Attempts to stop processing on the job and will not return until the * job state indicates that it is no longer running. * * @param stopReason The reason that the job is to be stopped. * * @return The result code from the attempted stop operation. */ public synchronized int stopAndWait(int stopReason) { // First, send all threads the stop signal stop(stopReason); // Now loop until the job state shows the job is no longer running while (jobState == Constants.JOB_STATE_RUNNING) { try { Thread.sleep(Constants.THREAD_BLOCK_SLEEP_TIME); } catch (InterruptedException ie) {} } jobState = stopReason; return Constants.MESSAGE_RESPONSE_SUCCESS; } /** * Attempts to forcefully stop execution of the job by interrupting the * thread. This should only be used if the job is not responding to normal * stop requests, most likely because it is blocked. * * @param stopReason The stop reason that should be used for the job. */ public final void forcefullyStop(int stopReason) { while (! activeThreads.isEmpty()) { JobClass jobThread = activeThreads.remove(0); messageWriter.writeVerbose("Forcefully removing thread " + jobThread.getThreadID() + " from active list"); try { jobThread.stopJob(stopReason); jobThread.interrupt(); Thread.sleep(100); } catch (Exception e) {} // Hopefully, the above interrupt worked. However, if it did not, then // try waiting a little while longer and interrupting again. We'll only // try to interrupt a thread twice before trying the job thread's destroy // method. If the job thread still won't die after the call to destroy // (which is possible, since destroy does nothing by default), then it // could be hung and require administrative action. if (jobThread.isAlive()) { try { Thread.sleep(1000); if (jobThread.isAlive()) { jobThread.interrupt(); Thread.sleep(100); if (jobThread.isAlive()) { jobThread.destroyThread(); } } } catch (Exception e) {} } } } /** * Retrieves the state of this job. * * @return The state of this job. */ public int getJobState() { return jobState; } /** * Specifies the job state to use for this job. * * @param jobState The job state to use for this job. */ public void setJobState(int jobState) { this.jobState = jobState; } /** * Retrieves the number of threads that are currently active. * * @return The number of threads that are currently active. */ public int getActiveThreadCount() { return activeThreads.size(); } /** * Retrieves the stat tracker information for this job. * * @param aggregateThreadData Indicates whether the data collected by each * thread should be aggregated before returning * the results. * * @return The stat tracker information for this job. */ public StatTracker[] getStatTrackers(boolean aggregateThreadData) { // If there are no job threads defined, then return an empty array if ((jobThreads == null) || (jobThreads.length == 0)) { return new StatTracker[0]; } if (aggregateThreadData) { // Create a linked hash map to hold named lists of the different kinds // of stat trackers while preserving the original order of the trackers. LinkedHashMap<String,ArrayList<StatTracker>> hashMap = new LinkedHashMap<String,ArrayList<StatTracker>>(); for (int i=0; i < jobThreads.length; i++) { StatTracker[] threadTrackers = jobThreads[i].getStatTrackers(); for (int j=0; j < threadTrackers.length; j++) { ArrayList<StatTracker> trackerList = hashMap.get(threadTrackers[j].getDisplayName()); if (trackerList == null) { trackerList = new ArrayList<StatTracker>(); trackerList.add(threadTrackers[j]); hashMap.put(threadTrackers[j].getDisplayName(), trackerList); } else { trackerList.add(threadTrackers[j]); } } } // Now get the values from the hash map and aggregate all the data into // a single array of stat trackers. Collection<ArrayList<StatTracker>> values = hashMap.values(); StatTracker[] aggregateTrackers = new StatTracker[values.size()]; Iterator<ArrayList<StatTracker>> iterator = values.iterator(); int i = 0; while (iterator.hasNext()) { ArrayList<StatTracker> trackerList = iterator.next(); StatTracker[] trackerArray = new StatTracker[trackerList.size()]; trackerList.toArray(trackerArray); aggregateTrackers[i] = trackerArray[0].newInstance(); aggregateTrackers[i].aggregate(trackerArray); aggregateTrackers[i].setThreadID("aggregated"); i++; } return aggregateTrackers; } else { // Create the array to return ArrayList<StatTracker> trackerList = new ArrayList<StatTracker>(); for (int i=0; i < jobThreads.length; i++) { StatTracker[] threadTrackers = jobThreads[i].getStatTrackers(); for (int j=0; j < threadTrackers.length; j++) { trackerList.add(threadTrackers[j]); } } // Return the set of status counters StatTracker[] trackers = new StatTracker[trackerList.size()]; trackerList.toArray(trackers); return trackers; } } /** * Retrieves an array containing all of the messages that have been logged * for this job. * * @return An array containing all of the messages that have been logged for * this job. */ public String[] getLogMessages() { ArrayList clone = (ArrayList) logMessages.clone(); String[] messageArray = new String[clone.size()]; for (int i=0; i < messageArray.length; i++) { messageArray[i] = (String) clone.get(i); } return messageArray; } /** * Writes a new message about this job into the list of messages to log. Note * that the messages will not actually be logged into the SLAMD server's log * until they are sent back to the server in a status response or job * completed message in order to cut down on the network traffic and reduce * latency associated with job processing. * * @param message The message to be logged. */ private void logMessage(String message) { synchronized (logMutex) { logMessages.add(message); } messageWriter.writeMessage(message); } /** * Writes the provided message to the client message writer using the verbose * setting (so the message will not be displayed if the client is not * operating in verbose mode). The specified message will not be written to * the SLAMD log. * * @param message The message to be written. */ public void writeVerbose(String message) { messageWriter.writeVerbose(message); } /** * Writes a new message from the specified thread into the list of messages to * log. Note that the messages will not actually be logged into the SLAMD * server's log until they are sent back to the server in a status response or * job completed message in order to cut down on the network traffic and * reduce latency associated with job processing. * * @param threadID The ID of the thread logging this message. * @param message The message to be logged. */ public void logMessage(String threadID, String message) { StringBuilder messageBuffer = new StringBuilder(); // The date formatter is not threadsafe, so we need to protect access to it. synchronized (logMutex) { messageBuffer.append('['); messageBuffer.append(dateFormat.format(new Date())); messageBuffer.append(']'); } messageBuffer.append(" - "); messageBuffer.append( Constants.logLevelToString(Constants.LOG_LEVEL_JOB_PROCESSING)); messageBuffer.append(" - "); if (client != null) { messageBuffer.append("client="); messageBuffer.append(client.clientID); } if (jobID != null) { messageBuffer.append(" job="); messageBuffer.append(jobID); } messageBuffer.append(" - "); messageBuffer.append(message); synchronized (logMutex) { logMessages.add(messageBuffer.toString()); } messageWriter.writeMessage(message); } /** * Indicates that the specified thread is no longer running. This removes the * thread from the active thread list and once all threads have completed * performs the appropriate job cleanup work. * * @param jobThread The job thread that has finished. */ public void threadDone(JobClass jobThread) { synchronized (jobThreadMutex) { // Remove it from the list of active threads for (int i=0; i < activeThreads.size(); i++) { if (jobThread.getThreadID().equals(activeThreads.get(i).getThreadID())) { messageWriter.writeVerbose("Removing thread " + jobThread.getThreadID() + " from active list"); activeThreads.remove(i); break; } } // See if the active thread list is empty. If so, then the whole job is // done. if (activeThreads.isEmpty()) { messageWriter.writeVerbose("All job threads have completed"); isDone = true; actualStopTime = new Date().getTime(); // Run the per-client finalization try { jobThread.finalizeClient(); } catch (Exception e) { messageWriter.writeVerbose("ERROR running per-client " + "finalization: " + e); } if (client != null) { client.jobDone(); } } } } /** * Indicates whether this job has been completed or is otherwise stopped for * some reason. A job will not be considered done if it was never started. * * @return <CODE>true</CODE> if the job is done, or <CODE>false</CODE> if * not. */ public boolean isDone() { return isDone; } }