/*
* 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 org.ngrinder.agent.service;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import net.grinder.common.processidentity.AgentIdentity;
import net.grinder.engine.controller.AgentControllerIdentityImplementation;
import net.sf.ehcache.Ehcache;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.mutable.MutableInt;
import org.ngrinder.agent.model.ClusteredAgentRequest;
import org.ngrinder.infra.logger.CoreLogger;
import org.ngrinder.infra.schedule.ScheduledTaskService;
import org.ngrinder.model.AgentInfo;
import org.ngrinder.model.User;
import org.ngrinder.monitor.controller.model.SystemDataModel;
import org.ngrinder.region.service.RegionService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.Cache.ValueWrapper;
import org.springframework.cache.CacheManager;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static net.grinder.message.console.AgentControllerState.INACTIVE;
import static net.grinder.message.console.AgentControllerState.WRONG_REGION;
import static org.ngrinder.agent.model.ClusteredAgentRequest.RequestType.*;
import static org.ngrinder.agent.repository.AgentManagerSpecification.active;
import static org.ngrinder.agent.repository.AgentManagerSpecification.visible;
import static org.ngrinder.common.util.CollectionUtils.newArrayList;
import static org.ngrinder.common.util.CollectionUtils.newHashMap;
import static org.ngrinder.common.util.TypeConvertUtils.cast;
/**
* Cluster enabled version of {@link AgentManagerService}.
*
* @author JunHo Yoon
* @since 3.1
*/
public class ClusteredAgentManagerService extends AgentManagerService {
private static final Logger LOGGER = LoggerFactory.getLogger(ClusteredAgentManagerService.class);
@Autowired
CacheManager cacheManager;
private Cache agentRequestCache;
private Cache agentMonitoringTargetsCache;
@Autowired
private RegionService regionService;
/**
* Initialize.
*/
@PostConstruct
public void init() {
super.init();
agentMonitoringTargetsCache = cacheManager.getCache("agent_monitoring_targets");
if (getConfig().isClustered()) {
agentRequestCache = cacheManager.getCache("agent_request");
scheduledTaskService.addFixedDelayedScheduledTask(new Runnable() {
@Override
public void run() {
List<String> keys = cast(((Ehcache) agentRequestCache.getNativeCache())
.getKeysWithExpiryCheck());
String region = getConfig().getRegion() + "|";
for (String each : keys) {
if (each.startsWith(region)) {
if (agentRequestCache.get(each) != null) {
try {
ClusteredAgentRequest agentRequest = cast(agentRequestCache.get(each).get());
if (agentRequest.getRequestType() ==
ClusteredAgentRequest.RequestType.EXPIRE_LOCAL_CACHE) {
expireLocalCache();
} else {
AgentControllerIdentityImplementation agentIdentity = getAgentIdentityByIpAndName(
agentRequest.getAgentIp(), agentRequest.getAgentName());
if (agentIdentity != null) {
agentRequest.getRequestType().process(ClusteredAgentManagerService.this,
agentIdentity);
}
}
agentRequestCache.evict(each);
} catch (Exception e) {
CoreLogger.LOGGER.error(e.getMessage(), e);
}
}
}
}
}
}, 3000);
}
}
@Override
public void checkAgentStatePeriodically() {
super.checkAgentStatePeriodically();
collectAgentSystemData();
}
/**
* Run a scheduled task to check the agent statuses.
*
* @since 3.1
*/
@Override
public void checkAgentState() {
List<AgentInfo> newAgents = newArrayList(0);
List<AgentInfo> updatedAgents = newArrayList(0);
List<AgentInfo> stateUpdatedAgents = newArrayList(0);
Set<AgentIdentity> allAttachedAgents = getAgentManager().getAllAttachedAgents();
Map<String, AgentControllerIdentityImplementation> attachedAgentMap = newHashMap(allAttachedAgents);
for (AgentIdentity agentIdentity : allAttachedAgents) {
AgentControllerIdentityImplementation existingAgent = cast(agentIdentity);
attachedAgentMap.put(createKey(existingAgent), existingAgent);
}
Map<String, AgentInfo> agentsInDBMap = Maps.newHashMap();
// step1. check all agents in DB, whether they are attached to
// controller.
for (AgentInfo eachAgentInDB : getAllLocal()) {
String keyOfAgentInDB = createKey(eachAgentInDB);
agentsInDBMap.put(keyOfAgentInDB, eachAgentInDB);
AgentControllerIdentityImplementation agentIdentity = attachedAgentMap.remove(keyOfAgentInDB);
if (agentIdentity != null) {
// if the agent attached to current controller
if (!isCurrentRegion(agentIdentity)) {
if (eachAgentInDB.getState() != WRONG_REGION) {
eachAgentInDB.setApproved(false);
eachAgentInDB.setRegion(getConfig().getRegion());
eachAgentInDB.setState(WRONG_REGION);
updatedAgents.add(eachAgentInDB);
}
} else if (!hasSameInfo(eachAgentInDB, agentIdentity)) {
fillUp(eachAgentInDB, agentIdentity);
updatedAgents.add(eachAgentInDB);
} else if (!hasSameState(eachAgentInDB, agentIdentity)) {
eachAgentInDB.setState(getAgentManager().getAgentState(agentIdentity));
stateUpdatedAgents.add(eachAgentInDB);
} else if (eachAgentInDB.getApproved() == null) {
updatedAgents.add(fillUpApproval(eachAgentInDB));
}
} else { // the agent in DB is not attached to current controller
if (eachAgentInDB.getState() != INACTIVE) {
eachAgentInDB.setState(INACTIVE);
stateUpdatedAgents.add(eachAgentInDB);
}
}
}
// step2. check all attached agents, whether they are new, and not saved
// in DB.
for (AgentControllerIdentityImplementation agentIdentity : attachedAgentMap.values()) {
AgentInfo agentInfo = agentManagerRepository.findByIpAndHostName(
agentIdentity.getIp(),
agentIdentity.getName());
if (agentInfo == null) {
agentInfo = new AgentInfo();
newAgents.add(fillUp(agentInfo, agentIdentity));
} else {
updatedAgents.add(fillUp(agentInfo, agentIdentity));
}
if (!isCurrentRegion(agentIdentity)) {
agentInfo.setState(WRONG_REGION);
agentInfo.setApproved(false);
}
}
cachedLocalAgentService.updateAgents(newAgents, updatedAgents, stateUpdatedAgents, null);
if (!newAgents.isEmpty() || !updatedAgents.isEmpty()) {
expireLocalCache();
}
}
private Gson gson = new Gson();
/**
* Collect the agent system info every second.
*/
public void collectAgentSystemData() {
Ehcache nativeCache = (Ehcache) agentMonitoringTargetsCache.getNativeCache();
List<String> keysWithExpiryCheck = cast(nativeCache.getKeysWithExpiryCheck());
for (String each : keysWithExpiryCheck) {
ValueWrapper value = agentMonitoringTargetsCache.get(each);
AgentControllerIdentityImplementation agentIdentity = cast(value.get());
if (agentIdentity != null) {
// Is Same Region
if (isCurrentRegion(agentIdentity)) {
try {
updateSystemStat(agentIdentity);
} catch (IllegalStateException e) {
LOGGER.error("error while update system stat.");
}
}
}
}
}
public void updateSystemStat(final AgentControllerIdentityImplementation agentIdentity) {
cachedLocalAgentService.doSthInTransaction(new Runnable() {
public void run() {
agentManagerRepository.updateSystemStat(agentIdentity.getIp(),
agentIdentity.getName(),
gson.toJson(getSystemDataModel(agentIdentity)));
}
});
}
private SystemDataModel getSystemDataModel(AgentIdentity agentIdentity) {
return getAgentManager().getSystemDataModel(agentIdentity);
}
public List<AgentInfo> getAllActive() {
return filterOnlyActiveRegion(agentManagerRepository.findAll(active()));
}
public List<AgentInfo> getAllVisible() {
return filterOnlyActiveRegion(agentManagerRepository.findAll(visible()));
}
private List<AgentInfo> filterOnlyActiveRegion(List<AgentInfo> agents) {
final Set<String> regions = getRegions();
return Lists.newArrayList(Iterables.filter(agents,
new Predicate<AgentInfo>() {
@Override
public boolean apply(@Nullable AgentInfo input) {
return input != null && regions.contains(extractRegionFromAgentRegion(input.getRegion()));
}
}));
}
/**
* Get the available agent count map in all regions of the user, including
* the free agents and user specified agents.
*
* @param user current user
* @return user available agent count map
*/
@Override
public Map<String, MutableInt> getAvailableAgentCountMap(User user) {
Set<String> regions = getRegions();
Map<String, MutableInt> availShareAgents = newHashMap(regions);
Map<String, MutableInt> availUserOwnAgent = newHashMap(regions);
for (String region : regions) {
availShareAgents.put(region, new MutableInt(0));
availUserOwnAgent.put(region, new MutableInt(0));
}
String myAgentSuffix = "owned_" + user.getUserId();
for (AgentInfo agentInfo : getAllActive()) {
// Skip all agents which are disapproved, inactive or
// have no region prefix.
if (!agentInfo.isApproved()) {
continue;
}
String fullRegion = agentInfo.getRegion();
String region = extractRegionFromAgentRegion(fullRegion);
if (StringUtils.isBlank(region) || !regions.contains(region)) {
continue;
}
// It's my own agent
if (fullRegion.endsWith(myAgentSuffix)) {
incrementAgentCount(availUserOwnAgent, region, user.getUserId());
} else if (!fullRegion.contains("owned_")) {
incrementAgentCount(availShareAgents, region, user.getUserId());
}
}
int maxAgentSizePerConsole = getMaxAgentSizePerConsole();
for (String region : regions) {
MutableInt mutableInt = availShareAgents.get(region);
int shareAgentCount = mutableInt.intValue();
mutableInt.setValue(Math.min(shareAgentCount, maxAgentSizePerConsole));
mutableInt.add(availUserOwnAgent.get(region));
}
return availShareAgents;
}
protected Set<String> getRegions() {
return regionService.getAll().keySet();
}
protected boolean isCurrentRegion(AgentControllerIdentityImplementation agentIdentity) {
return StringUtils.equals(extractRegionFromAgentRegion(agentIdentity.getRegion()), getConfig().getRegion());
}
private void incrementAgentCount(Map<String, MutableInt> agentMap, String region, String userId) {
if (!agentMap.containsKey(region)) {
LOGGER.warn("Region :{} not exist in cluster nor owned by user:{}.", region, userId);
} else {
agentMap.get(region).increment();
}
}
@Override
public AgentInfo approve(Long id, boolean approve) {
AgentInfo agent = super.approve(id, approve);
if (agent != null) {
agentRequestCache.put(extractRegionFromAgentRegion(agent.getRegion()) + "|" + createKey(agent),
new ClusteredAgentRequest(agent.getIp(), agent.getName(), EXPIRE_LOCAL_CACHE));
}
return agent;
}
/**
* Stop agent. In cluster mode, it queues the agent stop request to
* agentRequestCache.
*
* @param id agent id in db
*/
@Override
public void stopAgent(Long id) {
AgentInfo agent = getOne(id);
if (agent == null) {
return;
}
agentRequestCache.put(extractRegionFromAgentRegion(agent.getRegion()) + "|" + createKey(agent),
new ClusteredAgentRequest(agent.getIp(), agent.getName(), STOP_AGENT));
}
/**
* Add the agent system data model share request on cache.
*
* @param id agent id in db.
*/
@Override
public void requestShareAgentSystemDataModel(Long id) {
AgentInfo agent = getOne(id);
if (agent == null) {
return;
}
agentRequestCache.put(extractRegionFromAgentRegion(agent.getRegion()) + "|" + createKey(agent),
new ClusteredAgentRequest(agent.getIp(), agent.getName(), SHARE_AGENT_SYSTEM_DATA_MODEL));
}
/**
* Get the agent system data model for the given IP. This method is cluster
* aware.
*
* @param ip agent ip
* @param name agent name
* @return {@link SystemDataModel} instance.
*/
@Override
public SystemDataModel getSystemDataModel(String ip, String name) {
AgentInfo found = agentManagerRepository.findByIpAndHostName(ip, name);
String systemStat = (found == null) ? null : found.getSystemStat();
return (StringUtils.isEmpty(systemStat)) ? new SystemDataModel() : gson.fromJson(systemStat,
SystemDataModel.class);
}
/**
* Register agent monitoring target. This method should be called in the
* controller in which the given agent exists.
*
* @param agentIdentity agent identity
*/
public void addAgentMonitoringTarget(AgentControllerIdentityImplementation agentIdentity) {
agentMonitoringTargetsCache.put(createKey(agentIdentity), agentIdentity);
}
/**
* Stop agent.
*
* @param agentIdentity agent identity to be stopped.
*/
public void stopAgent(AgentControllerIdentityImplementation agentIdentity) {
getAgentManager().stopAgent(agentIdentity);
}
/**
* Update agent by id.
*
* @param id agent id
*/
@Override
public void update(Long id) {
AgentInfo agent = getOne(id);
if (agent == null) {
return;
}
agentRequestCache.put(extractRegionFromAgentRegion(agent.getRegion()) + "|" + createKey(agent),
new ClusteredAgentRequest(agent.getIp(), agent.getName(), UPDATE_AGENT));
}
/**
* Clean up the agents from db which belongs to the inactive regions.
*/
@Transactional
public void cleanup() {
super.cleanup();
final Set<String> regions = getRegions();
for (AgentInfo each : agentManagerRepository.findAll()) {
if (!regions.contains(extractRegionFromAgentRegion(each.getRegion()))) {
agentManagerRepository.delete(each);
}
}
}
}