Use RestconfFuture in putData() 97/108297/1
authorRobert Varga <robert.varga@pantheon.tech>
Tue, 10 Oct 2023 07:53:19 +0000 (09:53 +0200)
committerRobert Varga <robert.varga@pantheon.tech>
Tue, 10 Oct 2023 07:56:35 +0000 (09:56 +0200)
RestconfStrategy.putData() is the final caller of
TransactionUtil.syncCommit().

Refactor it to return a RestconfFuture, adjust callers and eliminate
syncCommit().

JIRA: NETCONF-718
Change-Id: I1f34ddf767bb340d479fdd7fffa4287040559c85
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
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/RestconfStrategy.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/TransactionUtil.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfDataServiceImplTest.java

index b5e968147ebdacec6ff0f3f55381066a0b2274a9..5f3c78dcd9a99b6292a8dc3f1b564461382e32c3 100644 (file)
@@ -297,7 +297,7 @@ public final class RestconfDataServiceImpl {
      *
      * @param uriInfo request URI information
      * @param body data node for put to config DS
-     * @return {@link Response}
+     * @param ar {@link AsyncResponse} which needs to be completed
      */
     @PUT
     @Path("/data")
@@ -305,9 +305,9 @@ public final class RestconfDataServiceImpl {
         MediaTypes.APPLICATION_YANG_DATA_JSON,
         MediaType.APPLICATION_JSON,
     })
