Remove DeleteDataTransactionUtil 12/107212/11
authorRobert Varga <robert.varga@pantheon.tech>
Wed, 2 Aug 2023 12:44:21 +0000 (14:44 +0200)
committerRobert Varga <nite@hq.sk>
Thu, 3 Aug 2023 11:07:46 +0000 (11:07 +0000)
DeleteDataTransactionUtil contains only a single method, which is the
request to delete data from the configuration datastore.

Rather than having a free-standing utility, integrate this request into
RestconfStrategy as a well-defined method. The strategy can then
determine what is the appropriate way to implement it.

In the course of this exercise, we introduce RestconfFuture, which is a
ListenableFuture of a non-null value, which can only fail with a
RestconfDocumentedException.

As a side-effect of this, the MD-SAL implementation is made more
performant by removing an intermediate synchronization point when we
check for presence of target data.

JIRA: NETCONF-1111
Change-Id: I984547691baae19f21e8611a14ca50e4d5298c64
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/errors/RestconfFuture.java [new file with mode: 0644]
restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/errors/SettableRestconfFuture.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfDataServiceImpl.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/MdsalRestconfStrategy.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/NetconfRestconfStrategy.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/RestconfStrategy.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/utils/DeleteDataTransactionUtil.java [deleted file]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/utils/TransactionUtil.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/AbstractRestconfStrategyTest.java

