/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.grinder;
import net.grinder.AgentDaemon.AgentShutDownListener;
import net.grinder.common.GrinderException;
import net.grinder.common.GrinderProperties;
import net.grinder.communication.*;
import net.grinder.engine.agent.Agent;
import net.grinder.engine.common.AgentControllerConnectorFactory;
import net.grinder.engine.communication.AgentControllerServerListener;
import net.grinder.engine.communication.AgentDownloadGrinderMessage;
import net.grinder.engine.communication.AgentUpdateGrinderMessage;
import net.grinder.engine.communication.LogReportGrinderMessage;
import net.grinder.engine.controller.AgentControllerIdentityImplementation;
import net.grinder.message.console.AgentControllerProcessReportMessage;
import net.grinder.message.console.AgentControllerState;
import net.grinder.messages.agent.StartGrinderMessage;
import net.grinder.messages.console.AgentAddress;
import net.grinder.util.LogCompressUtils;
import net.grinder.util.NetworkUtils;
import net.grinder.util.thread.Condition;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ArrayUtils;
import org.ngrinder.common.constants.AgentConstants;
import org.ngrinder.infra.AgentConfig;
import org.ngrinder.monitor.collector.SystemDataCollector;
import org.ngrinder.monitor.controller.model.SystemDataModel;
import org.ngrinder.monitor.share.domain.SystemInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FilenameFilter;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import static org.ngrinder.common.constants.InternalConstants.PROP_INTERNAL_NGRINDER_VERSION;
import static org.ngrinder.common.util.NoOp.noOp;
import static org.ngrinder.common.util.Preconditions.checkNotNull;
/**
* Agent Controller which handles agent start and stop.
*
* @author JunHo Yoon
* @since 3.0
*/
public class AgentController implements Agent, AgentConstants {
private static final Logger LOGGER = LoggerFactory.getLogger("agent controller");
private final AgentConfig agentConfig;
private Timer m_timer;
@SuppressWarnings("FieldCanBeLocal")
private final Condition m_eventSynchronization = new Condition();
private final AgentControllerIdentityImplementation m_agentIdentity;
private final AgentControllerServerListener m_agentControllerServerListener;
private FanOutStreamSender m_fanOutStreamSender;
private final AgentControllerConnectorFactory m_connectorFactory = new AgentControllerConnectorFactory(
ConnectionType.AGENT);
private final Condition m_eventSyncCondition;
private volatile AgentControllerState m_state = AgentControllerState.STARTED;
private SystemDataCollector agentSystemDataCollector = new SystemDataCollector();
private int m_connectionPort = 0;
private static SystemDataModel emptySystemDataModel = new SystemDataModel();
private AgentUpdateHandler agentUpdateHandler;
private int retryCount = 0;
private String version;
/**
* Constructor.
*
* @param eventSyncCondition event sync condition to wait until agent start to run.
*/
public AgentController(Condition eventSyncCondition, AgentConfig agentConfig) throws GrinderException {
this.m_eventSyncCondition = eventSyncCondition;
this.agentConfig = agentConfig;
this.version = agentConfig.getInternalProperties().getProperty(PROP_INTERNAL_NGRINDER_VERSION);
this.m_agentControllerServerListener = new AgentControllerServerListener(m_eventSynchronization, LOGGER);
// Set it with the default name
this.m_agentIdentity = new AgentControllerIdentityImplementation(agentConfig.getAgentHostID(), NetworkUtils.DEFAULT_LOCAL_HOST_ADDRESS);
this.m_agentIdentity.setRegion(agentConfig.getRegion());
this.agentSystemDataCollector = new SystemDataCollector();
this.agentSystemDataCollector.setAgentHome(agentConfig.getHome().getDirectory());
this.agentSystemDataCollector.refresh();
}
/**
* Run the agent controller.
*
* @throws GrinderException occurs when the test execution is failed.
*/
@SuppressWarnings("ConstantConditions")
public void run() throws GrinderException {
synchronized (m_eventSyncCondition) {
m_eventSyncCondition.notifyAll();
}
StartGrinderMessage startMessage = null;
ConsoleCommunication consoleCommunication = null;
m_fanOutStreamSender = new FanOutStreamSender(GrinderConstants.AGENT_CONTROLLER_FANOUT_STREAM_THREAD_COUNT);
m_timer = new Timer(false);
AgentDaemon agent = new AgentDaemon(checkNotNull(agentConfig,
"agent.conf should be provided before agent daemon start."));
try {
while (true) {
do {
if (consoleCommunication == null) {
final Connector connector = m_connectorFactory.create(agentConfig.getControllerIP(), agentConfig.getControllerPort());
try {
consoleCommunication = new ConsoleCommunication(connector);
consoleCommunication.start();
LOGGER.info("Connected to agent controller server at {}", connector.getEndpointAsString());
} catch (CommunicationException e) {
LOGGER.error("Error while connecting to agent controller server at {}",
connector.getEndpointAsString());
return;
}
}
if (consoleCommunication != null && startMessage == null) {
if (m_state == AgentControllerState.UPDATING) {
m_agentControllerServerListener.waitForMessage();
break;
} else {
LOGGER.info("Waiting for agent controller server signal");
m_state = AgentControllerState.READY;
m_agentControllerServerListener.waitForMessage();
if (m_agentControllerServerListener.received(AgentControllerServerListener.START)) {
startMessage = m_agentControllerServerListener.getLastStartGrinderMessage();
LOGGER.info("Agent start message is received from controller {}", startMessage);
continue;
} else {
break; // Another message, check at end of outer
// while loop.
}
}
}
if (startMessage != null) {
m_agentIdentity.setNumber(startMessage.getAgentNumber());
}
} while (checkNotNull(startMessage).getProperties() == null);
// Here the agent run code goes..
if (startMessage != null) {
final String testId = startMessage.getProperties().getProperty("grinder.test.id", "unknown");
LOGGER.info("Starting agent... for {}", testId);
m_state = AgentControllerState.BUSY;
m_connectionPort = startMessage.getProperties().getInt(GrinderProperties.CONSOLE_PORT, 0);
agent.run(startMessage.getProperties());
final ConsoleCommunication conCom = consoleCommunication;
agent.resetListeners();
agent.addListener(new AgentShutDownListener() {
@Override
public void shutdownAgent() {
LOGGER.info("Send log for {}", testId);
sendLog(conCom, testId);
m_state = AgentControllerState.READY;
m_connectionPort = 0;
}
});
}
// Ignore any pending start messages.
m_agentControllerServerListener.discardMessages(AgentControllerServerListener.START);
if (!m_agentControllerServerListener.received(AgentControllerServerListener.ANY)) {
// We've got here naturally, without a console signal.
LOGGER.info("Agent is started. Waiting for agent controller signal");
m_agentControllerServerListener.waitForMessage();
}
if (m_agentControllerServerListener.received(AgentControllerServerListener.START)) {
startMessage = m_agentControllerServerListener.getLastStartGrinderMessage();
} else if (m_agentControllerServerListener.received(AgentControllerServerListener.STOP)) {
agent.shutdown();
startMessage = null;
m_connectionPort = 0;
m_agentControllerServerListener.discardMessages(AgentControllerServerListener.STOP);
} else if (m_agentControllerServerListener.received(AgentControllerServerListener.SHUTDOWN)) {
m_connectionPort = 0;
break;
} else if (m_agentControllerServerListener.received(AgentControllerServerListener.AGENT_UPDATE)) {
// Do update agent by downloading new version.
startMessage = null;
m_connectionPort = 0;
m_state = AgentControllerState.UPDATING;
final AgentUpdateGrinderMessage message = m_agentControllerServerListener.getLastAgentUpdateGrinderMessage();
m_agentControllerServerListener.discardMessages(AgentControllerServerListener.AGENT_UPDATE);
AgentDownloadGrinderMessage agentDownloadGrinderMessage = new AgentDownloadGrinderMessage(message.getVersion());
try {
// If it's initial message
if (agentUpdateHandler == null && message.getNext() == 0) {
IOUtils.closeQuietly(agentUpdateHandler);
agentUpdateHandler = new AgentUpdateHandler(agentConfig, message);
} else if (agentUpdateHandler != null) {
if (message.isValid()) {
retryCount = 0;
agentUpdateHandler.update(message);
agentDownloadGrinderMessage.setNext(message.getNext());
} else if (retryCount <= AgentDownloadGrinderMessage.MAX_RETRY_COUNT) {
retryCount++;
agentDownloadGrinderMessage.setNext(message.getOffset());
} else {
throw new CommunicationException("Error while getting the agent package from " +
"controller");
}
} else {
throw new CommunicationException("Error while getting the agent package from controller");
}
if (consoleCommunication != null) {
consoleCommunication.sendMessage(agentDownloadGrinderMessage);
} else {
break;
}
} catch (IllegalArgumentException ex) {
IOUtils.closeQuietly(agentUpdateHandler);
agentUpdateHandler = null;
retryCount = 0;
LOGGER.info("same or old agent version {} is sent for update. skip this.",
message.getVersion());
m_state = AgentControllerState.READY;
} catch (Exception e) {
retryCount = 0;
IOUtils.closeQuietly(agentUpdateHandler);
agentUpdateHandler = null;
LOGGER.error("While updating agent, the exception occurred.", e);
m_state = AgentControllerState.READY;
}
} else {
// ConsoleListener.RESET or natural death.
startMessage = null;
}
}
} finally {
m_connectionPort = 0;
// Abnormal state.
agent.shutdown();
m_state = AgentControllerState.FINISHED;
shutdownConsoleCommunication(consoleCommunication);
m_timer.cancel();
}
}
private void sendLog(ConsoleCommunication consoleCommunication, String testId) {
File logFolder = new File(agentConfig.getHome().getLogDirectory(), testId);
if (!logFolder.exists()) {
return;
}
File[] logFiles = logFolder.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return (name.endsWith(".log"));
}
});
if (logFiles == null || ArrayUtils.isEmpty(logFiles)) {
LOGGER.error("No log exists under {}", logFolder.getAbsolutePath());
return;
}
Arrays.sort(logFiles);
// Take only one file... if agent.send.all.logs is not set.
if (!agentConfig.getAgentProperties().getPropertyBoolean(PROP_AGENT_ALL_LOGS)) {
logFiles = new File[]{logFiles[0]};
}
final byte[] compressedLog = LogCompressUtils.compress(logFiles,
Charset.defaultCharset(), Charset.forName("UTF-8")
);
consoleCommunication.sendMessage(new LogReportGrinderMessage(testId, compressedLog, new AgentAddress(m_agentIdentity)));
// Delete logs to clean up
if (!agentConfig.getAgentProperties().getPropertyBoolean(PROP_AGENT_KEEP_LOGS)) {
LOGGER.info("Clean up the perftest logs");
FileUtils.deleteQuietly(logFolder);
}
}
private void shutdownConsoleCommunication(ConsoleCommunication consoleCommunication) {
sendCurrentState(consoleCommunication);
if (consoleCommunication != null) {
consoleCommunication.shutdown();
//noinspection UnusedAssignment
consoleCommunication = null;
}
m_agentControllerServerListener.discardMessages(AgentControllerServerListener.ANY);
}
private void sendCurrentState(ConsoleCommunication consoleCommunication) {
if (consoleCommunication != null) {
try {
consoleCommunication.sendCurrentState();
} catch (CommunicationException e) {
LOGGER.error("Error while sending current state : {}.", e.getMessage());
LOGGER.debug("The error detail is ", e);
}
}
}
/**
* Clean up resources.
*/
public void shutdown() {
if (m_timer != null) {
m_timer.cancel();
}
if (m_fanOutStreamSender != null) {
m_fanOutStreamSender.shutdown();
}
m_agentControllerServerListener.shutdown();
LOGGER.info("Agent controller shuts down");
}
/**
* Get current System performance.
*
* @return {@link SystemDataModel} instance
*/
public SystemDataModel getSystemDataModel() {
try {
SystemInfo systemInfo = agentSystemDataCollector.execute();
return new SystemDataModel(systemInfo, this.version);
} catch (Exception e) {
LOGGER.error("Error while getting system data model : {} ", e.getMessage());
LOGGER.debug("The error detail is ", e);
return emptySystemDataModel;
}
}
public AgentConfig getAgentConfig() {
return agentConfig;
}
public final class ConsoleCommunication {
private final ClientSender m_sender;
private final TimerTask m_reportRunningTask;
private final MessagePump m_messagePump;
public ConsoleCommunication(Connector connector) throws CommunicationException {
final ClientReceiver receiver = ClientReceiver.connect(connector, new AgentAddress(m_agentIdentity));
m_sender = ClientSender.connect(receiver);
m_sender.send(new AgentControllerProcessReportMessage(AgentControllerState.STARTED, getSystemDataModel(),
m_connectionPort, version));
final MessageDispatchSender messageDispatcher = new MessageDispatchSender();
m_agentControllerServerListener.registerMessageHandlers(messageDispatcher);
m_messagePump = new MessagePump(receiver, messageDispatcher, 1);
m_reportRunningTask = new TimerTask() {
public void run() {
try {
sendCurrentState();
} catch (CommunicationException e) {
cancel();
LOGGER.error("Error while sending current state:" + e.getMessage());
LOGGER.debug("The error detail is", e);
}
}
};
}
public void sendMessage(Message message) {
try {
m_sender.send(message);
} catch (CommunicationException e) {
LOGGER.error("{}. This error is not critical if it doesn't occur much.", e.getMessage());
}
}
public void sendCurrentState() throws CommunicationException {
sendMessage(new AgentControllerProcessReportMessage(m_state, getSystemDataModel(), m_connectionPort, version));
}
public void start() {
m_messagePump.start();
m_timer.schedule(m_reportRunningTask, 0, GrinderConstants.AGENT_CONTROLLER_HEARTBEAT_INTERVAL);
}
public void shutdown() {
m_reportRunningTask.cancel();
try {
m_sender.send(new AgentControllerProcessReportMessage(AgentControllerState.FINISHED, null, 0, version));
} catch (CommunicationException e) {
// Fall through
// Ignore - peer has probably shut down.
noOp();
} finally {
m_messagePump.shutdown();
}
}
}
}