All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.grinder.engine.agent.AgentImplementation Maven / Gradle / Ivy

The newest version!
// Copyright (C) 2000 Paco Gomez
// Copyright (C) 2000 - 2012 Philip Aston
// Copyright (C) 2004 Bertrand Ave
// Copyright (C) 2008 Pawel Lacinski
// All rights reserved.
//
// This file is part of The Grinder software distribution. Refer to
// the file LICENSE which is part of The Grinder distribution for
// licensing details. The Grinder distribution is available on the
// Internet at http://grinder.sourceforge.net/
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
// COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
// OF THE POSSIBILITY OF SUCH DAMAGE.

package net.grinder.engine.agent;

import java.io.File;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Timer;
import java.util.TimerTask;

import net.grinder.common.GrinderBuild;
import net.grinder.common.GrinderException;
import net.grinder.common.GrinderProperties;
import net.grinder.common.GrinderProperties.PersistenceException;
import net.grinder.common.processidentity.ProcessReport;
import net.grinder.communication.ClientReceiver;
import net.grinder.communication.ClientSender;
import net.grinder.communication.CommunicationException;
import net.grinder.communication.ConnectionType;
import net.grinder.communication.Connector;
import net.grinder.communication.FanOutStreamSender;
import net.grinder.communication.IgnoreShutdownSender;
import net.grinder.communication.MessageDispatchSender;
import net.grinder.communication.MessagePump;
import net.grinder.communication.TeeSender;
import net.grinder.engine.common.ConnectorFactory;
import net.grinder.engine.common.EngineException;
import net.grinder.engine.common.ScriptLocation;
import net.grinder.engine.communication.ConsoleListener;
import net.grinder.messages.agent.StartGrinderMessage;
import net.grinder.messages.console.AgentAddress;
import net.grinder.messages.console.AgentProcessReportMessage;
import net.grinder.util.Directory;
import net.grinder.util.thread.Condition;

import org.slf4j.Logger;


/**
 * This is the entry point of The Grinder agent process.
 *
 * @author Paco Gomez
 * @author Philip Aston
 * @author Bertrand Ave
 * @author Pawel Lacinski
 */
public final class AgentImplementation implements Agent {

  private final Logger m_logger;
  private final File m_alternateFile;
  private final boolean m_proceedWithoutConsole;

  private final Timer m_timer = new Timer(true);
  private final Condition m_eventSynchronisation = new Condition();
  private final AgentIdentityImplementation m_agentIdentity;
  private final ConsoleListener m_consoleListener;
  private final FanOutStreamSender m_fanOutStreamSender =
    new FanOutStreamSender(3);
  private final ConnectorFactory m_connectorFactory =
    new ConnectorFactory(ConnectionType.AGENT);

  /**
   * We use an most one file store throughout an agent's life, but can't
   * initialise it until we've read the properties and connected to the console.
   */
  private volatile FileStore m_fileStore;

  /**
   * Constructor.
   *
   * @param logger Logger.
   * @param alternateFile Alternative properties file, or null.
   * @param proceedWithoutConsole true => proceed if a console
   * connection could not be made.
   * @throws GrinderException If an error occurs.
   */
  public AgentImplementation(Logger logger,
                             File alternateFile,
                             boolean proceedWithoutConsole)
    throws GrinderException {

    m_logger = logger;
    m_alternateFile = alternateFile;
    m_proceedWithoutConsole = proceedWithoutConsole;

    m_consoleListener = new ConsoleListener(m_eventSynchronisation, m_logger);
    m_agentIdentity = new AgentIdentityImplementation(getHostName());
  }