diff --git a/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/errors/RestconfFuture.java b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/errors/RestconfFuture.java
new file mode 100644 (file)
index 0000000..c49fde6
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.common.errors;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.base.Throwables;
+import com.google.common.util.concurrent.AbstractFuture;
+import com.google.common.util.concurrent.ListenableFuture;
+import java.util.concurrent.ExecutionException;
+import org.eclipse.jdt.annotation.NonNull;
+
+/**
+ * A {@link ListenableFuture} specialization, which fails only with {@link RestconfDocumentedException} and does not
+ * produce {@code null} values.
+ *
+ * @param <V> resulting value type
+ */
+public sealed class RestconfFuture<V> extends AbstractFuture<@NonNull V> permits SettableRestconfFuture {
+    RestconfFuture() {
+        // Hidden on purpose
+    }
+
+    public static <V> RestconfFuture<V> of(final V value) {
+        final var future = new RestconfFuture<V>();
+        future.set(requireNonNull(value));
+        return future;
+    }
+
+    public static <V> RestconfFuture<V> failed(final RestconfDocumentedException cause) {
+        final var future = new RestconfFuture<V>();
+        future.setException(requireNonNull(cause));
+        return future;
+    }
+
+    @Override
+    public final boolean cancel(final boolean mayInterruptIfRunning) {
+        return false;
+    }
+
+    /**
+     * Get the result.
+     *
+     * @return The result
+     * @throws RestconfDocumentedException if this future failed or this call is interrupted.
+     */
+    public final @NonNull V getOrThrow() {
+        try {
+            return get();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new RestconfDocumentedException("Interrupted while waiting", e);
+        } catch (ExecutionException e) {
+            Throwables.throwIfInstanceOf(e.getCause(), RestconfDocumentedException.class);
+            throw new RestconfDocumentedException("Operation failed", e);
+        }
+    }
+}
diff --git a/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/errors/SettableRestconfFuture.java b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/errors/SettableRestconfFuture.java
new file mode 100644 (file)
index 0000000..cf6927b
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.common.errors;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * A {@link RestconfFuture} which allows the result to be set via {@link #set(Object)} and
+ * {@link #setFailure(RestconfDocumentedException)}.
+ *
+ * @param <V> resulting value type
+ */
+public final class SettableRestconfFuture<V> extends RestconfFuture<V> {
+    @Override
+    public boolean set(final V value) {
+        return super.set(requireNonNull(value));
+    }
+
+    public boolean setFailure(final RestconfDocumentedException cause) {
+        return setException(requireNonNull(cause));
+    }
+}
index 524b5278dd550995b3bcbe9ad91a9a3a31fd2bba..91e0ab6a4d726118dbf6cb6c5dcf3ead7cb1ebe1 100644 (file)
@@ -56,7 +56,6 @@ import org.opendaylight.restconf.nb.rfc8040.rests.services.api.RestconfDataServi
 import org.opendaylight.restconf.nb.rfc8040.rests.services.api.RestconfStreamsSubscriptionService;
 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.MdsalRestconfStrategy;
 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.RestconfStrategy;
-import org.opendaylight.restconf.nb.rfc8040.rests.utils.DeleteDataTransactionUtil;
 import org.opendaylight.restconf.nb.rfc8040.rests.utils.PatchDataTransactionUtil;
 import org.opendaylight.restconf.nb.rfc8040.rests.utils.PlainPatchDataTransactionUtil;
 import org.opendaylight.restconf.nb.rfc8040.rests.utils.PostDataTransactionUtil;
@@ -273,11 +272,12 @@ public class RestconfDataServiceImpl implements RestconfDataService {
 
     @Override
     public Response deleteData(final String identifier) {
-        final InstanceIdentifierContext instanceIdentifier = ParserIdentifier.toInstanceIdentifier(identifier,
+        final var instanceIdentifier = ParserIdentifier.toInstanceIdentifier(identifier,
             databindProvider.currentContext().modelContext(), mountPointService);
-        final DOMMountPoint mountPoint = instanceIdentifier.getMountPoint();
-        final RestconfStrategy strategy = getRestconfStrategy(mountPoint);
-        return DeleteDataTransactionUtil.deleteData(strategy, instanceIdentifier.getInstanceIdentifier());
+        getRestconfStrategy(instanceIdentifier.getMountPoint())
+            .delete(instanceIdentifier.getInstanceIdentifier())
+            .getOrThrow();
+        return Response.noContent().build();
     }
 
     @Override
index df5764ac2cada0eafd178cd33a1a66fa11620f40..dbb1ce4f0df6c757289ade03c3012d101c2cf2f7 100644 (file)
@@ -8,15 +8,24 @@
 package org.opendaylight.restconf.nb.rfc8040.rests.transactions;
 
 import static java.util.Objects.requireNonNull;
+import static org.opendaylight.mdsal.common.api.LogicalDatastoreType.CONFIGURATION;
 
+import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
 import java.util.List;
 import java.util.Optional;
+import org.opendaylight.mdsal.common.api.CommitInfo;
 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
 import org.opendaylight.mdsal.dom.api.DOMDataBroker;
 import org.opendaylight.mdsal.dom.api.DOMDataTreeReadWriteTransaction;
 import org.opendaylight.mdsal.dom.api.DOMTransactionChain;
+import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
+import org.opendaylight.restconf.common.errors.SettableRestconfFuture;
+import org.opendaylight.yangtools.yang.common.Empty;
+import org.opendaylight.yangtools.yang.common.ErrorTag;
+import org.opendaylight.yangtools.yang.common.ErrorType;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 
@@ -38,6 +47,45 @@ public final class MdsalRestconfStrategy extends RestconfStrategy {
         return new MdsalRestconfTransaction(dataBroker);
     }
 
+    @Override
+    protected void delete(final SettableRestconfFuture<Empty> future, final YangInstanceIdentifier path) {
+        final var tx = dataBroker.newReadWriteTransaction();
+        tx.exists(CONFIGURATION, path).addCallback(new FutureCallback<>() {
+            @Override
+            public void onSuccess(final Boolean result) {
+                if (!result) {
+                    cancelTx(new RestconfDocumentedException("Data does not exist", ErrorType.PROTOCOL,
+                        ErrorTag.DATA_MISSING, path));
+                    return;
+                }
+
+                tx.delete(CONFIGURATION, path);
+                tx.commit().addCallback(new FutureCallback<CommitInfo>() {
+                    @Override
+                    public void onSuccess(final CommitInfo result) {
+                        future.set(Empty.value());
+                    }
+
+                    @Override
+                    public void onFailure(final Throwable cause) {
+                        future.setFailure(new RestconfDocumentedException("Transaction to delete " + path + " failed",
+                            cause));
+                    }
+                }, MoreExecutors.directExecutor());
+            }
+
+            @Override
+            public void onFailure(final Throwable cause) {
+                cancelTx(new RestconfDocumentedException("Failed to access " + path, cause));
+            }
+
+            private void cancelTx(final RestconfDocumentedException ex) {
+                tx.cancel();
+                future.setFailure(ex);
+            }
+        }, MoreExecutors.directExecutor());
+    }
+
     @Override
     public ListenableFuture<Optional<NormalizedNode>> read(final LogicalDatastoreType store,
             final YangInstanceIdentifier path) {
index 91510806c0fcd35e8308decc77a948f351401f74..fc5922e8420abca4aac1905f50f96dfa52b19d38 100644 (file)
@@ -16,9 +16,13 @@ import com.google.common.util.concurrent.MoreExecutors;
 import com.google.common.util.concurrent.SettableFuture;
 import java.util.List;
 import java.util.Optional;
+import org.opendaylight.mdsal.common.api.CommitInfo;
 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
 import org.opendaylight.mdsal.common.api.ReadFailedException;
 import org.opendaylight.netconf.dom.api.NetconfDataTreeService;
+import org.opendaylight.restconf.common.errors.SettableRestconfFuture;
+import org.opendaylight.restconf.nb.rfc8040.rests.utils.TransactionUtil;
+import org.opendaylight.yangtools.yang.common.Empty;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 
@@ -34,6 +38,23 @@ public final class NetconfRestconfStrategy extends RestconfStrategy {
         this.netconfService = requireNonNull(netconfService);
     }
 
+    @Override
+    protected void delete(final SettableRestconfFuture<Empty> future, final YangInstanceIdentifier path) {
+        final var tx = prepareWriteExecution();
+        tx.delete(path);
+        Futures.addCallback(tx.commit(), new FutureCallback<CommitInfo>() {
+            @Override
+            public void onSuccess(final CommitInfo result) {
+                future.set(Empty.value());
+            }
+
+            @Override
+            public void onFailure(final Throwable cause) {
+                future.setFailure(TransactionUtil.decodeException(cause, "DELETE", path));
+            }
+        }, MoreExecutors.directExecutor());
+    }
+
     @Override
     public RestconfTransaction prepareWriteExecution() {
         return new NetconfRestconfTransaction(netconfService);
index f15f01f208fb4dd7e3d6ff7ffff44c366503c29f..6bc9d8351eecd3514a6324fcf0cbfe43257a2cc2 100644 (file)
@@ -7,13 +7,19 @@
  */
 package org.opendaylight.restconf.nb.rfc8040.rests.transactions;
 
+import static java.util.Objects.requireNonNull;
+
 import com.google.common.util.concurrent.ListenableFuture;
 import java.util.List;
 import java.util.Optional;
+import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
 import org.opendaylight.mdsal.dom.api.DOMDataBroker;
 import org.opendaylight.mdsal.dom.api.DOMMountPoint;
 import org.opendaylight.netconf.dom.api.NetconfDataTreeService;
+import org.opendaylight.restconf.common.errors.RestconfFuture;
+import org.opendaylight.restconf.common.errors.SettableRestconfFuture;
+import org.opendaylight.yangtools.yang.common.Empty;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 
@@ -84,4 +90,20 @@ public abstract class RestconfStrategy {
      * @return a FluentFuture containing the result of the check
      */
     public abstract ListenableFuture<Boolean> exists(LogicalDatastoreType store, YangInstanceIdentifier path);
+
+    /**
+     * Delete data from the configuration datastore. If the data does not exist, this operation will fail, as outlined
+     * in <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.7">RFC8040 section 4.7</a>
+     *
+     * @param path Path to delete
+     * @return A {@link RestconfFuture}
+     * @throws NullPointerException if {@code path} is {@code null}
+     */
+    public final @NonNull RestconfFuture<Empty> delete(final YangInstanceIdentifier path) {
+        final var ret = new SettableRestconfFuture<Empty>();
+        delete(ret, requireNonNull(path));
+        return ret;
+    }
+
+    protected abstract void delete(@NonNull SettableRestconfFuture<Empty> future, @NonNull YangInstanceIdentifier path);
 }
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/utils/DeleteDataTransactionUtil.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/utils/DeleteDataTransactionUtil.java
deleted file mode 100644 (file)
index ea9c997..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License v1.0 which accompanies this distribution,
- * and is available at http://www.eclipse.org/legal/epl-v10.html
- */
-package org.opendaylight.restconf.nb.rfc8040.rests.utils;
-
-import javax.ws.rs.core.Response;
-import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
-import org.opendaylight.restconf.nb.rfc8040.rests.transactions.RestconfStrategy;
-import org.opendaylight.restconf.nb.rfc8040.rests.transactions.RestconfTransaction;
-import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
-
-/**
- * Util class for delete specific data in config DS.
- */
-public final class DeleteDataTransactionUtil {
-    private DeleteDataTransactionUtil() {
-        // Hidden on purpose
-    }
-
-    /**
-     * Delete data from DS via transaction.
-     *
-     * @param strategy object that perform the actual DS operations
-     * @return {@link Response}
-     */
-    public static Response deleteData(final RestconfStrategy strategy, final YangInstanceIdentifier path) {
-        final RestconfTransaction transaction = strategy.prepareWriteExecution();
-        try {
-            transaction.delete(path);
-        } catch (RestconfDocumentedException e) {
-            // close transaction if any and pass exception further
-            transaction.cancel();
-            throw e;
-        }
-
-        TransactionUtil.syncCommit(transaction.commit(), "DELETE", path);
-        return Response.noContent().build();
-    }
-}
index 2d25fbaa015c835121a52d4e7250cd18e14fce56..a3c2c4b2f9afad1bd0b27676c5169bdcd735cdda 100644 (file)
@@ -117,13 +117,23 @@ public final class TransactionUtil {
         LOG.trace("Transaction({}) SUCCESSFUL", txType);
     }
 
+    public static @NonNull RestconfDocumentedException decodeException(final Throwable throwable,
+            final String txType, final YangInstanceIdentifier path) {
+        return decodeException(throwable, throwable, txType, path);
+    }
+
     private static @NonNull RestconfDocumentedException decodeException(final ExecutionException ex,
             final String txType, final YangInstanceIdentifier path) {
-        if (ex.getCause() instanceof TransactionCommitFailedException commitFailed) {
+        return decodeException(ex, ex.getCause(), txType, path);
+    }
+
+    private static @NonNull RestconfDocumentedException decodeException(final Throwable ex, final Throwable cause,
+            final String txType, final YangInstanceIdentifier path) {
+        if (cause instanceof TransactionCommitFailedException) {
             // If device send some error message we want this message to get to client and not just to throw it away
             // or override it with new generic message. We search for NetconfDocumentedException that was send from
             // netconfSB and we create RestconfDocumentedException accordingly.
-            for (var error : Throwables.getCausalChain(commitFailed)) {
+            for (var error : Throwables.getCausalChain(cause)) {
                 if (error instanceof DocumentedException documentedError) {
                     final ErrorTag errorTag = documentedError.getErrorTag();
                     if (errorTag.equals(ErrorTag.DATA_EXISTS)) {
index 0b49cd03498710c9e2871d7c10b496257f0903b5..d6a8088c56b577f9e6639b26b607c9e9283eb830 100644 (file)
@@ -8,9 +8,11 @@
 package org.opendaylight.restconf.nb.rfc8040.rests.transactions;
 
 import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.instanceOf;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
@@ -22,10 +24,11 @@ import static org.opendaylight.restconf.common.patch.PatchEditOperation.REMOVE;
 import static org.opendaylight.restconf.common.patch.PatchEditOperation.REPLACE;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.Futures;
 import java.net.URLDecoder;
 import java.nio.charset.StandardCharsets;
 import java.util.List;
-import javax.ws.rs.core.Response.Status;
+import java.util.concurrent.ExecutionException;
 import javax.ws.rs.core.UriBuilder;
 import javax.ws.rs.core.UriInfo;
 import org.eclipse.jdt.annotation.NonNull;
@@ -40,7 +43,6 @@ import org.opendaylight.restconf.common.patch.PatchEntity;
 import org.opendaylight.restconf.common.patch.PatchStatusContext;
 import org.opendaylight.restconf.nb.rfc8040.AbstractJukeboxTest;
 import org.opendaylight.restconf.nb.rfc8040.WriteDataParams;
-import org.opendaylight.restconf.nb.rfc8040.rests.utils.DeleteDataTransactionUtil;
 import org.opendaylight.restconf.nb.rfc8040.rests.utils.PatchDataTransactionUtil;
 import org.opendaylight.restconf.nb.rfc8040.rests.utils.PlainPatchDataTransactionUtil;
 import org.opendaylight.restconf.nb.rfc8040.rests.utils.PostDataTransactionUtil;
@@ -233,11 +235,9 @@ abstract class AbstractRestconfStrategyTest extends AbstractJukeboxTest {
      * Test of successful DELETE operation.
      */
     @Test
-    public final void testDeleteData() {
-        final var response = DeleteDataTransactionUtil.deleteData(testDeleteDataStrategy(),
-            YangInstanceIdentifier.of());
-        // assert success
-        assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
+    public final void testDeleteData() throws Exception {
+        final var future = testDeleteDataStrategy().delete(YangInstanceIdentifier.of());
+        assertNotNull(Futures.getDone(future));
     }
 
     abstract @NonNull RestconfStrategy testDeleteDataStrategy();
@@ -247,10 +247,10 @@ abstract class AbstractRestconfStrategyTest extends AbstractJukeboxTest {
      */
     @Test
     public final void testNegativeDeleteData() {
-        final var strategy = testNegativeDeleteDataStrategy();
-        final var ex = assertThrows(RestconfDocumentedException.class,
-            () -> DeleteDataTransactionUtil.deleteData(strategy, YangInstanceIdentifier.of()));
-        final var errors = ex.getErrors();
+        final var future = testNegativeDeleteDataStrategy().delete(YangInstanceIdentifier.of());
+        final var ex = assertThrows(ExecutionException.class, () -> Futures.getDone(future)).getCause();
+        assertThat(ex, instanceOf(RestconfDocumentedException.class));
+        final var errors = ((RestconfDocumentedException) ex).getErrors();
         assertEquals(1, errors.size());
         final var error = errors.get(0);
         assertEquals(ErrorType.PROTOCOL, error.getErrorType());