/**
* Copyright 2007-2015, Kaazing Corporation. All rights reserved.
*
* 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.kaazing.k3po.driver.internal;
import static java.lang.Boolean.TRUE;
import static java.lang.String.format;
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.k3po.driver.internal.netty.bootstrap.BootstrapFactory.newBootstrapFactory;
import static org.kaazing.k3po.driver.internal.netty.channel.ChannelAddressFactory.newChannelAddressFactory;
import static org.kaazing.k3po.lang.internal.RegionInfo.newSequential;
import java.io.ByteArrayInputStream;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
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.ChannelHandler.Sharable;
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.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.k3po.driver.internal.behavior.Barrier;
import org.kaazing.k3po.driver.internal.behavior.Configuration;
import org.kaazing.k3po.driver.internal.behavior.ScriptProgress;
import org.kaazing.k3po.driver.internal.behavior.ScriptProgressException;
import org.kaazing.k3po.driver.internal.behavior.handler.CompletionHandler;
import org.kaazing.k3po.driver.internal.behavior.parser.Parser;
import org.kaazing.k3po.driver.internal.behavior.parser.ScriptValidator;
import org.kaazing.k3po.driver.internal.behavior.visitor.GenerateConfigurationVisitor;
import org.kaazing.k3po.driver.internal.netty.bootstrap.BootstrapFactory;
import org.kaazing.k3po.driver.internal.netty.bootstrap.ClientBootstrap;
import org.kaazing.k3po.driver.internal.netty.bootstrap.ServerBootstrap;
import org.kaazing.k3po.driver.internal.netty.channel.ChannelAddressFactory;
import org.kaazing.k3po.driver.internal.netty.channel.CompositeChannelFuture;
import org.kaazing.k3po.driver.internal.resolver.ClientBootstrapResolver;
import org.kaazing.k3po.driver.internal.resolver.ServerBootstrapResolver;
import org.kaazing.k3po.lang.internal.RegionInfo;
import org.kaazing.k3po.lang.internal.ast.AstScriptNode;
import org.kaazing.k3po.lang.internal.parser.ScriptParser;
public class Robot {
private static final InternalLogger LOGGER = InternalLoggerFactory.getInstance(Robot.class);
private final List<ChannelFuture> bindFutures = new ArrayList<>();
private final List<ChannelFuture> connectFutures = new ArrayList<>();
private final Channel channel = new DefaultLocalClientChannelFactory().newChannel(pipeline(new SimpleChannelHandler()));
private final ChannelFuture startedFuture = Channels.future(channel);
private final ChannelFuture abortedFuture = Channels.future(channel);
private final ChannelFuture finishedFuture = Channels.future(channel);
private final ChannelFuture disposedFuture = Channels.future(channel);
private final DefaultChannelGroup serverChannels = new DefaultChannelGroup();
private final DefaultChannelGroup clientChannels = new DefaultChannelGroup();
private Configuration configuration;
private ChannelFuture preparedFuture;
private final ChannelAddressFactory addressFactory;
private final BootstrapFactory bootstrapFactory;
private ScriptProgress progress;
private final ChannelHandler closeOnExceptionHandler = new CloseOnExceptionHandler();
private final ConcurrentMap<String, Barrier> barriersByName = new ConcurrentHashMap<String, Barrier>();
public Robot() {
this.addressFactory = newChannelAddressFactory();
this.bootstrapFactory =
newBootstrapFactory(Collections.<Class<?>, Object>singletonMap(ChannelAddressFactory.class, addressFactory));
ChannelFutureListener stopConfigurationListener = createStopConfigurationListener();
this.abortedFuture.addListener(stopConfigurationListener);
this.finishedFuture.addListener(stopConfigurationListener);
}
public ChannelFuture getPreparedFuture() {
return preparedFuture;
}
public ChannelFuture getStartedFuture() {
return startedFuture;
}
public ChannelFuture prepare(String expectedScript) throws Exception {
if (preparedFuture != null) {
throw new IllegalStateException("Script already prepared");
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Expected script:\n" + expectedScript);
}
final ScriptParser parser = new Parser();
AstScriptNode scriptAST = parser.parse(new ByteArrayInputStream(expectedScript.getBytes(UTF_8)));
final ScriptValidator validator = new ScriptValidator();
validator.validate(scriptAST);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Parsed script:\n" + scriptAST);
}
RegionInfo scriptInfo = scriptAST.getRegionInfo();
progress = new ScriptProgress(scriptInfo, expectedScript);
final GenerateConfigurationVisitor visitor = new GenerateConfigurationVisitor(bootstrapFactory, addressFactory);
configuration = scriptAST.accept(visitor, new GenerateConfigurationVisitor.State(barriersByName));
preparedFuture = prepareConfiguration();
return preparedFuture;
}
// ONLY used for testing, TODO, remove and use TestSpecification instead
ChannelFuture prepareAndStart(String script) throws Exception {
ChannelFuture preparedFuture = prepare(script);
preparedFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
start();
}
});
return startedFuture;
}
public ChannelFuture start() throws Exception {
if (preparedFuture == null || !preparedFuture.isDone()) {
throw new IllegalStateException("Script has not been prepared or is still preparing");
} else if (startedFuture.isDone()) {
throw new IllegalStateException("Script has already been started");
}
// ensure prepare has completed before start can progress
preparedFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
try {
startConfiguration();
startedFuture.setSuccess();
} catch (Exception ex) {
startedFuture.setFailure(ex);
}
}
});
return startedFuture;
}
public ChannelFuture abort() {
abortedFuture.setSuccess();
return finishedFuture;
}
public ChannelFuture finish() {
return finishedFuture;
}
public String getObservedScript() {
return (progress != null) ? progress.getObservedScript() : null;
}
public ChannelFuture dispose() {
if (preparedFuture == null) {
// no need to clean up if never started
disposedFuture.setSuccess();
} else if (!disposedFuture.isDone()) {
ChannelFuture future = abort();
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
// avoid I/O deadlock checker
new Thread(new Runnable() {
public void run() {
// close server and client channels
// final ChannelGroupFuture closeFuture =
serverChannels.close().addListener(new ChannelGroupFutureListener() {
@Override
public void operationComplete(ChannelGroupFuture future) throws Exception {
clientChannels.close();
try {
bootstrapFactory.shutdown();
bootstrapFactory.releaseExternalResources();
for (AutoCloseable resource : configuration.getResources()) {
try {
resource.close();
} catch (Exception e) {
// ignore
}
}
} catch (Exception e) {
if (LOGGER.isDebugEnabled()) {
LOGGER.error("Caught exception releasing resources", e);
}
} finally {
disposedFuture
.setFailure(new Throwable("Disposed due to shutdown of channel, not due to command"));
}
}
});
}
}).start();
}
});
}
return disposedFuture;
}
private ChannelFuture prepareConfiguration() throws Exception {
List<ChannelFuture> completionFutures = new ArrayList<>();
ChannelFutureListener streamCompletionListener = createStreamCompletionListener();
for (ChannelPipeline pipeline : configuration.getClientAndServerPipelines()) {
CompletionHandler completionHandler = pipeline.get(CompletionHandler.class);
ChannelFuture completionFuture = completionHandler.getHandlerFuture();
completionFutures.add(completionFuture);
completionFuture.addListener(streamCompletionListener);
}
ChannelFuture executionFuture = new CompositeChannelFuture<>(channel, completionFutures);
ChannelFutureListener executionListener = createScriptCompletionListener();
executionFuture.addListener(executionListener);
return prepareServers();
}
private ChannelFuture prepareServers() throws Exception {
/* Accept's ... Robot acting as a server */
for (ServerBootstrapResolver serverResolver : configuration.getServerResolvers()) {
ServerBootstrap server = serverResolver.resolve();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Binding to address " + 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.
RegionInfo regionInfo = (RegionInfo) server.getOption("regionInfo");
bindFuture.addListener(createBindCompleteListener(regionInfo, serverResolver.getNotifyBarrier()));
}
return new CompositeChannelFuture<>(channel, bindFutures);
}
private void startConfiguration() throws Exception {
/* Connect to any clients */
for (final ClientBootstrapResolver clientResolver : configuration.getClientResolvers()) {
Barrier awaitBarrier = clientResolver.getAwaitBarrier();
if (awaitBarrier != null) {
awaitBarrier.getFuture().addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
connectClient(clientResolver);
}
});
} else {
connectClient(clientResolver);
}
}
}
private void connectClient(ClientBootstrapResolver clientResolver) throws Exception {
final RegionInfo regionInfo = clientResolver.getRegionInfo();
ClientBootstrap client = clientResolver.resolve();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("[id: ] connect " + client.getOption("remoteAddress"));
}
ChannelFuture connectFuture = client.connect();
connectFutures.add(connectFuture);
clientChannels.add(connectFuture.getChannel());
connectFuture.addListener(createConnectCompleteListener(regionInfo));
}
private void stopConfiguration() throws Exception {
if (configuration == null) {
// abort received but script not prepared, therefore entire script failed
if (progress == null) {
progress = new ScriptProgress(newSequential(0, 0), "");
}
RegionInfo scriptInfo = progress.getScriptInfo();
progress.addScriptFailure(scriptInfo);
} else {
// stopping the configuration will implicitly trigger the script complete listener
// to handle incomplete script that is being aborted by canceling the finish future
// clear out the pipelines for new connections to avoid impacting the observed script
for (ServerBootstrapResolver serverResolver : configuration.getServerResolvers()) {
try {
ServerBootstrap server = serverResolver.resolve();
server.setPipelineFactory(pipelineFactory(pipeline(closeOnExceptionHandler)));
} catch (RuntimeException e) {
LOGGER.warn("Exception caught while trying to stop server pipelies", e);
}
}
for (ClientBootstrapResolver clientResolver : configuration.getClientResolvers()) {
try {
ClientBootstrap client = clientResolver.resolve();
client.setPipelineFactory(pipelineFactory(pipeline(closeOnExceptionHandler)));
} catch (RuntimeException e) {
LOGGER.warn("Exception caught while trying to stop client pipelies", e);
}
}
// remove each handler from the configuration pipelines
// this will trigger failures for any handlers on a pipeline for an incomplete stream
// including pipelines not yet associated with any channel
for (ChannelPipeline pipeline : configuration.getClientAndServerPipelines()) {
stopStream(pipeline);
}
// cancel any pending binds and connects
for (ChannelFuture bindFuture : bindFutures) {
bindFuture.cancel();
}
for (ChannelFuture connectFuture : connectFutures) {
if (connectFuture.cancel()) {
LOGGER.debug("Cancelled connect future: " + connectFuture.getChannel().getRemoteAddress());
}
}
}
}
private void stopStream(final ChannelPipeline pipeline) {
if (pipeline.isAttached()) {
// avoid race between pipeline clean up and channel events on same pipeline
// by executing the pipeline clean up on the I/O worker thread
pipeline.execute(new Runnable() {
@Override
public void run() {
stopStreamAligned(pipeline);
}
});
} else {
// no race if not attached
stopStreamAligned(pipeline);
}
}
private void stopStreamAligned(final ChannelPipeline pipeline) {
LOGGER.debug("Stopping pipeline");
for (ChannelHandler handler : pipeline.toMap().values()) {
if (LOGGER.isDebugEnabled()) {
Channel channel = pipeline.getChannel();
int id = (channel != null) ? channel.getId() : 0;
LOGGER.debug(format("[id: 0x%08x] %s", id, handler));
}
// note: removing this handler can trigger script completion
// which in turn can re-attempt to stop this pipeline
pipeline.remove(handler);
}
// non-empty pipeline required to avoid warnings
if (pipeline.getContext(closeOnExceptionHandler) == null) {
pipeline.addLast("closeOnException", closeOnExceptionHandler);
}
}
private ChannelFutureListener createBindCompleteListener(final RegionInfo regionInfo, final Barrier notifyBarrier) {
return new ChannelFutureListener() {
@Override
public void operationComplete(final ChannelFuture bindFuture) throws Exception {
Channel boundChannel = bindFuture.getChannel();
SocketAddress localAddress = boundChannel.getLocalAddress();
if (bindFuture.isSuccess()) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Successfully bound to " + localAddress);
}
if (notifyBarrier != null) {
ChannelFuture barrierFuture = notifyBarrier.getFuture();
barrierFuture.setSuccess();
}
} else {
Throwable cause = bindFuture.getCause();
String message = format("accept failed: %s", cause.getMessage());
progress.addScriptFailure(regionInfo, message);
// fail each pipeline that required this bind to succeed
List<ChannelPipeline> acceptedPipelines = configuration.getServerPipelines(regionInfo);
for (ChannelPipeline acceptedPipeline : acceptedPipelines) {
stopStream(acceptedPipeline);
}
}
}
};
}
private ChannelFutureListener createConnectCompleteListener(final RegionInfo regionInfo) {
return new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture connectFuture) throws Exception {
if (connectFuture.isCancelled()) {
// This is more that the connect never really fired, as in the case of a barrier, or the the connect
// is still in process here, so an empty line annotates that it did not do a connect, an actual
// connect
// failure should fail the future
progress.addScriptFailure(regionInfo, "");
} else if (!connectFuture.isSuccess()) {
Throwable cause = connectFuture.getCause();
String message = format("connect failed: %s", cause.getMessage());
progress.addScriptFailure(regionInfo, message);
}
}
};
}
private ChannelFutureListener createStreamCompletionListener() {
return new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture completionFuture) throws Exception {
if (!completionFuture.isSuccess()) {
Throwable cause = completionFuture.getCause();
if (cause instanceof ScriptProgressException) {
ScriptProgressException exception = (ScriptProgressException) cause;
progress.addScriptFailure(exception.getRegionInfo(), exception.getMessage());
} else {
LOGGER.warn("Unexpected exception", cause);
}
}
}
};
}
private ChannelFutureListener createScriptCompletionListener() {
ChannelFutureListener executionListener = new ChannelFutureListener() {
@Override
public void operationComplete(final ChannelFuture future) throws Exception {
if (LOGGER.isDebugEnabled()) {
// detect observed script
String observedScript = progress.getObservedScript();
LOGGER.debug("Observed script:\n" + observedScript);
}
if (abortedFuture.isDone()) {
// abort complete, trigger finished future
finishedFuture.setSuccess();
} else {
// execution complete, trigger finished future
finishedFuture.setSuccess();
}
}
};
return executionListener;
}
private ChannelFutureListener createStopConfigurationListener() {
return new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
stopConfiguration();
}
};
}
@Sharable
private static final class CloseOnExceptionHandler extends SimpleChannelHandler {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
// avoid stack overflow when exception happens on close
if (TRUE != ctx.getAttachment()) {
ctx.setAttachment(TRUE);
// close channel and avoid warning logged by default exceptionCaught implementation
Channel channel = ctx.getChannel();
channel.close();
} else {
// log exception during close
super.exceptionCaught(ctx, e);
}
}
@Override
public String toString() {
return "close-on-exception";
}
}
public Map<String, Barrier> getBarriersByName() {
return barriersByName;
}
public void notifyBarrier(String barrierName) throws Exception {
final Barrier barrier = barriersByName.get(barrierName);
if (barrier == null) {
throw new Exception("Can not notify a barrier that does not exist in the script: " + barrierName);
}
barrier.getFuture().setSuccess();
}
public ChannelFuture awaitBarrier(String barrierName) throws Exception {
final Barrier barrier = barriersByName.get(barrierName);
if (barrier == null) {
throw new Exception("Can not notify a barrier that does not exist in the script: " + barrierName);
}
return barrier.getFuture();
}
}