  /**
   * Run the Grinder agent process.
   *
   * @throws GrinderException
   *             If an error occurs.
   */
  @Override
  public void run() throws GrinderException {

    StartGrinderMessage startMessage = null;
    ConsoleCommunication consoleCommunication = null;

    try {
      while (true) {
        m_logger.info(GrinderBuild.getName());

        ScriptLocation script = null;
        GrinderProperties properties;

        do {
          properties =
            createAndMergeProperties(startMessage != null ?
                                     startMessage.getProperties() : null);

          m_agentIdentity.setName(
            properties.getProperty("grinder.hostID", getHostName()));

          final Connector connector =
            properties.getBoolean("grinder.useConsole", true) ?
            m_connectorFactory.create(properties) : null;

          // We only reconnect if the connection details have changed.
          if (consoleCommunication != null &&
              !consoleCommunication.getConnector().equals(connector)) {
            shutdownConsoleCommunication(consoleCommunication);
            consoleCommunication = null;
            // Accept any startMessage from previous console - see bug 2092881.
          }

          if (consoleCommunication == null && connector != null) {
            try {
              consoleCommunication = new ConsoleCommunication(connector);
              consoleCommunication.start();
              m_logger.info(
                "connected to console at {}", connector.getEndpointAsString());
            }
            catch (final CommunicationException e) {
              if (m_proceedWithoutConsole) {
                m_logger.warn(
                  "{}, proceeding without the console; set " +
                  "grinder.useConsole=false to disable this warning.",
                  e.getMessage());
              }
              else {
                m_logger.error(e.getMessage());
                return;
              }
            }
          }

          if (consoleCommunication != null && startMessage == null) {
            m_logger.info("waiting for console signal");
            m_consoleListener.waitForMessage();

            if (m_consoleListener.received(ConsoleListener.START)) {
              startMessage = m_consoleListener.getLastStartGrinderMessage();
              continue; // Loop to handle new properties.
            }
            else {
              break;    // Another message, check at end of outer while loop.
            }
          }

          if (startMessage != null) {
            final GrinderProperties messageProperties =
              startMessage.getProperties();
            final Directory fileStoreDirectory = m_fileStore.getDirectory();

            if (messageProperties.getAssociatedFile() != null) {
              // If the properties is associated with a file in the file store
              // we rebase it so the script location is calculated relative to
              // the file.

              messageProperties.setAssociatedFile(
                fileStoreDirectory.getFile(
                  messageProperties.getAssociatedFile()));

              final File consoleScript =
                  messageProperties.resolveRelativeFile(
                    messageProperties.getFile(
                      GrinderProperties.SCRIPT,
                      GrinderProperties.DEFAULT_SCRIPT));

              // We fall back to the agent properties if the start message
              // doesn't specify a script and there is no default script.
              if (messageProperties.containsKey(GrinderProperties.SCRIPT) ||
                  consoleScript.canRead()) {
                // The script directory may not be the file's direct parent.
                script = new ScriptLocation(fileStoreDirectory, consoleScript);
              }
            }

            m_agentIdentity.setNumber(startMessage.getAgentNumber());
          }
          else {
            m_agentIdentity.setNumber(-1);
          }

          if (script == null) {
            final File scriptFile =
              properties.resolveRelativeFile(
                properties.getFile(GrinderProperties.SCRIPT,
                                   GrinderProperties.DEFAULT_SCRIPT));

            script = new ScriptLocation(scriptFile);
          }

          if (!script.getFile().canRead()) {
            m_logger.error("The script file '" + script +
                           "' does not exist or is not readable.");
            script = null;
            break;
          }
        }
        while (script == null);

        if (script != null) {
          final String jvmArguments =
            properties.getProperty("grinder.jvm.arguments");

          final WorkerFactory workerFactory;

          if (!properties.getBoolean("grinder.debug.singleprocess", false)) {

            final WorkerProcessCommandLine workerCommandLine =
              new WorkerProcessCommandLine(properties,
                                           System.getProperties(),
                                           jvmArguments,
                                           script.getDirectory());

            m_logger.info("Worker process command line: {}", workerCommandLine);

            workerFactory =
              new ProcessWorkerFactory(
                workerCommandLine, m_agentIdentity, m_fanOutStreamSender,
                consoleCommunication != null, script, properties);
          }
          else {
            m_logger.info(
              "DEBUG MODE: Spawning threads rather than processes");

            if (jvmArguments != null) {
              m_logger.warn(
                "grinder.jvm.arguments ({}) ignored in single process mode",
                jvmArguments);
            }

            workerFactory =
              new DebugThreadWorkerFactory(
                m_agentIdentity, m_fanOutStreamSender,
                consoleCommunication != null, script, properties);
          }

          final WorkerLauncher workerLauncher =
            new WorkerLauncher(properties.getInt("grinder.processes", 1),
                               workerFactory,
                               m_eventSynchronisation,
                               m_logger);

          final int increment =
            properties.getInt("grinder.processIncrement", 0);

          if (increment > 0) {
            final boolean moreProcessesToStart =
              workerLauncher.startSomeWorkers(
                properties.getInt("grinder.initialProcesses", increment));

            if (moreProcessesToStart) {
              final int incrementInterval =
                properties.getInt("grinder.processIncrementInterval", 60000);

              final RampUpTimerTask rampUpTimerTask =
                new RampUpTimerTask(workerLauncher, increment);

              m_timer.scheduleAtFixedRate(
                rampUpTimerTask, incrementInterval, incrementInterval);
            }
          }
          else {
            workerLauncher.startAllWorkers();
          }

          // Wait for a termination event.
          synchronized (m_eventSynchronisation) {
            final long maximumShutdownTime = 20000;
            long consoleSignalTime = -1;

            while (!workerLauncher.allFinished()) {
              if (consoleSignalTime == -1 &&
                  m_consoleListener.checkForMessage(ConsoleListener.ANY ^
                                                    ConsoleListener.START)) {
                workerLauncher.dontStartAnyMore();
                consoleSignalTime = System.currentTimeMillis();
              }

              if (consoleSignalTime >= 0 &&
                  System.currentTimeMillis() - consoleSignalTime >
                  maximumShutdownTime) {

                m_logger.info("forcibly terminating unresponsive processes");

                // destroyAllWorkers() prevents further workers from starting.
                workerLauncher.destroyAllWorkers();
              }

              m_eventSynchronisation.waitNoInterrruptException(
                maximumShutdownTime);
            }
          }

          workerLauncher.shutdown();
        }

        if (consoleCommunication == null) {
          break;
        }
        else {
          // Ignore any pending start messages.
          m_consoleListener.discardMessages(ConsoleListener.START);

          if (!m_consoleListener.received(ConsoleListener.ANY)) {
            // We've got here naturally, without a console signal.
            m_logger.info("finished, waiting for console signal");
            m_consoleListener.waitForMessage();
          }

          if (m_consoleListener.received(ConsoleListener.START)) {
            startMessage = m_consoleListener.getLastStartGrinderMessage();
          }
          else if (m_consoleListener.received(ConsoleListener.STOP |
                                              ConsoleListener.SHUTDOWN)) {
            break;
          }
          else {
            // ConsoleListener.RESET or natural death.
            startMessage = null;
          }
        }
      }
    }
    finally {
      shutdownConsoleCommunication(consoleCommunication);
    }
  }

