/*
* 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.console.communication;
import net.grinder.common.GrinderProperties;
import net.grinder.common.processidentity.AgentIdentity;
import net.grinder.common.processidentity.ProcessIdentity;
import net.grinder.communication.CommunicationException;
import net.grinder.communication.MessageDispatchRegistry;
import net.grinder.communication.MessageDispatchRegistry.AbstractHandler;
import net.grinder.engine.communication.AgentDownloadGrinderMessage;
import net.grinder.engine.communication.AgentUpdateGrinderMessage;
import net.grinder.engine.communication.LogReportGrinderMessage;
import net.grinder.message.console.AgentControllerProcessReportMessage;
import net.grinder.message.console.AgentControllerState;
import net.grinder.messages.agent.StartGrinderMessage;
import net.grinder.messages.agent.StopGrinderMessage;
import net.grinder.messages.console.AgentAddress;
import net.grinder.util.ListenerSupport;
import net.grinder.util.ListenerSupport.Informer;
import org.ngrinder.monitor.controller.model.SystemDataModel;
import org.python.google.common.base.Predicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import static org.ngrinder.common.util.CollectionUtils.newLinkedHashSet;
/**
* Implementation of {@link AgentProcessControl}.
*
* @author JunHo Yoon
*/
@SuppressWarnings("SynchronizeOnNonFinalField")
public class AgentProcessControlImplementation implements AgentProcessControl {
private final ConsoleCommunication m_consoleCommunication;
private Map<AgentIdentity, AgentStatus> m_agentMap = new ConcurrentHashMap<AgentIdentity, AgentStatus>();
private final ListenerSupport<Listener> m_listeners = new ListenerSupport<Listener>();
private final ListenerSupport<LogArrivedListener> m_logListeners = new ListenerSupport<LogArrivedListener>();
private AgentDownloadRequestListener m_agentDownloadListener;
private static final Logger LOGGER = LoggerFactory.getLogger(AgentProcessControlImplementation.class);
/**
* Period at which to update the listeners.
*/
private static final long UPDATE_PERIOD = 500;
/**
* We keep a record of processes for a few seconds after they have been terminated.
* <p/>
* Every FLUSH_PERIOD, process statuses are checked. Those haven't reported for a while are
* marked and are discarded if they still haven't been updated by the next FLUSH_PERIOD.
*/
private static final long FLUSH_PERIOD = 2000;
private volatile boolean m_newData = false;
/**
* Constructor.
*
* @param timer Timer that can be used to schedule housekeeping tasks.
* @param consoleCommunication The console communication handler.
*/
public AgentProcessControlImplementation(Timer timer, ConsoleCommunication consoleCommunication) {
m_consoleCommunication = consoleCommunication;
timer.schedule(new TimerTask() {
public void run() {
update();
}
}, 0, UPDATE_PERIOD);
timer.schedule(new TimerTask() {
public void run() {
synchronized (m_agentMap) {
purge(m_agentMap);
}
}
}, 0, FLUSH_PERIOD);
final MessageDispatchRegistry messageDispatchRegistry = consoleCommunication.getMessageDispatchRegistry();
messageDispatchRegistry.set(AgentControllerProcessReportMessage.class,
new AbstractHandler<AgentControllerProcessReportMessage>() {
public void handle(AgentControllerProcessReportMessage message) {
addAgentStatusReport(message);
}
});
messageDispatchRegistry.set(LogReportGrinderMessage.class, new AbstractHandler<LogReportGrinderMessage>() {
public void handle(final LogReportGrinderMessage message) {
m_logListeners.apply(new Informer<LogArrivedListener>() {
@Override
public void inform(LogArrivedListener listener) {
listener.logArrived(message.getTestId(), message.getAddress(), message.getLogs());
}
});
}
});
messageDispatchRegistry.set(AgentDownloadGrinderMessage.class, new AbstractHandler<AgentDownloadGrinderMessage>() {
public void handle(final AgentDownloadGrinderMessage message) {
final AgentUpdateGrinderMessage agentUpdateGrinderMessage = m_agentDownloadListener.onAgentDownloadRequested(message.getVersion(), message.getNext());
m_consoleCommunication.sendToAddressedAgents(message.getAddress(), agentUpdateGrinderMessage);
}
});
}
/**
* Add Agent status report.
*
* @param message {@link AgentControllerProcessReportMessage}
*/
public void addAgentStatusReport(AgentControllerProcessReportMessage message) {
AgentStatus agentStatus = getAgentStatus(message.getAgentIdentity());
agentStatus.setAgentProcessStatus(message);
m_newData = true;
}
/**
* Get agent status. It's for internal use.
*
* @param agentIdentity agent identity
* @return {@link AgentStatus}
*/
private AgentStatus getAgentStatus(AgentIdentity agentIdentity) {
synchronized (m_agentMap) {
final AgentStatus existing = m_agentMap.get(agentIdentity);
if (existing != null) {
m_agentMap.put(agentIdentity, existing);
return existing;
}
final AgentStatus created = new AgentStatus(agentIdentity);
m_agentMap.put(agentIdentity, created);
return created;
}
}
/**
* Update agent status.
*/
private void update() {
if (!m_newData) {
return;
}
m_newData = false;
m_listeners.apply(new ListenerSupport.Informer<Listener>() {
public void inform(Listener l) {
l.update(new ConcurrentHashMap<AgentIdentity, AgentStatus>(m_agentMap));
}
});
}
public void setAgentDownloadListener(AgentDownloadRequestListener agentDownloadListener) {
this.m_agentDownloadListener = agentDownloadListener;
}
/**
* Interface for listeners to SampleModelImplementation.
*/
interface Listener extends EventListener {
/**
* Update agent status.
*
* @param agentMap agent map
*/
public void update(Map<AgentIdentity, AgentStatus> agentMap);
}
/**
* Callers are for synchronization.
*
* @param purgableMap map for {@link ProcessIdentity}
*/
private void purge(Map<? extends ProcessIdentity, ? extends Purgable> purgableMap) {
final Set<ProcessIdentity> zombies = new HashSet<ProcessIdentity>();
for (Entry<? extends ProcessIdentity, ? extends Purgable> entry : purgableMap.entrySet()) {
if (entry.getValue().shouldPurge()) {
zombies.add(entry.getKey());
}
}
if (zombies.size() > 0) {
purgableMap.keySet().removeAll(zombies);
m_newData = true;
}
}
private interface Purgable {
/**
* check it should be purged.
*
* @return true if purse is necessary
*/
boolean shouldPurge();
}
private abstract class AbstractTimedReference implements Purgable {
private int m_purgeDelayCount;
@Override
public boolean shouldPurge() {
// Processes have a short time to report - see the javadoc for
// FLUSH_PERIOD.
if (m_purgeDelayCount > 0) {
return true;
}
++m_purgeDelayCount;
return false;
}
}
private final class AgentReference extends AbstractTimedReference {
private final AgentControllerProcessReportMessage m_agentProcessReportMessage;
/**
* Constructor.
*
* @param agentProcessReportMessage {@link AgentControllerProcessReportMessage}
*/
AgentReference(AgentControllerProcessReportMessage agentProcessReportMessage) {
this.m_agentProcessReportMessage = agentProcessReportMessage;
}
@Override
public boolean shouldPurge() {
final boolean purge = super.shouldPurge();
if (purge) {
// Protected against race with add since the caller holds
// m_agentIdentityToAgentAndWorkers, and we are about to be
// removed from m_agentIdentityToAgentAndWorkers.
m_agentMap.remove(m_agentProcessReportMessage.getAgentIdentity());
}
return purge;
}
}
/**
* Agent Status.
*
* @author JunHo Yoon
*/
public final class AgentStatus implements Purgable {
private volatile AgentReference m_agentReference;
/**
* Constructor.
*
* @param agentIdentity agent identity
*/
public AgentStatus(AgentIdentity agentIdentity) {
setAgentProcessStatus(new UnknownAgentProcessReport(new AgentAddress(agentIdentity)));
}
@Override
public boolean shouldPurge() {
return m_agentReference.shouldPurge();
}
/**
* Get agent controller status.
*
* @return {@link AgentControllerState} member
*/
public AgentControllerState getAgentControllerState() {
if (m_agentReference == null) {
return AgentControllerState.UNKNOWN;
}
AgentControllerProcessReportMessage agentProcessReport = m_agentReference.m_agentProcessReportMessage;
return agentProcessReport == null ? AgentControllerState.UNKNOWN : agentProcessReport.getState();
}
/**
* Set each agent process message on the agent status.
*
* @param message Message
*/
public void setAgentProcessStatus(AgentControllerProcessReportMessage message) {
LOGGER.trace("agent perf status on {} is {}", message.getAgentIdentity(), message.getSystemDataModel());
m_agentReference = new AgentReference(message);
}
public String getVersion() {
return m_agentReference == null ? null : m_agentReference.m_agentProcessReportMessage.getVersion();
}
public SystemDataModel getSystemDataModel() {
return m_agentReference == null ? null : m_agentReference.m_agentProcessReportMessage.getSystemDataModel();
}
public int getConnectingPort() {
return m_agentReference == null ? 0 : m_agentReference.m_agentProcessReportMessage.getConnectingPort();
}
public AgentIdentity getAgentIdentity() {
return m_agentReference == null ? null : m_agentReference.m_agentProcessReportMessage.getAgentIdentity();
}
public String getAgentName() {
return m_agentReference == null ? "" : m_agentReference.m_agentProcessReportMessage.getAgentIdentity()
.getName();
}
}
/**
* Add process control {@link Listener}.
*
* @param listener listener to be added
*/
public void addListener(Listener listener) {
m_listeners.add(listener);
}
/**
* Add Log control {@link LogArrivedListener}.
*
* @param listener listener to be added
*/
public void addLogArrivedListener(LogArrivedListener listener) {
m_logListeners.add(listener);
}
/*
* (non-Javadoc)
*
* @see net.grinder.console.communication.AgentProcessControl#startAgent(java .util.Set,
* net.grinder.common.GrinderProperties)
*/
@Override
public void startAgent(Set<AgentIdentity> agents, GrinderProperties properties) {
final GrinderProperties propertiesToSend = properties != null ? properties : new GrinderProperties();
for (AgentIdentity each : agents) {
m_consoleCommunication.sendToAddressedAgents(new AgentAddress(each), new StartGrinderMessage(
propertiesToSend, each.getNumber()));
}
}
/*
* (non-Javadoc)
*
* @see net.grinder.console.communication.AgentProcessControl#stopAgent(net.grinder
* .common.processidentity.AgentIdentity)
*/
@Override
public void stopAgent(AgentIdentity agentIdentity) {
m_consoleCommunication.sendToAddressedAgents(new AgentAddress(agentIdentity), new StopGrinderMessage());
}
/*
* (non-Javadoc)
*
* @see net.grinder.console.communication.AgentProcessControl#getNumberOfLiveAgents ()
*/
@Override
public int getNumberOfLiveAgents() {
synchronized (m_agentMap) {
return m_agentMap.size();
}
}
/*
* (non-Javadoc)
*
* @see net.grinder.console.communication.AgentProcessControl#getAgents(net.grinder
* .message.console.AgentControllerState, int)
*/
@Override
public Set<AgentIdentity> getAgents(AgentControllerState state, int count) {
count = count == 0 ? Integer.MAX_VALUE : count;
synchronized (m_agentMap) {
int i = 0;
Set<AgentIdentity> agents = new HashSet<AgentIdentity>();
for (Map.Entry<AgentIdentity, AgentStatus> each : m_agentMap.entrySet()) {
if (each.getValue().getAgentControllerState().equals(state) && ++i <= count) {
agents.add(each.getKey());
}
}
return agents;
}
}
/*
* (non-Javadoc)
*
* @see net.grinder.console.communication.AgentProcessControl#getAllAgents()
*/
@Override
public Set<AgentIdentity> getAllAgents() {
synchronized (m_agentMap) {
return m_agentMap.keySet();
}
}
private static class UnknownAgentProcessReport extends AgentControllerProcessReportMessage {
/**
* UUID.
*/
private static final long serialVersionUID = -2758014000696737553L;
/**
* Constructor.
*
* @param address {@link AgentAddress} in which the agent process is not known.
*/
public UnknownAgentProcessReport(AgentAddress address) {
super(AgentControllerState.UNKNOWN, null, 0, null);
try {
setAddress(address);
} catch (CommunicationException e) {
LOGGER.error("Error while setAdress" + address, e);
}
}
public AgentControllerState getState() {
return AgentControllerState.UNKNOWN;
}
}
@Override
public AgentControllerState getAgentControllerState(AgentIdentity agentIdentity) {
return getAgentStatus(agentIdentity).getAgentControllerState();
}
@Override
public String getAgentVersion(AgentIdentity agentIdentity) {
return getAgentStatus(agentIdentity).getVersion();
}
@Override
public SystemDataModel getSystemDataModel(AgentIdentity agentIdentity) {
return getAgentStatus(agentIdentity).getSystemDataModel();
}
@Override
public int getAgentConnectingPort(AgentIdentity agentIdentity) {
return getAgentStatus(agentIdentity).getConnectingPort();
}
/**
* Get agent identities and status map matching the given predicate.
*
* @param predicate predicate
* @return {@link AgentIdentity} {@link AgentStatus} map
* @since 3.1.2
*/
public Set<AgentStatus> getAgentStatusSet(Predicate<AgentStatus> predicate) {
Set<AgentStatus> statusSet = newLinkedHashSet();
for (Entry<AgentIdentity, AgentStatus> each : m_agentMap.entrySet()) {
if (predicate.apply(each.getValue())) {
statusSet.add(each.getValue());
}
}
return statusSet;
}
}