Merge "Fix improper cleanup of operational data in sal-netconf-connector's disconnect"
authorTony Tkacik <ttkacik@cisco.com>
Mon, 8 Dec 2014 09:08:11 +0000 (09:08 +0000)
committerGerrit Code Review <gerrit@opendaylight.org>
Mon, 8 Dec 2014 09:08:11 +0000 (09:08 +0000)
13 files changed:
features/mdsal/pom.xml
opendaylight/commons/protocol-framework/src/main/java/org/opendaylight/protocol/framework/AbstractProtocolSession.java
opendaylight/commons/protocol-framework/src/main/java/org/opendaylight/protocol/framework/ReconnectPromise.java
opendaylight/md-sal/sal-distributed-datastore/pom.xml
opendaylight/netconf/netconf-impl/src/test/java/org/opendaylight/controller/netconf/impl/NetconfMonitoringServiceImplTest.java
opendaylight/netconf/netconf-impl/src/test/java/org/opendaylight/controller/netconf/impl/mapping/operations/DefaultStopExiTest.java
opendaylight/netconf/netconf-monitoring/src/test/java/org/opendaylight/controller/netconf/monitoring/osgi/NetconfMonitoringActivatorTest.java
opendaylight/netconf/netconf-netty-util/src/main/java/org/opendaylight/controller/netconf/nettyutil/AbstractNetconfSession.java
opendaylight/netconf/netconf-netty-util/src/main/java/org/opendaylight/controller/netconf/nettyutil/handler/ChunkedFramingMechanismEncoder.java
opendaylight/netconf/netconf-netty-util/src/main/java/org/opendaylight/controller/netconf/nettyutil/handler/ssh/client/AsyncSshHandler.java
opendaylight/netconf/netconf-netty-util/src/test/java/org/opendaylight/controller/netconf/nettyutil/AbstractNetconfSessionTest.java
opendaylight/netconf/netconf-netty-util/src/test/java/org/opendaylight/controller/netconf/nettyutil/handler/ChunkedFramingMechanismEncoderTest.java
opendaylight/netconf/netconf-util/src/main/java/org/opendaylight/controller/netconf/util/messages/NetconfMessageConstants.java

index 6f43768a9dbb4a966ce35e37bcf3679cb308ef1e..6159922183432d6e23fd9c0b87eb01591577a2dd 100644 (file)
       <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>
index 47e96d1ff490df392accfb84dc10d55eb84a4583..af196a941a777df18eacc9d2929a3048e57677ab 100644 (file)
@@ -37,6 +37,12 @@ public abstract class AbstractProtocolSession<M> extends SimpleChannelInboundHan
     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
index b2ab27a82671cdc0c2380ac8f1084e540ee61691..aaec95a74b5886150c8d0bc4bb7d11a1a1570dbf 100644 (file)
@@ -47,13 +47,12 @@ final class ReconnectPromise<S extends ProtocolSession<?>, L extends SessionList
         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));
             }
         });
     }
@@ -91,9 +90,7 @@ final class ReconnectPromise<S extends ProtocolSession<?>, L extends SessionList
 
         @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;
             }
index bd42a9cdd9a547ae572b017c4bb3c9ef467b5329..cbaf278a87eda3944bac8c838625165f55667916 100644 (file)
       <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>
index 395e5c03384abad0e988789d5bf5d42f8db42d9c..0d296c5f5269c0662f9b27db9d6ef1973038d8c8 100644 (file)
@@ -98,6 +98,7 @@ public class NetconfMonitoringServiceImplTest {
 
         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));
index c06e78aa997cfaafdc5dd2624d784a33bb047364..aaaf5991d425d762ab1afa8303ae69d86c651c36 100644 (file)
@@ -31,6 +31,7 @@ public class DefaultStopExiTest {
         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);
index 6664f3e733bc399bd08d710dfbed882a36baa24e..792a591512baad9d9088e801c440e05b643fb780 100644 (file)
@@ -40,6 +40,7 @@ public class NetconfMonitoringActivatorTest {
         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
index efa1c731c8a8b239f18dd68d850aff3914bf7b62..fd11ce8c51875e379976917fd722ce6c5797d0ec 100644 (file)
@@ -87,6 +87,7 @@ public abstract class AbstractNetconfSession<S extends NetconfSession, L extends
     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();
     }
index c4cddb802ea35dd51b17d49d832114c404ae5085..2287a58602c7b9fd9638e17e4f26428c75c9f284 100644 (file)
@@ -8,13 +8,12 @@
 
 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;
@@ -27,9 +26,8 @@ public class ChunkedFramingMechanismEncoder extends MessageToByteEncoder<ByteBuf
         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;
     }
 
@@ -38,19 +36,17 @@ public class ChunkedFramingMechanismEncoder extends MessageToByteEncoder<ByteBuf
     }
 
     @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);
     }
 }
index c8c912828279e72eab9ac12dba25e2a29e2c10d4..05cd598cdc22f7b1265c661c447b850cab1a0256 100644 (file)
@@ -176,6 +176,7 @@ public class AsyncSshHandler extends ChannelOutboundHandlerAdapter {
 
     @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);
     }
@@ -187,23 +188,21 @@ public class AsyncSshHandler extends ChannelOutboundHandlerAdapter {
 
     @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
@@ -216,13 +215,17 @@ public class AsyncSshHandler extends ChannelOutboundHandlerAdapter {
             });
         }
 
-        // 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());
     }
index 8199963c8112d8e708c3d4076c23eada3e3604fe..7946afdbf5e1c80add226b08137b1faedd3bf6bf 100644 (file)
@@ -60,6 +60,7 @@ public class AbstractNetconfSessionTest {
 
         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));
index 556bece43f372b28b5c0aea12d096152638dd2a3..4488c5e1be63bc2f82bfb01575df6b5cbaa7be33 100644 (file)
@@ -9,20 +9,16 @@
 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 {
 
@@ -48,30 +44,20 @@ 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) {
index 5c2770a8c1f2387211f4cdcf1a1ef762ef8f11d8..89285d18c021fe4bcaf9eec7dc9b3a4deb662715 100644 (file)
@@ -27,6 +27,7 @@ public final class NetconfMessageConstants {
 
     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);
 
 }