BUG-732 Detect commit failure (and also put/delete/merge) in netconf-connector 71/9471/2
authorMaros Marsalek <mmarsale@cisco.com>
Tue, 29 Jul 2014 12:44:50 +0000 (14:44 +0200)
committerMaros Marsalek <mmarsale@cisco.com>
Fri, 1 Aug 2014 08:56:01 +0000 (10:56 +0200)
When a failed rpc is detected, issue a discard changes rpc to keep device (candidate) configuration consistant (if candidate is supported).

Change-Id: Ia752ee33043b3159fd71b01eef34e6e79baf18be
Signed-off-by: Maros Marsalek <mmarsale@cisco.com>
opendaylight/md-sal/sal-netconf-connector/src/main/java/org/opendaylight/controller/sal/connect/netconf/listener/NetconfSessionCapabilities.java
opendaylight/md-sal/sal-netconf-connector/src/main/java/org/opendaylight/controller/sal/connect/netconf/sal/NetconfDeviceDataBroker.java
opendaylight/md-sal/sal-netconf-connector/src/main/java/org/opendaylight/controller/sal/connect/netconf/sal/tx/NetconfDeviceReadOnlyTx.java
opendaylight/md-sal/sal-netconf-connector/src/main/java/org/opendaylight/controller/sal/connect/netconf/sal/tx/NetconfDeviceWriteOnlyTx.java
opendaylight/md-sal/sal-netconf-connector/src/main/java/org/opendaylight/controller/sal/connect/netconf/util/NetconfMessageTransformUtil.java
opendaylight/md-sal/sal-netconf-connector/src/test/java/org/opendaylight/controller/sal/connect/netconf/sal/tx/NetconfDeviceWriteOnlyTxTest.java [new file with mode: 0644]

index 7800ae6..1a7d90e 100644 (file)
@@ -96,6 +96,10 @@ public final class NetconfSessionCapabilities {
         return containsNonModuleCapability(NetconfMessageTransformUtil.NETCONF_ROLLBACK_ON_ERROR_URI.toString());
     }
 
