<groupId>org.opendaylight.controller</groupId>
<artifactId>sal-core-api</artifactId>
</dependency>
- <dependency>
- <groupId>org.opendaylight.controller</groupId>
- <artifactId>sal-core-api</artifactId>
- </dependency>
<dependency>
<groupId>org.opendaylight.controller</groupId>
<artifactId>sal-core-spi</artifactId>
public final void channelInactive(final ChannelHandlerContext ctx) {
LOG.debug("Channel {} inactive.", ctx.channel());
endOfInput();
+ try {
+ // Forward channel inactive event, all handlers in pipeline might be interested in the event e.g. close channel handler of reconnect promise
+ super.channelInactive(ctx);
+ } catch (final Exception e) {
+ throw new RuntimeException("Failed to delegate channel inactive event on channel " + ctx.channel(), e);
+ }
}
@Override
pending = this.dispatcher.createClient(this.address, cs, b, new AbstractDispatcher.PipelineInitializer<S>() {
@Override
public void initializeChannel(final SocketChannel channel, final Promise<S> promise) {
- // add closed channel handler
- // This handler has to be added before initializer.initializeChannel is called
- // Initializer might add some handlers using addFirst e.g. AsyncSshHandler and in that case
- // closed channel handler is before the handler that invokes channel inactive event
- channel.pipeline().addFirst(new ClosedChannelHandler(ReconnectPromise.this));
-
initializer.initializeChannel(channel, promise);
+ // add closed channel handler
+ // This handler has to be added as last channel handler and the channel inactive event has to be caught by it
+ // Handlers in front of it can react to channelInactive event, but have to forward the event or the reconnect will not work
+ // This handler is last so all handlers in front of it can handle channel inactive (to e.g. resource cleanup) before a new connection is started
+ channel.pipeline().addLast(new ClosedChannelHandler(ReconnectPromise.this));
}
});
}
@Override
public void channelInactive(final ChannelHandlerContext ctx) throws Exception {
- // Pass info about disconnect further and then reconnect
- super.channelInactive(ctx);
-
+ // This is the ultimate channel inactive handler, not forwarding
if (promise.isCancelled()) {
return;
}
<artifactId>yang-common</artifactId>
</dependency>
- <dependency>
- <groupId>org.opendaylight.yangtools</groupId>
- <artifactId>yang-data-api</artifactId>
- </dependency>
-
<dependency>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.core</artifactId>
NetconfServerSessionListener sessionListener = mock(NetconfServerSessionListener.class);
Channel channel = mock(Channel.class);
+ doReturn("mockChannel").when(channel).toString();
NetconfHelloMessageAdditionalHeader header = new NetconfHelloMessageAdditionalHeader("name", "addr", "2", "tcp", "id");
NetconfServerSession sm = new NetconfServerSession(sessionListener, channel, 10, header);
doNothing().when(sessionListener).onSessionUp(any(NetconfServerSession.class));
DefaultStopExi exi = new DefaultStopExi("");
Document doc = XmlUtil.newDocument();
Channel channel = mock(Channel.class);
+ doReturn("mockChannel").when(channel).toString();
ChannelPipeline pipeline = mock(ChannelPipeline.class);
doReturn(pipeline).when(channel).pipeline();
ChannelHandler channelHandler = mock(ChannelHandler.class);
ServiceReference<?>[] refs = new ServiceReference[2];
doReturn(Arrays.asList(refs)).when(context).getServiceReferences(any(Class.class), anyString());
doReturn(refs).when(context).getServiceReferences(anyString(), anyString());
+ doNothing().when(context).removeServiceListener(any(ServiceListener.class));
}
@Test
public String toString() {
final StringBuffer sb = new StringBuffer(getClass().getSimpleName() + "{");
sb.append("sessionId=").append(sessionId);
+ sb.append(", channel=").append(channel);
sb.append('}');
return sb.toString();
}
package org.opendaylight.controller.netconf.nettyutil.handler;
+import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import io.netty.buffer.ByteBuf;
-import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import org.opendaylight.controller.netconf.util.messages.NetconfMessageConstants;
-import org.opendaylight.controller.netconf.util.messages.NetconfMessageHeader;
public class ChunkedFramingMechanismEncoder extends MessageToByteEncoder<ByteBuf> {
public static final int DEFAULT_CHUNK_SIZE = 8192;
this(DEFAULT_CHUNK_SIZE);
}
- public ChunkedFramingMechanismEncoder(int chunkSize) {
- Preconditions.checkArgument(chunkSize > MIN_CHUNK_SIZE);
- Preconditions.checkArgument(chunkSize < MAX_CHUNK_SIZE);
+ public ChunkedFramingMechanismEncoder(final int chunkSize) {
+ Preconditions.checkArgument(chunkSize >= MIN_CHUNK_SIZE && chunkSize <= MAX_CHUNK_SIZE, "Unsupported chunk size %s", chunkSize);
this.chunkSize = chunkSize;
}
}
@Override
- protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) {
- while (msg.readableBytes() > chunkSize) {
- ByteBuf chunk = Unpooled.buffer(chunkSize);
- chunk.writeBytes(createChunkHeader(chunkSize));
- chunk.writeBytes(msg.readBytes(chunkSize));
- ctx.write(chunk);
- }
- out.writeBytes(createChunkHeader(msg.readableBytes()));
- out.writeBytes(msg.readBytes(msg.readableBytes()));
- out.writeBytes(NetconfMessageConstants.END_OF_CHUNK);
- }
+ protected void encode(final ChannelHandlerContext ctx, final ByteBuf msg, final ByteBuf out) {
+ do {
+ final int xfer = Math.min(chunkSize, msg.readableBytes());
+
+ out.writeBytes(NetconfMessageConstants.START_OF_CHUNK);
+ out.writeBytes(String.valueOf(xfer).getBytes(Charsets.US_ASCII));
+ out.writeByte('\n');
- private ByteBuf createChunkHeader(int chunkSize) {
- return Unpooled.wrappedBuffer(NetconfMessageHeader.toBytes(chunkSize));
+ out.writeBytes(msg, xfer);
+ } while (msg.isReadable());
+
+ out.writeBytes(NetconfMessageConstants.END_OF_CHUNK);
}
}
@Override
public synchronized void connect(final ChannelHandlerContext ctx, final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) throws Exception {
+ LOG.debug("XXX session connecting on channel {}. promise: {} ", ctx.channel(), connectPromise);
this.connectPromise = promise;
startSsh(ctx, remoteAddress);
}
@Override
public synchronized void disconnect(final ChannelHandlerContext ctx, final ChannelPromise promise) {
- // Super disconnect is necessary in this case since we are using NioSocketChannel and it needs to cleanup its resources
- // e.g. Socket that it tries to open in its constructor (https://bugs.opendaylight.org/show_bug.cgi?id=2430)
- // TODO better solution would be to implement custom ChannelFactory + Channel that will use mina SSH lib internally: port this to custom channel implementation
- try {
- super.disconnect(ctx, ctx.newPromise());
- } catch (final Exception e) {
- LOG.warn("Unable to cleanup all resources for channel: {}. Ignoring.", ctx.channel(), e);
- }
+ LOG.trace("Closing SSH session on channel: {} with connect promise in state: {}", ctx.channel(), connectPromise);
- if(sshReadAsyncListener != null) {
- sshReadAsyncListener.close();
+ // If we have already succeeded and the session was dropped after, we need to fire inactive to notify reconnect logic
+ if(connectPromise.isSuccess()) {
+ ctx.fireChannelInactive();
}
if(sshWriteAsyncHandler != null) {
sshWriteAsyncHandler.close();
}
+ if(sshReadAsyncListener != null) {
+ sshReadAsyncListener.close();
+ }
+
if(session!= null && !session.isClosed() && !session.isClosing()) {
session.close(false).addListener(new SshFutureListener<CloseFuture>() {
@Override
});
}
- // If we have already succeeded and the session was dropped after, we need to fire inactive to notify reconnect logic
- if(connectPromise.isSuccess()) {
- ctx.fireChannelInactive();
+ // Super disconnect is necessary in this case since we are using NioSocketChannel and it needs to cleanup its resources
+ // e.g. Socket that it tries to open in its constructor (https://bugs.opendaylight.org/show_bug.cgi?id=2430)
+ // TODO better solution would be to implement custom ChannelFactory + Channel that will use mina SSH lib internally: port this to custom channel implementation
+ try {
+ // Disconnect has to be closed after inactive channel event was fired, because it interferes with it
+ super.disconnect(ctx, ctx.newPromise());
+ } catch (final Exception e) {
+ LOG.warn("Unable to cleanup all resources for channel: {}. Ignoring.", ctx.channel(), e);
}
channel = null;
-
promise.setSuccess();
LOG.debug("SSH session closed on channel: {}", ctx.channel());
}
doReturn(mock(ChannelFuture.class)).when(channel).writeAndFlush(any(NetconfMessage.class));
doReturn(pipeline).when(channel).pipeline();
+ doReturn("mockChannel").when(channel).toString();
doReturn(mock(ChannelFuture.class)).when(channel).close();
doReturn(null).when(pipeline).replace(anyString(), anyString(), any(ChannelHandler.class));
package org.opendaylight.controller.netconf.nettyutil.handler;
import static org.junit.Assert.assertEquals;
-import static org.mockito.Matchers.anyObject;
-import static org.mockito.Mockito.doAnswer;
-import com.google.common.collect.Lists;
+import static org.junit.Assert.assertTrue;
+import com.google.common.base.Charsets;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
-import java.util.List;
+import java.nio.ByteBuffer;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-import org.opendaylight.controller.netconf.util.messages.NetconfMessageConstants;
public class ChunkedFramingMechanismEncoderTest {
@Test
public void testEncode() throws Exception {
- final List<ByteBuf> chunks = Lists.newArrayList();
- doAnswer(new Answer<Object>() {
- @Override
- public Object answer(final InvocationOnMock invocation) throws Throwable {
- chunks.add((ByteBuf) invocation.getArguments()[0]);
- return null;
- }
- }).when(ctx).write(anyObject());
-
final ChunkedFramingMechanismEncoder encoder = new ChunkedFramingMechanismEncoder(chunkSize);
final int lastChunkSize = 20;
final ByteBuf src = Unpooled.wrappedBuffer(getByteArray(chunkSize * 4 + lastChunkSize));
final ByteBuf destination = Unpooled.buffer();
encoder.encode(ctx, src, destination);
- assertEquals(4, chunks.size());
- final int framingSize = "#256\n".getBytes().length + 1/* new line at end */;
+ assertEquals(1077, destination.readableBytes());
- for (final ByteBuf chunk : chunks) {
- assertEquals(chunkSize + framingSize, chunk.readableBytes());
- }
+ byte[] buf = new byte[destination.readableBytes()];
+ destination.readBytes(buf);
+ String s = Charsets.US_ASCII.decode(ByteBuffer.wrap(buf)).toString();
- final int lastFramingSize = "#20\n".length() + NetconfMessageConstants.END_OF_CHUNK.length + 1/* new line at end */;
- assertEquals(lastChunkSize + lastFramingSize, destination.readableBytes());
+ assertTrue(s.startsWith("\n#256\na"));
+ assertTrue(s.endsWith("\n#20\naaaaaaaaaaaaaaaaaaaa\n##\n"));
}
private byte[] getByteArray(final int size) {
public static final int MAX_HEADER_LENGTH = 13;
+ public static final byte[] START_OF_CHUNK = "\n#".getBytes(Charsets.UTF_8);
public static final byte[] END_OF_CHUNK = "\n##\n".getBytes(Charsets.UTF_8);
}