-    public Response putDataJSON(@Context final UriInfo uriInfo, final InputStream body) {
+    public void putDataJSON(@Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
         try (var jsonBody = new JsonResourceBody(body)) {
-            return putData(null, uriInfo, jsonBody);
+            putData(null, uriInfo, jsonBody, ar);
         }
     }
 
@@ -317,7 +317,7 @@ public final class RestconfDataServiceImpl {
      * @param identifier path to target
      * @param uriInfo request URI information
      * @param body data node for put to config DS
-     * @return {@link Response}
+     * @param ar {@link AsyncResponse} which needs to be completed
      */
     @PUT
     @Path("/data/{identifier:.+}")
@@ -325,10 +325,10 @@ public final class RestconfDataServiceImpl {
         MediaTypes.APPLICATION_YANG_DATA_JSON,
         MediaType.APPLICATION_JSON,
     })
-    public Response putDataJSON(@Encoded @PathParam("identifier") final String identifier,
-            @Context final UriInfo uriInfo, final InputStream body) {
+    public void putDataJSON(@Encoded @PathParam("identifier") final String identifier,
+            @Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
         try (var jsonBody = new JsonResourceBody(body)) {
-            return putData(identifier, uriInfo, jsonBody);
+            putData(identifier, uriInfo, jsonBody, ar);
         }
     }
 
@@ -337,7 +337,7 @@ public final class RestconfDataServiceImpl {
      *
      * @param uriInfo request URI information
      * @param body data node for put to config DS
-     * @return {@link Response}
+     * @param ar {@link AsyncResponse} which needs to be completed
      */
     @PUT
     @Path("/data")
@@ -346,9 +346,9 @@ public final class RestconfDataServiceImpl {
         MediaType.APPLICATION_XML,
         MediaType.TEXT_XML
     })
-    public Response putDataXML(@Context final UriInfo uriInfo, final InputStream body) {
+    public void putDataXML(@Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
         try (var xmlBody = new XmlResourceBody(body)) {
-            return putData(null, uriInfo, xmlBody);
+            putData(null, uriInfo, xmlBody, ar);
         }
     }
 
@@ -358,7 +358,7 @@ public final class RestconfDataServiceImpl {
      * @param identifier path to target
      * @param uriInfo request URI information
      * @param body data node for put to config DS
-     * @return {@link Response}
+     * @param ar {@link AsyncResponse} which needs to be completed
      */
     @PUT
     @Path("/data/{identifier:.+}")
@@ -367,24 +367,25 @@ public final class RestconfDataServiceImpl {
         MediaType.APPLICATION_XML,
         MediaType.TEXT_XML
     })
-    public Response putDataXML(@Encoded @PathParam("identifier") final String identifier,
-            @Context final UriInfo uriInfo, final InputStream body) {
+    public void putDataXML(@Encoded @PathParam("identifier") final String identifier,
+            @Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
         try (var xmlBody = new XmlResourceBody(body)) {
-            return putData(identifier, uriInfo, xmlBody);
+            putData(identifier, uriInfo, xmlBody, ar);
         }
     }
 
-    private Response putData(final @Nullable String identifier, final UriInfo uriInfo, final ResourceBody body) {
+    private void putData(final @Nullable String identifier, final UriInfo uriInfo, final ResourceBody body,
+            final AsyncResponse ar) {
         final var reqPath = server.bindRequestPath(databindProvider.currentContext(), identifier);
         final var insert = QueryParams.parseInsert(reqPath.getSchemaContext(), uriInfo);
         final var req = bindResourceRequest(reqPath, body);
 
-        return switch (
-            req.strategy().putData(req.path(), req.data(), insert)) {
-            // Note: no Location header, as it matches the request path
-            case CREATED -> Response.status(Status.CREATED).build();
-            case REPLACED -> Response.noContent().build();
-        };
+        req.strategy().putData(req.path(), req.data(), insert).addCallback(new JaxRsRestconfCallback<>(ar,
+            status -> switch (status) {
+                // Note: no Location header, as it matches the request path
+                case CREATED -> Response.status(Status.CREATED).build();
+                case REPLACED -> Response.noContent().build();
+            }));
     }
 
     /**
index 958a06843c66c6f39962f22069c0fdc319ee4ccc..18f21c5339a47841645aa0cccd04f67187d35415 100644 (file)
@@ -241,8 +241,8 @@ public abstract class RestconfStrategy {
      * @param insert  {@link Insert}
      * @return A {@link CreateOrReplaceResult}
      */
-    public final @NonNull CreateOrReplaceResult putData(final YangInstanceIdentifier path, final NormalizedNode data,
-            final @Nullable Insert insert) {
+    public final RestconfFuture<CreateOrReplaceResult> putData(final YangInstanceIdentifier path,
+            final NormalizedNode data, final @Nullable Insert insert) {
         final var exists = TransactionUtil.syncAccess(exists(path), path);
 
         final ListenableFuture<? extends CommitInfo> commitFuture;
@@ -254,8 +254,21 @@ public abstract class RestconfStrategy {
             commitFuture = replaceAndCommit(prepareWriteExecution(), path, data);
         }
 
-        TransactionUtil.syncCommit(commitFuture, "PUT", path);
-        return exists ? CreateOrReplaceResult.REPLACED : CreateOrReplaceResult.CREATED;
+        final var ret = new SettableRestconfFuture<CreateOrReplaceResult>();
+
+        Futures.addCallback(commitFuture, new FutureCallback<CommitInfo>() {
+            @Override
+            public void onSuccess(final CommitInfo result) {
+                ret.set(exists ? CreateOrReplaceResult.REPLACED : CreateOrReplaceResult.CREATED);
+            }
+
+            @Override
+            public void onFailure(final Throwable cause) {
+                ret.setFailure(TransactionUtil.decodeException(cause, "PUT", path));
+            }
+        }, MoreExecutors.directExecutor());
+
+        return ret;
     }
 
     private ListenableFuture<? extends CommitInfo> insertAndCommitPut(final YangInstanceIdentifier path,
index efa3daf57c6e0d6bf67e8ac10557591fcc43396d..09c1190e489ed44d8247213f820521198d263a17 100644 (file)
@@ -11,7 +11,6 @@ import com.google.common.base.Throwables;
 import com.google.common.util.concurrent.ListenableFuture;
 import java.util.concurrent.ExecutionException;
 import org.eclipse.jdt.annotation.NonNull;
-import org.opendaylight.mdsal.common.api.CommitInfo;
 import org.opendaylight.mdsal.common.api.TransactionCommitFailedException;
 import org.opendaylight.netconf.api.DocumentedException;
 import org.opendaylight.netconf.api.NetconfDocumentedException;
@@ -53,45 +52,13 @@ final class TransactionUtil {
         }
     }
 
-    /**
-     * Synchronize commit future, translating any failure to a {@link RestconfDocumentedException}.
-     *
-     * @param future Commit future
-     * @param txType Transaction type name
-     * @param path Modified path
-     * @throws RestconfDocumentedException if commit fails
-     */
-    static void syncCommit(final ListenableFuture<? extends CommitInfo> future, final String txType,
+    static @NonNull RestconfDocumentedException decodeException(final Throwable ex, final String txType,
             final YangInstanceIdentifier path) {
-        try {
-            future.get();
-        } catch (InterruptedException e) {
-            LOG.warn("Transaction({}) FAILED!", txType, e);
-            throw new RestconfDocumentedException("Transaction failed", e);
-        } catch (ExecutionException e) {
-            LOG.warn("Transaction({}) FAILED!", txType, e);
-            throw decodeException(e, txType, path);
-        }
-        LOG.trace("Transaction({}) SUCCESSFUL", txType);
-    }
-
-    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) {
-        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 (ex 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(cause)) {
+            for (var error : Throwables.getCausalChain(ex)) {
                 if (error instanceof DocumentedException documentedError) {
                     final ErrorTag errorTag = documentedError.getErrorTag();
                     if (errorTag.equals(ErrorTag.DATA_EXISTS)) {
index 0116050ed49ce4142f1a885ec613c758788b5a85..90cb56692710d3b263f645db5da24d64b21e4e6d 100644 (file)
@@ -295,15 +295,17 @@ public class RestconfDataServiceImplTest extends AbstractJukeboxTest {
         doReturn(immediateTrueFluentFuture()).when(read)
                 .exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
         doNothing().when(readWrite).put(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, EMPTY_JUKEBOX);
-        final var response = dataService.putDataJSON("example-jukebox:jukebox", uriInfo, stringInputStream("""
+
+        doReturn(true).when(asyncResponse).resume(responseCaptor.capture());
+        dataService.putDataJSON("example-jukebox:jukebox", uriInfo, stringInputStream("""
             {
               "example-jukebox:jukebox" : {
                  "player": {
                    "gap": "0.2"
                  }
               }
-            }"""));
-        assertNotNull(response);
+            }"""), asyncResponse);
+        final var response = responseCaptor.getValue();
         assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus());
     }
 
@@ -312,14 +314,16 @@ public class RestconfDataServiceImplTest extends AbstractJukeboxTest {
         doReturn(immediateTrueFluentFuture()).when(read)
                 .exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
         doNothing().when(readWrite).put(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, EMPTY_JUKEBOX);
-        final var response = dataService.putDataXML("example-jukebox:jukebox/yang-ext:mount/example-jukebox:jukebox",
+
+        doReturn(true).when(asyncResponse).resume(responseCaptor.capture());
+        dataService.putDataXML("example-jukebox:jukebox/yang-ext:mount/example-jukebox:jukebox",
             uriInfo, stringInputStream("""
                 <jukebox xmlns="http://example.com/ns/example-jukebox">
                   <player>
                     <gap>0.2</gap>
                   </player>
-                </jukebox>"""));
-        assertNotNull(response);
+                </jukebox>"""), asyncResponse);
+        final var response = responseCaptor.getValue();
         assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus());
     }