  private GrinderProperties createAndMergeProperties(
      GrinderProperties startMessageProperties)
    throws PersistenceException {

    final GrinderProperties properties =
      new GrinderProperties(
        m_alternateFile != null ?
            m_alternateFile : GrinderProperties.DEFAULT_PROPERTIES);

    if (startMessageProperties != null) {
      properties.putAll(startMessageProperties);
    }

    // Ensure the log directory property is set and is absolute.
    final File nullFile = new File("");

    final File originalLogDirectory =
      properties.getFile(GrinderProperties.LOG_DIRECTORY, nullFile);

    if (!originalLogDirectory.isAbsolute()) {
      properties.setFile(GrinderProperties.LOG_DIRECTORY,
        new File(nullFile.getAbsoluteFile(), originalLogDirectory.getPath()));
    }

    return properties;
  }

  private void shutdownConsoleCommunication(
    ConsoleCommunication consoleCommunication) {

    if (consoleCommunication != null) {
      consoleCommunication.shutdown();
    }

    m_consoleListener.discardMessages(ConsoleListener.ANY);
  }

  /**
   * Clean up resources.
   */
  @Override
  public void shutdown() {
    m_timer.cancel();
    m_fanOutStreamSender.shutdown();
    m_consoleListener.shutdown();

    m_logger.info("finished");
  }

