org.kaazing.robot.driver.Robot Maven / Gradle / Ivy
The newest version!
/*
* Copyright (c) 2014 "Kaazing Corporation," (www.kaazing.com)
*
* This file is part of Robot.
*
* Robot is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
package org.kaazing.robot.driver;
import static org.jboss.netty.channel.Channels.pipeline;
import static org.jboss.netty.channel.Channels.pipelineFactory;
import static org.jboss.netty.util.CharsetUtil.UTF_8;
import static org.kaazing.robot.driver.netty.bootstrap.BootstrapFactory.newBootstrapFactory;
import static org.kaazing.robot.driver.netty.channel.ChannelAddressFactory.newChannelAddressFactory;
import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelHandler;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.ChildChannelStateEvent;
import org.jboss.netty.channel.DefaultChannelFuture;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.SimpleChannelHandler;
import org.jboss.netty.channel.group.ChannelGroupFuture;
import org.jboss.netty.channel.group.ChannelGroupFutureListener;
import org.jboss.netty.channel.group.DefaultChannelGroup;
import org.jboss.netty.channel.local.DefaultLocalClientChannelFactory;
import org.jboss.netty.logging.InternalLogger;
import org.jboss.netty.logging.InternalLoggerFactory;
import org.kaazing.robot.driver.behavior.Configuration;
import org.kaazing.robot.driver.behavior.PlayBackScript;
import org.kaazing.robot.driver.behavior.RobotCompletionFuture;
import org.kaazing.robot.driver.behavior.RobotCompletionFutureImpl;
import org.kaazing.robot.driver.behavior.handler.CompletionHandler;
import org.kaazing.robot.driver.behavior.parser.Parser;
import org.kaazing.robot.driver.behavior.visitor.GatherStreamsLocationVisitor;
import org.kaazing.robot.driver.behavior.visitor.GenerateConfigurationVisitor;
import org.kaazing.robot.driver.netty.bootstrap.BootstrapFactory;
import org.kaazing.robot.driver.netty.bootstrap.ClientBootstrap;
import org.kaazing.robot.driver.netty.bootstrap.ServerBootstrap;
import org.kaazing.robot.driver.netty.channel.ChannelAddressFactory;
import org.kaazing.robot.driver.netty.channel.CompositeChannelFuture;
import org.kaazing.robot.lang.LocationInfo;
import org.kaazing.robot.lang.ast.AstScriptNode;
import org.kaazing.robot.lang.parser.ScriptParser;
public class Robot {
private static final InternalLogger LOGGER = InternalLoggerFactory.getInstance(Robot.class);
/*
* A list of completion futures that will indicate that the script is completed. Each stream except for a AcceptNode has
* a completion handler. The completion handler's handlerFuture is the complete future
*/
private final List completionFutures = new ArrayList<>();
private final List progressInfos = new CopyOnWriteArrayList<>();
private final Map serverLocations = new HashMap<>();
private final List bindFutures = new ArrayList<>();
private final List connectFutures = new ArrayList<>();
private final Channel channel = new DefaultLocalClientChannelFactory().newChannel(pipeline(new SimpleChannelHandler()));
private final ChannelFuture startedFuture = Channels.future(channel);
private final RobotCompletionFutureImpl finishedFuture = new RobotCompletionFutureImpl(channel, true);
private final DefaultChannelGroup serverChannels = new DefaultChannelGroup();
private final DefaultChannelGroup clientChannels = new DefaultChannelGroup();
private final Map failedCauses = new ConcurrentHashMap<>();
private String expectedScript;
private Configuration configuration;
private AstScriptNode scriptAST;
private ChannelFuture preparedFuture;
private volatile boolean destroyed;
private final ChannelAddressFactory addressFactory;
private final BootstrapFactory bootstrapFactory;
private final boolean releaseBootstrapFactory;
// tests
public Robot() {
this(newChannelAddressFactory());
}
private Robot(ChannelAddressFactory addressFactory) {
this(addressFactory,
newBootstrapFactory(Collections., Object>singletonMap(ChannelAddressFactory.class, addressFactory)), true);
}
public Robot(ChannelAddressFactory addressFactory, BootstrapFactory bootstrapFactory) {
this(addressFactory, bootstrapFactory, false);
}
private Robot(
ChannelAddressFactory addressFactory,
BootstrapFactory bootstrapFactory,
boolean releaseBootstrapFactory) {
this.bootstrapFactory = bootstrapFactory;
this.addressFactory = addressFactory;
this.releaseBootstrapFactory = releaseBootstrapFactory;
listenForFinishedFuture();
}
public RobotCompletionFuture getScriptCompleteFuture() {
return finishedFuture;
}
public ChannelFuture getPreparedFuture() {
return preparedFuture;
}
public ChannelFuture getStartedFuture() {
return startedFuture;
}
public ChannelFuture prepare(String script) throws Exception {
if (preparedFuture != null) {
throw new IllegalStateException("Script already prepared");
}
this.expectedScript = script;
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Expected script:\n" + expectedScript);
}
final ScriptParser parser = new Parser();
scriptAST = parser.parse(new ByteArrayInputStream(expectedScript.getBytes(UTF_8)));
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Parsed script:\n" + scriptAST);
}
final GenerateConfigurationVisitor visitor = new GenerateConfigurationVisitor(bootstrapFactory, addressFactory);
configuration = scriptAST.accept(visitor, new GenerateConfigurationVisitor.State());
preparedFuture = bindServers();
/* Iterate over the set of completion handlers. */
for (final CompletionHandler h : configuration.getCompletionHandlers()) {
/* Add the completion future */
final ChannelFuture f = h.getHandlerFuture();
completionFutures.add(f);
/*
* Listen for each completion future and grab its location info.
* This is the last command or event (not implicit) that
* succeeds
*/
f.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(final ChannelFuture future) throws Exception {
LocationInfo location = h.getProgressInfo();
/*
* An accept or connect that never connected will have a
* null location. Don't include these.
*/
if (location != null) {
progressInfos.add(location);
}
Throwable cause = future.getCause();
if (cause != null) {
failedCauses.put(h.getStreamStartLocation(), cause);
}
}
});
}
// We start listening before start because one can abort before start.
listenForScriptCompletion();
return preparedFuture;
}
public ChannelFuture prepareAndStart(String script) throws Exception {
ChannelFuture prepareFuture = prepare(script);
prepareFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(final ChannelFuture future) throws Exception {
start();
}
});
return startedFuture;
}
public ChannelFuture start() throws Exception {
if (preparedFuture == null || !preparedFuture.isSuccess()) {
throw new IllegalStateException("Script has not been prepared or is still preparing");
} else if (startedFuture.isDone()) {
throw new IllegalStateException("Script has already been started");
}
/* Connect to any clients */
for (final ClientBootstrap client : configuration.getClientBootstraps()) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("[id: ] connect " + client.getOption("remoteAddress"));
}
ChannelFuture connectFuture = client.connect();
connectFutures.add(connectFuture);
clientChannels.add(connectFuture.getChannel());
}
/*
* If we have no completion futures it means that there was an error or
* otherwise we have the null script
*/
if (completionFutures.isEmpty() && !scriptAST.toString().equals("")) {
throw new RobotException("No Completion Futures exists");
}
startedFuture.setSuccess();
return startedFuture;
}
public RobotCompletionFuture abort() {
if (!finishedFuture.isDone()) {
finishedFuture.cancel();
}
return finishedFuture;
}
public boolean isDestroyed() {
return destroyed;
}
public boolean destroy() {
if (destroyed) {
return true;
}
abort();
if (releaseBootstrapFactory) {
try {
bootstrapFactory.releaseExternalResources();
}
catch (Exception e) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Caught exception releasing resources", e);
}
return false;
}
}
return destroyed = true;
}
private void listenForScriptCompletion() {
/*
* OK. Now listen for the set of all completion futures so that we can
* tell the client when we are done
*/
ChannelFuture executionFuture = new CompositeChannelFuture<>(channel, completionFutures);
executionFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(final ChannelFuture future) throws Exception {
/*
* We need to map our progressInfos to streams so that we can create the observed script. After running the
* GatherStreamsLocationVisitor our results are in state.results.
*/
List progressInfos = new ArrayList<>(Robot.this.progressInfos);
final GatherStreamsLocationVisitor.State state = new GatherStreamsLocationVisitor.State(progressInfos,
serverLocations);
scriptAST.accept(new GatherStreamsLocationVisitor(), state);
// Create observed Script
PlayBackScript o = new PlayBackScript(expectedScript, state.results, failedCauses);
String observedScript = o.createPlayBackScript();
detachAllPipelines();
// Cancel any pending binds and connects
for (ChannelFuture f : bindFutures) {
f.cancel();
}
for (ChannelFuture f : connectFutures) {
f.cancel();
}
// Close server and client channels
closeChannels();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Observed script:\n" + observedScript);
}
if (finishedFuture.isDone()) {
finishedFuture.setExpectedScript(expectedScript);
finishedFuture.setObservedScript(observedScript);
} else {
finishedFuture.setSuccess(observedScript, expectedScript);
}
}
});
}
// If we are canceling we have to cancel the script execution.
// And then ... in all cases we need to release external resources.
private void listenForFinishedFuture() {
finishedFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(final ChannelFuture future) throws Exception {
if (future.isCancelled()) {
if (configuration != null) {
for (final CompletionHandler h : configuration.getCompletionHandlers()) {
// Cancel the completion handler
ChannelFuture cancelFuture = h.cancel();
boolean isDefaultChannelFuture = cancelFuture instanceof DefaultChannelFuture;
// Edge case. Normally the cancel occurs immediately. Only when the pipeline is not
// prepared (preperation event) does it not.
// Since we are aborting we don't care that we are blocking in the io thread.
if (isDefaultChannelFuture) {
DefaultChannelFuture.setUseDeadLockChecker(false);
}
boolean isCancelled = cancelFuture.awaitUninterruptibly(500);
if (isDefaultChannelFuture) {
DefaultChannelFuture.setUseDeadLockChecker(true);
}
if (!isCancelled) {
// Force the completion handler future to success if it does not cancel in half a second
h.getHandlerFuture().setSuccess();
}
}
} else {
// Then we just get the empty script
LOGGER.debug("Abort received but script not prepared");
finishedFuture.setObservedScript("");
}
}
}
});
}
private void detachAllPipelines() {
// We need some kind of handler to avoid warnings.
ChannelHandler finalHandler = new SimpleChannelHandler() {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
Channel channel = ctx.getChannel();
channel.close();
}
};
/*
* Set the pipelines to empty just in case some one trys to connect before we close the server channel Or just in
* case one of the client bootstraps havent connected yet ... dont think that is poosible
*/
for (ServerBootstrap bootstrap : configuration.getServerBootstraps()) {
bootstrap.setPipelineFactory(pipelineFactory(pipeline(finalHandler)));
}
for (ClientBootstrap bootstrap : configuration.getClientBootstraps()) {
bootstrap.setPipelineFactory(pipelineFactory(pipeline(finalHandler)));
}
/*
* Remove all the handlers from any existing channels. The problem we are solving here is that when a script is
* aborted we set the pipeline future of the completion handler to success. However, this does not cause earlier
* pipeline futures to succeed. As such if there are any barriers. A subsequent close will end up getting queued and
* will never end. Another option would be when be to cancel the pipeline future. And make that cancel on a composite
* cancel all its containing futures. However, it does not seem right to do that for cancel, but not setSuccess and
* setFailure. But maybe we can do that too. But ... removing all the handlers seems cleaner anyway. Why have events
* flowing through the system do to us closing the channels when the script is already considered complete.
*/
for (Channel c : clientChannels) {
ChannelPipeline pipeline = c.getPipeline();
for (ChannelHandler handler : pipeline.toMap().values()) {
pipeline.remove(handler);
}
pipeline.addLast("SCRIPTDONEHANDLER", finalHandler);
}
}
private void closeChannels() {
final ChannelGroupFuture closeFuture = serverChannels.close();
closeFuture.addListener(new ChannelGroupFutureListener() {
@Override
public void operationComplete(final ChannelGroupFuture future) {
clientChannels.close();
}
});
}
private ChannelFuture bindServers() {
/* Accept's ... Robot acting as a server */
for (final ServerBootstrap server : configuration.getServerBootstraps()) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Binding to address " + server.getOption("localAddress"));
}
final LocationInfo location = (LocationInfo) server.getOption("locationInfo");
assert !serverLocations.containsKey(location) : "There is already a location " + location
+ " for this server " + server.getOption("localAddress");
/* Keep track of the client channels */
server.setParentHandler(new SimpleChannelHandler() {
@Override
public void childChannelOpen(ChannelHandlerContext ctx, ChildChannelStateEvent e) throws Exception {
clientChannels.add(e.getChildChannel());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
Channel channel = ctx.getChannel();
channel.close();
}
});
// Bind Asynchronously
ChannelFuture bindFuture = server.bindAsync();
// Add to out serverChannel Group
serverChannels.add(bindFuture.getChannel());
// Add to our list of bindFutures so we can cancel them later on a possible abort
bindFutures.add(bindFuture);
// Listen for the bindFuture.
bindFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(final ChannelFuture future) throws Exception {
final boolean isDebugEnabled = LOGGER.isDebugEnabled();
if (future.isSuccess()) {
if (isDebugEnabled) {
LOGGER.debug("Successfully bound to " + server.getOption("localAddress"));
}
// Add to our list of serverLocations ... which contain the locationInfo's of successfully bound
// server channels
serverLocations.put((LocationInfo) server.getOption("locationInfo"), null);
} else {
Throwable cause = future.getCause();
/*
* Grab the set of completion handlers for the server. This is the set of completion futures for the
* Accept stream.
*/
@SuppressWarnings("unchecked")
final Collection serverCompletionFutures =
(Collection) server.getOption("completionFutures");
/* Set all the futures to fail. If we couldn't bind */
for (ChannelFuture f : serverCompletionFutures) {
f.setFailure(cause);
}
}
}
});
}
// What should prepared mean ... server channels have all completed binding or just that they started.
// Initially I was thinking that it should be when they are done. But I'm not so sure.
// In that case what should happen if a subset of the binds fail and a subset succeed? What should happen if they all
// fail? I think in either case the robot should generate an observed script with the exception events in place of
// the accept lines. This is why I choose to return a successful future rather than some composite of the bind
// futures.
return Channels.succeededFuture(channel);
}
}