/*
* 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.perftest.service;
import net.grinder.SingleConsole;
import net.grinder.console.model.ConsoleCommunicationSetting;
import net.grinder.console.model.ConsoleProperties;
import org.h2.util.StringUtils;
import org.ngrinder.infra.config.Config;
import org.ngrinder.perftest.model.NullSingleConsole;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
import static net.grinder.util.NetworkUtils.getAvailablePorts;
import static org.ngrinder.common.constant.ControllerConstants.*;
import static org.ngrinder.common.util.ExceptionUtils.processException;
import static org.ngrinder.common.util.NoOp.noOp;
/**
* Console manager is responsible for console instance management.
* <p/>
* A number of consoles(specified in ngrinder.maxConcurrentTest in system.conf) are pooled. Actually console itself is
* not pooled but the {@link ConsoleEntry} which contains console information are pooled internally. Whenever a user
* requires a new console, it gets the one {@link ConsoleEntry} from the pool and creates new console with the
* {@link ConsoleEntry}. Currently using consoles are kept in {@link #consoleInUse} member variable.
*
* @author JunHo Yoon
* @since 3.0
*/
@Component
public class ConsoleManager {
private static final int MAX_PORT_NUMBER = 65000;
private static final Logger LOG = LoggerFactory.getLogger(ConsoleManager.class);
private volatile ArrayBlockingQueue<ConsoleEntry> consoleQueue;
private volatile List<SingleConsole> consoleInUse = Collections.synchronizedList(new ArrayList<SingleConsole>());
@Autowired
private Config config;
@Autowired
private AgentManager agentManager;
/**
* Prepare console queue.
*/
@PostConstruct
public void init() {
int consoleSize = getConsoleSize();
consoleQueue = new ArrayBlockingQueue<ConsoleEntry>(consoleSize);
final String currentIP = config.getCurrentIP();
for (int each : getAvailablePorts(currentIP, consoleSize, getConsolePortBase(), MAX_PORT_NUMBER)) {
final ConsoleEntry e = new ConsoleEntry(config.getCurrentIP(), each);
try {
e.occupySocket();
consoleQueue.add(e);
} catch (Exception ex) {
LOG.error("socket binding to {}:{} is failed", config.getCurrentIP(), each);
}
}
}
/**
* Get the base port number of console.
* <p/>
* It can be specified at ngrinder.consolePortBase in system.conf. Each console will be created from that port.
*
* @return base port number
*/
protected int getConsolePortBase() {
return config.getControllerProperties().getPropertyInt(PROP_CONTROLLER_CONSOLE_PORT_BASE);
}
/**
* Get the console pool size. It can be specified at ngrinder.maxConcurrentTest in system.conf.
*
* @return console size.
*/
protected int getConsoleSize() {
return config.getControllerProperties().getPropertyInt(PROP_CONTROLLER_MAX_CONCURRENT_TEST);
}
/**
* Get Timeout (in second).
*
* @return 5000 second
*/
protected long getMaxWaitingMilliSecond() {
return config.getControllerProperties().getPropertyInt(PROP_CONTROLLER_MAX_CONNECTION_WAITING_MILLISECOND);
}
/**
* Get an available console.
* <p/>
* If there is no available console, it waits until available console is returned back. If the specific time is
* elapsed, the timeout error occurs and throws {@link org.ngrinder.common.exception.NGrinderRuntimeException} . The
* timeout can be adjusted by overriding {@link #getMaxWaitingMilliSecond()}.
*
* @param baseConsoleProperties base {@link net.grinder.console.model.ConsoleProperties}
* @return console
*/
public SingleConsole getAvailableConsole(ConsoleProperties baseConsoleProperties) {
ConsoleEntry consoleEntry = null;
try {
consoleEntry = consoleQueue.poll(getMaxWaitingMilliSecond(), TimeUnit.MILLISECONDS);
if (consoleEntry == null) {
throw processException("no console entry available");
}
synchronized (this) {
consoleEntry.releaseSocket();
// FIXME : It might fail here
ConsoleCommunicationSetting consoleCommunicationSetting = ConsoleCommunicationSetting.asDefault();
if (config.getInactiveClientTimeOut() > 0) {
consoleCommunicationSetting.setInactiveClientTimeOut(config.getInactiveClientTimeOut());
}
SingleConsole singleConsole = new SingleConsole(config.getCurrentIP(), consoleEntry.getPort(),
consoleCommunicationSetting, baseConsoleProperties);
getConsoleInUse().add(singleConsole);
singleConsole.setCsvSeparator(config.getCsvSeparator());
return singleConsole;
}
} catch (Exception e) {
if (consoleEntry != null) {
consoleQueue.add(consoleEntry);
}
throw processException("no console entry available");
}
}
/**
* Return back the given console.
* <p/>
* Duplicated returns is allowed.
*
* @param testIdentifier test identifier
* @param console console which will be returned back.
*/
public void returnBackConsole(String testIdentifier, SingleConsole console) {
if (console == null || console instanceof NullSingleConsole) {
LOG.error("Attempt to return back null console for {}.", testIdentifier);
return;
}
try {
console.sendStopMessageToAgents();
} catch (Exception e) {
LOG.error("Exception occurred during console return back for test {}.",
testIdentifier, e);
// But the port is getting back.
} finally {
// This is very careful implementation..
try {
// Wait console is completely shutdown...
console.waitUntilAllAgentDisconnected();
} catch (Exception e) {
LOG.error("Exception occurred during console return back for test {}.",
testIdentifier, e);
// If it's not disconnected still, stop them by force.
agentManager.stopAgent(console.getConsolePort());
}
try {
console.shutdown();
} catch (Exception e) {
LOG.error("Exception occurred during console return back for test {}.",
testIdentifier, e);
}
int consolePort;
String consoleIP;
try {
consolePort = console.getConsolePort();
consoleIP = console.getConsoleIP();
ConsoleEntry consoleEntry = new ConsoleEntry(consoleIP, consolePort);
synchronized (this) {
if (!consoleQueue.contains(consoleEntry)) {
consoleEntry.occupySocket();
consoleQueue.add(consoleEntry);
if (!getConsoleInUse().contains(console)) {
LOG.error("Try to return back the not used console on {} port", consolePort);
}
getConsoleInUse().remove(console);
}
}
} catch (Exception e) {
noOp();
}
}
}
/**
* Get the list of {@link SingleConsole} which are used.
*
* @return {@link SingleConsole} list in use
*/
public List<SingleConsole> getConsoleInUse() {
return consoleInUse;
}
/**
* Get the size of currently available consoles.
*
* @return size of available consoles.
*/
public Integer getAvailableConsoleSize() {
return consoleQueue.size();
}
/**
* Get the {@link SingleConsole} instance which is using the given port.
*
* @param port port which the console is using
* @return {@link SingleConsole} instance if found. Otherwise, {@link NullSingleConsole} instance.
*/
public SingleConsole getConsoleUsingPort(Integer port) {
String currentIP = config.getCurrentIP();
for (SingleConsole each : consoleInUse) {
// Avoid to Klocwork error.
if (each instanceof NullSingleConsole) {
continue;
}
if (StringUtils.equals(each.getConsoleIP(), currentIP) && each.getConsolePort() == port) {
return each;
}
}
return new NullSingleConsole();
}
}