  private static String getHostName() {
    try {
      return InetAddress.getLocalHost().getHostName();
    }
    catch (final UnknownHostException e) {
      return "UNNAMED HOST";
    }
  }

  private static class RampUpTimerTask extends TimerTask {

    private final WorkerLauncher m_processLauncher;
    private final int m_processIncrement;

    public RampUpTimerTask(WorkerLauncher processLauncher,
                           int processIncrement) {
      m_processLauncher = processLauncher;
      m_processIncrement = processIncrement;
    }

    @Override
    public void run() {
      try {
        final boolean moreProcessesToStart =
          m_processLauncher.startSomeWorkers(m_processIncrement);

        if (!moreProcessesToStart) {
          super.cancel();
        }
      }
      catch (final EngineException e) {
        // Really an assertion. Can't use logger because its not thread-safe.
        System.err.println("Failed to start processes");
        e.printStackTrace();
      }
    }
  }

  private final class ConsoleCommunication {
    private final ClientSender m_sender;
    private final Connector m_connector;
    private final TimerTask m_reportRunningTask;
    private final MessagePump m_messagePump;

    public ConsoleCommunication(Connector connector)
        throws CommunicationException, FileStore.FileStoreException {

      final ClientReceiver receiver =
        ClientReceiver.connect(connector, new AgentAddress(m_agentIdentity));
      m_sender = ClientSender.connect(receiver);
      m_connector = connector;

      if (m_fileStore == null) {
        // Only create the file store if we connected.
        m_fileStore =
          new FileStore(
            new File("./" + m_agentIdentity.getName() + "-file-store"),
            m_logger);
      }

      m_sender.send(
        new AgentProcessReportMessage(ProcessReport.State.STARTED,
                                      m_fileStore.getCacheHighWaterMark()));

      final MessageDispatchSender fileStoreMessageDispatcher =
        new MessageDispatchSender();
      m_fileStore.registerMessageHandlers(fileStoreMessageDispatcher);

      final MessageDispatchSender messageDispatcher =
        new MessageDispatchSender();
      m_consoleListener.registerMessageHandlers(messageDispatcher);

      // Everything that the file store doesn't handle is tee'd to the
      // worker processes and our message handlers.
      fileStoreMessageDispatcher.addFallback(
        new TeeSender(messageDispatcher,
                      new IgnoreShutdownSender(m_fanOutStreamSender)));

      m_messagePump =
        new MessagePump(receiver, fileStoreMessageDispatcher, 1);

      m_reportRunningTask = new TimerTask() {
        @Override
        public void run() {
          try {
            m_sender.send(
              new AgentProcessReportMessage(
                ProcessReport.State.RUNNING,
                m_fileStore.getCacheHighWaterMark()));
          }
          catch (final CommunicationException e) {
            cancel();
            e.printStackTrace();
          }
        }
      };
    }

    public void start() {
      m_messagePump.start();
      m_timer.schedule(m_reportRunningTask, 1000, 1000);
    }

    public Connector getConnector() {
      return m_connector;
    }

    public void shutdown() {
      m_reportRunningTask.cancel();

      try {
        m_sender.send(
          new AgentProcessReportMessage(
            ProcessReport.State.FINISHED,
            m_fileStore.getCacheHighWaterMark()));
      }
      catch (final CommunicationException e) {
        // Ignore - peer has probably shut down.
      }
      finally {
        m_messagePump.shutdown();
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy