/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.jmeter.engine;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.MalformedURLException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jorphan.collections.HashTree;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class serves all responsibility of starting and stopping distributed tests.
* It was refactored from JMeter and RemoteStart classes to unify retry behavior.
*
* @see org.apache.jmeter.JMeter
* @see org.apache.jmeter.gui.action.RemoteStart
*/
public class DistributedRunner {
private static final Logger log = LoggerFactory.getLogger(DistributedRunner.class);
public static final String RETRIES_NUMBER = "client.tries"; // $NON-NLS-1$
public static final String RETRIES_DELAY = "client.retries_delay"; // $NON-NLS-1$
public static final String CONTINUE_ON_FAIL = "client.continue_on_fail"; // $NON-NLS-1$
private final Properties remoteProps;
private final boolean continueOnFail;
private final int retriesDelay;
private final int retriesNumber;
private PrintStream stdout = new PrintStream(new SilentOutputStream());
private PrintStream stderr = new PrintStream(new SilentOutputStream());
private final Map<String, JMeterEngine> engines = new HashMap<>();
public DistributedRunner() {
this(new Properties());
}
public DistributedRunner(Properties props) {
remoteProps = props;
retriesNumber = JMeterUtils.getPropDefault(RETRIES_NUMBER, 1);
continueOnFail = JMeterUtils.getPropDefault(CONTINUE_ON_FAIL, false);
retriesDelay = JMeterUtils.getPropDefault(RETRIES_DELAY, 5000);
}
public void init(List<String> addresses, HashTree tree) {
// converting list into mutable version
List<String> addrs = new LinkedList<>(addresses);
for (int tryNo = 0; tryNo < retriesNumber; tryNo++) {
if (tryNo > 0) {
println("Following remote engines will retry configuring: " + addrs);
println("Pausing before retry for " + retriesDelay + "ms");
try {
Thread.sleep(retriesDelay);
} catch (InterruptedException e) { // NOSONAR
throw new RuntimeException("Interrupted while initializing remote", e);
}
}
int idx = 0;
while (idx < addrs.size()) {
String address = addrs.get(idx);
println("Configuring remote engine: " + address);
JMeterEngine engine = getClientEngine(address.trim(), tree);
if (engine != null) {
engines.put(address, engine);
addrs.remove(address);
} else {
println("Failed to configure " + address);
idx++;
}
}
if (addrs.isEmpty()) {
break;
}
}
if (!addrs.isEmpty()) {
String msg = "Following remote engines could not be configured:" + addrs;
if (!continueOnFail || engines.size() == 0) {
stop();
throw new RuntimeException(msg); // NOSONAR
} else {
println(msg);
println("Continuing without failed engines...");
}
}
}
/**
* Starts a remote testing engines
*
* @param addresses list of the DNS names or IP addresses of the remote testing engines
*/
public void start(List<String> addresses) {
println("Starting remote engines");
long now = System.currentTimeMillis();
println("Starting the test @ " + new Date(now) + " (" + now + ")");
for (String address : addresses) {
try {
if (engines.containsKey(address)) {
engines.get(address).runTest();
} else {
log.warn("Host not found in list of active engines: {}", address);
}
} catch (IllegalStateException | JMeterEngineException e) { // NOSONAR already reported to user
JMeterUtils.reportErrorToUser(e.getMessage(), JMeterUtils.getResString("remote_error_starting")); // $NON-NLS-1$
}
}
println("Remote engines have been started");
}
/**
* Start all engines that were previously initiated
*/
public void start() {
List<String> addresses = new LinkedList<>();
addresses.addAll(engines.keySet());
start(addresses);
}
public void stop(List<String> addresses) {
println("Stopping remote engines");
for (String address : addresses) {
try {
if (engines.containsKey(address)) {
engines.get(address).stopTest(true);
} else {
log.warn("Host not found in list of active engines: {}", address);
}
} catch (RuntimeException e) {
errln("Failed to stop test on " + address, e);
}
}
println("Remote engines have been stopped");
}
/**
* Stop all engines that were previously initiated
*/
public void stop() {
List<String> addresses = new LinkedList<>();
addresses.addAll(engines.keySet());
stop(addresses);
}
public void shutdown(List<String> addresses) {
println("Shutting down remote engines");
for (String address : addresses) {
try {
if (engines.containsKey(address)) {
engines.get(address).stopTest(false);
} else {
log.warn("Host not found in list of active engines: {}", address);
}
} catch (RuntimeException e) {
errln("Failed to shutdown test on " + address, e);
}
}
println("Remote engines have been shut down");
}
public void exit(List<String> addresses) {
println("Exiting remote engines");
for (String address : addresses) {
try {
if (engines.containsKey(address)) {
engines.get(address).exit();
} else {
log.warn("Host not found in list of active engines: {}", address);
}
} catch (RuntimeException e) {
errln("Failed to exit on " + address, e);
}
}
println("Remote engines have been exited");
}
private JMeterEngine getClientEngine(String address, HashTree testTree) {
JMeterEngine engine;
try {
engine = createEngine(address);
engine.configure(testTree);
if (!remoteProps.isEmpty()) {
engine.setProperties(remoteProps);
}
return engine;
} catch (Exception ex) {
log.error("Failed to create engine at {}", address, ex);
JMeterUtils.reportErrorToUser(ex.getMessage(),
JMeterUtils.getResString("remote_error_init") + ": " + address); // $NON-NLS-1$ $NON-NLS-2$
return null;
}
}
/**
* A factory method that might be overridden for unit testing
*
* @param address address for engine
* @return engine instance
* @throws RemoteException if registry can't be contacted
* @throws NotBoundException when name for address can't be found
* @throws MalformedURLException when address can't be converted to valid URL
*/
protected JMeterEngine createEngine(String address) throws RemoteException, NotBoundException, MalformedURLException {
return new ClientJMeterEngine(address);
}
private void println(String s) {
log.info(s);
stdout.println(s);
}
private void errln(String s, Exception e) {
log.error(s, e);
stderr.println(s + ": ");
e.printStackTrace(stderr); // NOSONAR
}
public void setStdout(PrintStream stdout) {
this.stdout = stdout;
}
public void setStdErr(PrintStream stdErr) {
this.stderr = stdErr;
}
private static class SilentOutputStream extends OutputStream {
@Override
public void write(int b) throws IOException {
// enjoy the silence
}
}
/**
* @return {@link Collection} of {@link JMeterEngine}
*/
public Collection<? extends JMeterEngine> getEngines() {
return engines.values();
}
}