+    public boolean isCandidateSupported() {
+        return containsNonModuleCapability(NetconfMessageTransformUtil.NETCONF_CANDIDATE_URI.toString());
+    }
+
     public boolean isMonitoringSupported() {
         return containsModuleCapability(NetconfMessageTransformUtil.IETF_NETCONF_MONITORING)
                 || containsNonModuleCapability(NetconfMessageTransformUtil.IETF_NETCONF_MONITORING.getNamespace().toString());
index 817fc29..f3a9acd 100644 (file)
@@ -52,8 +52,7 @@ final class NetconfDeviceDataBroker implements DOMDataBroker {
 
     @Override
     public DOMDataWriteTransaction newWriteOnlyTransaction() {
-        // FIXME detect if candidate is supported, true is hardcoded
-        return new NetconfDeviceWriteOnlyTx(id, rpc, normalizer, true, netconfSessionPreferences.isRollbackSupported());
+        return new NetconfDeviceWriteOnlyTx(id, rpc, normalizer, netconfSessionPreferences.isCandidateSupported(), netconfSessionPreferences.isRollbackSupported());
     }
 
     @Override
index 03ee2d6..9ef44f6 100644 (file)
@@ -34,6 +34,7 @@ import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+
 public final class NetconfDeviceReadOnlyTx implements DOMDataReadOnlyTransaction {
 
     private static final Logger LOG  = LoggerFactory.getLogger(NetconfDeviceReadOnlyTx.class);
index 6f6e814..87f5477 100644 (file)
@@ -8,10 +8,11 @@
 
 package org.opendaylight.controller.sal.connect.netconf.sal.tx;
 
+import static org.opendaylight.controller.sal.connect.netconf.util.NetconfMessageTransformUtil.DISCARD_CHANGES_RPC_CONTENT;
 import static org.opendaylight.controller.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_CANDIDATE_QNAME;
-import static org.opendaylight.controller.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_COMMIT_QNAME;
 import static org.opendaylight.controller.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_CONFIG_QNAME;
 import static org.opendaylight.controller.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_DEFAULT_OPERATION_QNAME;
+import static org.opendaylight.controller.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_DISCARD_CHANGES_QNAME;
 import static org.opendaylight.controller.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_EDIT_CONFIG_QNAME;
 import static org.opendaylight.controller.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_ERROR_OPTION_QNAME;
 import static org.opendaylight.controller.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_OPERATION_QNAME;
@@ -26,13 +27,14 @@ import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.util.concurrent.CheckedFuture;
+import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ExecutionException;
-import javax.annotation.Nullable;
+import java.util.concurrent.atomic.AtomicBoolean;
 import org.opendaylight.controller.md.sal.common.api.TransactionStatus;
 import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
 import org.opendaylight.controller.md.sal.common.api.data.TransactionCommitFailedException;
@@ -57,65 +59,81 @@ import org.opendaylight.yangtools.yang.data.impl.util.CompositeNodeBuilder;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class NetconfDeviceWriteOnlyTx implements DOMDataWriteTransaction {
+public class NetconfDeviceWriteOnlyTx implements DOMDataWriteTransaction, FutureCallback<RpcResult<TransactionStatus>> {
 
     private static final Logger LOG  = LoggerFactory.getLogger(NetconfDeviceWriteOnlyTx.class);
 
     private final RemoteDeviceId id;
     private final RpcImplementation rpc;
     private final DataNormalizer normalizer;
+
     private final boolean rollbackSupported;
+    private final boolean candidateSupported;
     private final CompositeNode targetNode;
 
+    // Allow commit to be called only once
+    private final AtomicBoolean finished = new AtomicBoolean(false);
+
     public NetconfDeviceWriteOnlyTx(final RemoteDeviceId id, final RpcImplementation rpc, final DataNormalizer normalizer, final boolean candidateSupported, final boolean rollbackOnErrorSupported) {
         this.id = id;
         this.rpc = rpc;
         this.normalizer = normalizer;
-        this.targetNode = getTargetNode(candidateSupported);
+
+        this.candidateSupported = candidateSupported;
+        this.targetNode = getTargetNode(this.candidateSupported);
         this.rollbackSupported = rollbackOnErrorSupported;
     }
 
-    // FIXME add logging
-
     @Override
     public boolean cancel() {
-        if(isCommitted()) {
+        if(isFinished()) {
             return false;
         }
 
         return discardChanges();
     }
 
-    private boolean isCommitted() {
-        // TODO 732
-        return true;
+    private boolean isFinished() {
+        return finished.get();
     }
 
     private boolean discardChanges() {
-        // TODO 732
+        finished.set(true);
+
+        if(candidateSupported) {
+            sendDiscardChanges();
+        }
         return true;
     }
 
     // TODO should the edit operations be blocking ?
+    // TODO should the discard-changes operations be blocking ?
 
     @Override
     public void put(final LogicalDatastoreType store, final YangInstanceIdentifier path, final NormalizedNode<?, ?> data) {
-        Preconditions.checkArgument(store == LogicalDatastoreType.CONFIGURATION, "Can replaceModuleCaps only configuration, not %s", store);
+        checkNotFinished();
+        Preconditions.checkArgument(store == LogicalDatastoreType.CONFIGURATION, "Can merge only configuration, not %s", store);
 
         try {
             final YangInstanceIdentifier legacyPath = NetconfDeviceReadOnlyTx.toLegacyPath(normalizer, path, id);
             final CompositeNode legacyData = normalizer.toLegacy(path, data);
-            sendEditRpc(createEditConfigStructure(legacyPath, Optional.of(ModifyAction.REPLACE), Optional.fromNullable(legacyData)), Optional.of(ModifyAction.NONE));
+            sendEditRpc(
+                    createEditConfigStructure(legacyPath, Optional.of(ModifyAction.REPLACE), Optional.fromNullable(legacyData)), Optional.of(ModifyAction.NONE));
         } catch (final ExecutionException e) {
-            LOG.warn("Error putting data to {}, data: {}, discarding changes", path, data, e);
+            LOG.warn("{}: Error putting data to {}, data: {}, discarding changes", id, path, data, e);
             discardChanges();
-            throw new RuntimeException("Error while replacing " + path, e);
+            throw new RuntimeException(id + ": Error while replacing " + path, e);
         }
     }
 
+    private void checkNotFinished() {
+        Preconditions.checkState(isFinished() == false, "%s: Transaction %s already finished", id, getIdentifier());
+    }
+
     @Override
     public void merge(final LogicalDatastoreType store, final YangInstanceIdentifier path, final NormalizedNode<?, ?> data) {
-        Preconditions.checkArgument(store == LogicalDatastoreType.CONFIGURATION, "Can replaceModuleCaps only configuration, not %s", store);
+        checkNotFinished();
+        Preconditions.checkArgument(store == LogicalDatastoreType.CONFIGURATION, "%s: Can merge only configuration, not %s", id, store);
 
         try {
             final YangInstanceIdentifier legacyPath = NetconfDeviceReadOnlyTx.toLegacyPath(normalizer, path, id);
@@ -123,31 +141,32 @@ public class NetconfDeviceWriteOnlyTx implements DOMDataWriteTransaction {
             sendEditRpc(
                     createEditConfigStructure(legacyPath, Optional.<ModifyAction> absent(), Optional.fromNullable(legacyData)), Optional.<ModifyAction> absent());
         } catch (final ExecutionException e) {
-            LOG.warn("Error merging data to {}, data: {}, discarding changes", path, data, e);
+            LOG.warn("{}: Error merging data to {}, data: {}, discarding changes", id, path, data, e);
             discardChanges();
-            throw new RuntimeException("Error while merging " + path, e);
+            throw new RuntimeException(id + ": Error while merging " + path, e);
         }
     }
 
     @Override
     public void delete(final LogicalDatastoreType store, final YangInstanceIdentifier path) {
-        Preconditions.checkArgument(store == LogicalDatastoreType.CONFIGURATION, "Can replaceModuleCaps only configuration, not %s", store);
+        checkNotFinished();
+        Preconditions.checkArgument(store == LogicalDatastoreType.CONFIGURATION, "%s: Can merge only configuration, not %s", id, store);
 
         try {
-            sendEditRpc(createEditConfigStructure(NetconfDeviceReadOnlyTx.toLegacyPath(normalizer, path, id), Optional.of(ModifyAction.DELETE), Optional.<CompositeNode>absent()), Optional.of(ModifyAction.NONE));
+            sendEditRpc(
+                    createEditConfigStructure(NetconfDeviceReadOnlyTx.toLegacyPath(normalizer, path, id), Optional.of(ModifyAction.DELETE), Optional.<CompositeNode>absent()), Optional.of(ModifyAction.NONE));
         } catch (final ExecutionException e) {
-            LOG.warn("Error deleting data {}, discarding changes", path, e);
+            LOG.warn("{}: Error deleting data {}, discarding changes", id, path, e);
             discardChanges();
-            throw new RuntimeException("Error while deleting " + path, e);
+            throw new RuntimeException(id + ": Error while deleting " + path, e);
         }
     }
 
     @Override
     public CheckedFuture<Void, TransactionCommitFailedException> submit() {
         final ListenableFuture<Void> commmitFutureAsVoid = Futures.transform(commit(), new Function<RpcResult<TransactionStatus>, Void>() {
-            @Nullable
             @Override
-            public Void apply(@Nullable final RpcResult<TransactionStatus> input) {
+            public Void apply(final RpcResult<TransactionStatus> input) {
                 return null;
             }
         });
@@ -162,25 +181,46 @@ public class NetconfDeviceWriteOnlyTx implements DOMDataWriteTransaction {
 
     @Override
     public ListenableFuture<RpcResult<TransactionStatus>> commit() {
-        // FIXME do not allow commit if closed or failed
+        checkNotFinished();
+        finished.set(true);
 
-        final ListenableFuture<RpcResult<CompositeNode>> rpcResult = rpc.invokeRpc(NetconfMessageTransformUtil.NETCONF_COMMIT_QNAME, getCommitRequest());
-        return Futures.transform(rpcResult, new Function<RpcResult<CompositeNode>, RpcResult<TransactionStatus>>() {
-            @Override
-            public RpcResult<TransactionStatus> apply(@Nullable final RpcResult<CompositeNode> input) {
-                if(input.isSuccessful()) {
-                    return RpcResultBuilder.success(TransactionStatus.COMMITED).build();
-                } else {
-                    final RpcResultBuilder<TransactionStatus> failed = RpcResultBuilder.failed();
-                    for (final RpcError rpcError : input.getErrors()) {
-                        failed.withError(rpcError.getErrorType(), rpcError.getTag(), rpcError.getMessage(), rpcError.getApplicationTag(), rpcError.getInfo(), rpcError.getCause());
+        if(candidateSupported == false) {
+            return Futures.immediateFuture(RpcResultBuilder.success(TransactionStatus.COMMITED).build());
+        }
+
+        final ListenableFuture<RpcResult<CompositeNode>> rpcResult = rpc.invokeRpc(
+                NetconfMessageTransformUtil.NETCONF_COMMIT_QNAME, NetconfMessageTransformUtil.COMMIT_RPC_CONTENT);
+
+        final ListenableFuture<RpcResult<TransactionStatus>> transformed = Futures.transform(rpcResult,
+                new Function<RpcResult<CompositeNode>, RpcResult<TransactionStatus>>() {
+                    @Override
+                    public RpcResult<TransactionStatus> apply(final RpcResult<CompositeNode> input) {
+                        if (input.isSuccessful()) {
+                            return RpcResultBuilder.success(TransactionStatus.COMMITED).build();
+                        } else {
+                            final RpcResultBuilder<TransactionStatus> failed = RpcResultBuilder.failed();
+                            for (final RpcError rpcError : input.getErrors()) {
+                                failed.withError(rpcError.getErrorType(), rpcError.getTag(), rpcError.getMessage(),
+                                        rpcError.getApplicationTag(), rpcError.getInfo(), rpcError.getCause());
+                            }
+                            return failed.build();
+                        }
                     }
-                    return failed.build();
-                }
-            }
-        });
+                });
 
-        // FIXME 732 detect commit failure
+        Futures.addCallback(transformed, this);
+        return transformed;
+    }
+
+    @Override
+    public void onSuccess(final RpcResult<TransactionStatus> result) {
+        LOG.debug("{}: Write successful, transaction: {}", id, getIdentifier());
+    }
+
+    @Override
+    public void onFailure(final Throwable t) {
+        LOG.warn("{}: Write failed, transaction {}, discarding changes", id, getIdentifier(), t);
+        discardChanges();
     }
 
     private void sendEditRpc(final CompositeNode editStructure, final Optional<ModifyAction> defaultOperation) throws ExecutionException {
@@ -200,6 +240,22 @@ public class NetconfDeviceWriteOnlyTx implements DOMDataWriteTransaction {
         }
     }
 
+    private void sendDiscardChanges() {
+        final ListenableFuture<RpcResult<CompositeNode>> discardFuture = rpc.invokeRpc(NETCONF_DISCARD_CHANGES_QNAME, DISCARD_CHANGES_RPC_CONTENT);
+        Futures.addCallback(discardFuture, new FutureCallback<RpcResult<CompositeNode>>() {
+            @Override
+            public void onSuccess(final RpcResult<CompositeNode> result) {
+                LOG.debug("{}: Discarding transaction: {}", id, getIdentifier());
+            }
+
+            @Override
+            public void onFailure(final Throwable t) {
+                LOG.error("{}: Discarding changes failed, transaction: {}. Device configuration might be corrupted", id, getIdentifier(), t);
+                throw new RuntimeException(id + ": Discarding changes failed, transaction " + getIdentifier(), t);
+            }
+        });
+    }
+
     private CompositeNode createEditConfigStructure(final YangInstanceIdentifier dataPath, final Optional<ModifyAction> operation,
                                                     final Optional<CompositeNode> lastChildOverride) {
         Preconditions.checkArgument(Iterables.isEmpty(dataPath.getPathArguments()) == false, "Instance identifier with empty path %s", dataPath);
@@ -298,13 +354,6 @@ public class NetconfDeviceWriteOnlyTx implements DOMDataWriteTransaction {
         }
     }
 
-    private ImmutableCompositeNode getCommitRequest() {
-        final CompositeNodeBuilder<ImmutableCompositeNode> commitInput = ImmutableCompositeNode.builder();
-        commitInput.setQName(NETCONF_COMMIT_QNAME);
-        return commitInput.toInstance();
-    }
-
-
     @Override
     public Object getIdentifier() {
         return this;
index a6924d9..d3faddd 100644 (file)
@@ -35,6 +35,7 @@ import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.Node;
 import org.opendaylight.yangtools.yang.data.impl.CompositeNodeTOImpl;
 import org.opendaylight.yangtools.yang.data.impl.ImmutableCompositeNode;
+import org.opendaylight.yangtools.yang.data.impl.NodeFactory;
 import org.opendaylight.yangtools.yang.data.impl.SimpleNodeTOImpl;
 import org.opendaylight.yangtools.yang.data.impl.util.CompositeNodeBuilder;
 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
@@ -45,8 +46,7 @@ import org.w3c.dom.Element;
 
 public class NetconfMessageTransformUtil {
 
-    private NetconfMessageTransformUtil() {
-    }
+    private NetconfMessageTransformUtil() {}
 
     public static final QName IETF_NETCONF_MONITORING = QName.create("urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring", "2010-10-04", "ietf-netconf-monitoring");
     public static URI NETCONF_URI = URI.create("urn:ietf:params:xml:ns:netconf:base:1.0");
@@ -66,14 +66,27 @@ public class NetconfMessageTransformUtil {
     public static QName NETCONF_DEFAULT_OPERATION_QNAME = QName.create(NETCONF_OPERATION_QNAME, "default-operation");
     public static QName NETCONF_EDIT_CONFIG_QNAME = QName.create(NETCONF_QNAME, "edit-config");
     public static QName NETCONF_GET_CONFIG_QNAME = QName.create(NETCONF_QNAME, "get-config");
+    public static QName NETCONF_DISCARD_CHANGES_QNAME = QName.create(NETCONF_QNAME, "discard-changes");
     public static QName NETCONF_TYPE_QNAME = QName.create(NETCONF_QNAME, "type");
     public static QName NETCONF_FILTER_QNAME = QName.create(NETCONF_QNAME, "filter");
     public static QName NETCONF_GET_QNAME = QName.create(NETCONF_QNAME, "get");
     public static QName NETCONF_RPC_QNAME = QName.create(NETCONF_QNAME, "rpc");
+
     public static URI NETCONF_ROLLBACK_ON_ERROR_URI = URI
             .create("urn:ietf:params:netconf:capability:rollback-on-error:1.0");
     public static String ROLLBACK_ON_ERROR_OPTION = "rollback-on-error";
 
+    public static URI NETCONF_CANDIDATE_URI = URI
+            .create("urn:ietf:params:netconf:capability:candidate:1.0");
+
+    // Discard changes message
+    public static final CompositeNode DISCARD_CHANGES_RPC_CONTENT =
+            NodeFactory.createImmutableCompositeNode(NETCONF_DISCARD_CHANGES_QNAME, null, Collections.<Node<?>>emptyList());
+
+    // Commit changes message
+    public static final CompositeNode COMMIT_RPC_CONTENT =
+            NodeFactory.createImmutableCompositeNode(NETCONF_COMMIT_QNAME, null, Collections.<Node<?>>emptyList());
+
     public static Node<?> toFilterStructure(final YangInstanceIdentifier identifier) {
         Node<?> previous = null;
         if (Iterables.isEmpty(identifier.getPathArguments())) {
@@ -269,5 +282,4 @@ public class NetconfMessageTransformUtil {
             return it.toInstance();
         }
     }
-
 }
diff --git a/opendaylight/md-sal/sal-netconf-connector/src/test/java/org/opendaylight/controller/sal/connect/netconf/sal/tx/NetconfDeviceWriteOnlyTxTest.java b/opendaylight/md-sal/sal-netconf-connector/src/test/java/org/opendaylight/controller/sal/connect/netconf/sal/tx/NetconfDeviceWriteOnlyTxTest.java
new file mode 100644 (file)
index 0000000..a65e426
--- /dev/null
@@ -0,0 +1,79 @@
+package org.opendaylight.controller.sal.connect.netconf.sal.tx;
+
+import static junit.framework.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.opendaylight.controller.sal.connect.netconf.util.NetconfMessageTransformUtil.DISCARD_CHANGES_RPC_CONTENT;
+
+import com.google.common.util.concurrent.CheckedFuture;
+import com.google.common.util.concurrent.Futures;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
+import org.opendaylight.controller.md.sal.common.api.data.TransactionCommitFailedException;
+import org.opendaylight.controller.md.sal.common.impl.util.compat.DataNormalizer;
+import org.opendaylight.controller.sal.connect.netconf.util.NetconfMessageTransformUtil;
+import org.opendaylight.controller.sal.connect.util.RemoteDeviceId;
+import org.opendaylight.controller.sal.core.api.RpcImplementation;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.common.RpcResult;
+import org.opendaylight.yangtools.yang.common.RpcResultBuilder;
+import org.opendaylight.yangtools.yang.data.api.CompositeNode;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+
+public class NetconfDeviceWriteOnlyTxTest {
+
+    private final RemoteDeviceId id = new RemoteDeviceId("test-mount");
+
+    @Mock
+    private RpcImplementation rpc;
+    @Mock
+    private DataNormalizer normalizer;
+    private YangInstanceIdentifier yangIId;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        doReturn(Futures.<RpcResult<CompositeNode>>immediateFailedFuture(new IllegalStateException("Failed tx")))
+        .doReturn(Futures.immediateFuture(RpcResultBuilder.<CompositeNode>success().build()))
+                .when(rpc).invokeRpc(any(QName.class), any(CompositeNode.class));
+
+        yangIId = YangInstanceIdentifier.builder().node(QName.create("namespace", "2012-12-12", "name")).build();
+        doReturn(yangIId).when(normalizer).toLegacy(yangIId);
+    }
+
+    @Test
+    public void testDiscardCahnges() {
+        final NetconfDeviceWriteOnlyTx tx = new NetconfDeviceWriteOnlyTx(id, rpc, normalizer, true, true);
+        final CheckedFuture<Void, TransactionCommitFailedException> submitFuture = tx.submit();
+        try {
+            submitFuture.checkedGet();
+        } catch (final TransactionCommitFailedException e) {
+            // verify discard changes was sent
+            verify(rpc).invokeRpc(NetconfMessageTransformUtil.NETCONF_DISCARD_CHANGES_QNAME, DISCARD_CHANGES_RPC_CONTENT);
+            return;
+        }
+
+        fail("Submit should fail");
+    }
+
+
+    @Test
+    public void testDiscardCahngesNotSentWithoutCandidate() {
+        doReturn(Futures.immediateFuture(RpcResultBuilder.<CompositeNode>success().build()))
+        .doReturn(Futures.<RpcResult<CompositeNode>>immediateFailedFuture(new IllegalStateException("Failed tx")))
+                .when(rpc).invokeRpc(any(QName.class), any(CompositeNode.class));
+
+        final NetconfDeviceWriteOnlyTx tx = new NetconfDeviceWriteOnlyTx(id, rpc, normalizer, false, true);
+        tx.delete(LogicalDatastoreType.CONFIGURATION, yangIId);
+        verify(rpc).invokeRpc(eq(NetconfMessageTransformUtil.NETCONF_EDIT_CONFIG_QNAME), any(CompositeNode.class));
+        verifyNoMoreInteractions(rpc);
+    }
+
+}