You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@helix.apache.org by ji...@apache.org on 2021/08/03 17:54:06 UTC

[helix] branch master updated: Add response metadata to response header for partitionAssignment (#1797)

This is an automated email from the ASF dual-hosted git repository.

jiajunwang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/helix.git


The following commit(s) were added to refs/heads/master by this push:
     new 84835c5  Add response metadata to response header for partitionAssignment (#1797)
84835c5 is described below

commit 84835c5f8dcccb214e0b615aa6f15f8c6f1898dc
Author: xyuanlu <xy...@gmail.com>
AuthorDate: Tue Aug 3 10:53:59 2021 -0700

    Add response metadata to response header for partitionAssignment (#1797)
    
    Add response metadata to response header for partitionAssignment.
---
 .../rest/server/resources/AbstractResource.java    | 18 +++-
 .../helix/ResourceAssignmentOptimizerAccessor.java | 14 +++-
 .../helix/rest/server/AbstractTestClass.java       |  5 +-
 .../TestResourceAssignmentOptimizerAccessor.java   | 95 ++++++++++++++++++----
 4 files changed, 110 insertions(+), 22 deletions(-)

diff --git a/helix-rest/src/main/java/org/apache/helix/rest/server/resources/AbstractResource.java b/helix-rest/src/main/java/org/apache/helix/rest/server/resources/AbstractResource.java
index f95f20e..a709d6a 100644
--- a/helix-rest/src/main/java/org/apache/helix/rest/server/resources/AbstractResource.java
+++ b/helix-rest/src/main/java/org/apache/helix/rest/server/resources/AbstractResource.java
@@ -130,6 +130,14 @@ public class AbstractResource {
     return Response.ok().build();
   }
 
+  protected Response OKWithHeader(Object entity, String headerName, Object headerValue) {
+    if (headerName == null || headerName.length() == 0) {
+      return OK(entity);
+    } else {
+      return Response.ok(entity).header(headerName, headerValue).build();
+    }
+  }
+
   protected Response created() {
     return Response.status(Response.Status.CREATED).build();
   }
@@ -151,9 +159,17 @@ public class AbstractResource {
   }
 
   protected Response JSONRepresentation(Object entity) {
+    return JSONRepresentation(entity, null, null);
+  }
+
+  /**
+   * Any metadata about the response could be conveyed through the entity headers.
+   * More details can be found at 'REST-API-Design-Rulebook' -- Ch4 Metadata Design
+   */
+  protected Response JSONRepresentation(Object entity, String headerName, Object headerValue) {
     try {
       String jsonStr = toJson(entity);
-      return OK(jsonStr);
+      return OKWithHeader(jsonStr, headerName, headerValue);
     } catch (IOException e) {
       _logger.error("Failed to convert " + entity + " to JSON response", e);
       return serverError();
diff --git a/helix-rest/src/main/java/org/apache/helix/rest/server/resources/helix/ResourceAssignmentOptimizerAccessor.java b/helix-rest/src/main/java/org/apache/helix/rest/server/resources/helix/ResourceAssignmentOptimizerAccessor.java
index ee888e0..1e35d5f 100644
--- a/helix-rest/src/main/java/org/apache/helix/rest/server/resources/helix/ResourceAssignmentOptimizerAccessor.java
+++ b/helix-rest/src/main/java/org/apache/helix/rest/server/resources/helix/ResourceAssignmentOptimizerAccessor.java
@@ -65,6 +65,10 @@ public class ResourceAssignmentOptimizerAccessor extends AbstractHelixResource {
       org.apache.helix.rest.server.resources.helix.ResourceAssignmentOptimizerAccessor.class
           .getName());
 
+  public static String RESPONSE_HEADER_KEY = "Setting";
+  public static String[] RESPONSE_HEADER_FIELDS =
+      new String[]{"instanceFilter", "resourceFilter", "returnFormat"};
+
   private static class InputFields {
     List<String> newInstances = new ArrayList<>();
     List<String> instancesToRemove = new ArrayList<>();
@@ -140,7 +144,7 @@ public class ResourceAssignmentOptimizerAccessor extends AbstractHelixResource {
       result = computeOptimalAssignmentForResources(inputFields, clusterState, clusterId);
       // 4. Serialize result to JSON and return.
       // TODO: We will need to include user input to response header since user may do async call.
-      return JSONRepresentation(result);
+      return JSONRepresentation(result, RESPONSE_HEADER_KEY, buildResponseHeaders(inputFields));
     } catch (InvalidParameterException ex) {
       return badRequest(ex.getMessage());
     } catch (JsonProcessingException e) {
@@ -370,4 +374,12 @@ public class ResourceAssignmentOptimizerAccessor extends AbstractHelixResource {
     }
     result.put(resource, partitionAssignments);
   }
+
+  private Map<String,Object> buildResponseHeaders(InputFields inputFields) {
+    Map<String, Object> headers= new HashMap<>();
+    headers.put(RESPONSE_HEADER_FIELDS[0], inputFields.instanceFilter);
+    headers.put(RESPONSE_HEADER_FIELDS[1], inputFields.resourceFilter);
+    headers.put(RESPONSE_HEADER_FIELDS[2], inputFields.returnFormat.name());
+    return headers;
+  }
 }
\ No newline at end of file
diff --git a/helix-rest/src/test/java/org/apache/helix/rest/server/AbstractTestClass.java b/helix-rest/src/test/java/org/apache/helix/rest/server/AbstractTestClass.java
index cf3a4bf..0f29a6e 100644
--- a/helix-rest/src/test/java/org/apache/helix/rest/server/AbstractTestClass.java
+++ b/helix-rest/src/test/java/org/apache/helix/rest/server/AbstractTestClass.java
@@ -527,7 +527,7 @@ public class AbstractTestClass extends JerseyTestNg.ContainerPerClassTest {
     post(uri, queryParams, entity,expectedReturnStatus, false);
   }
 
-  protected String post(String uri, Map<String, String> queryParams, Entity entity,
+  protected Response post(String uri, Map<String, String> queryParams, Entity entity,
       int expectedReturnStatus, boolean  expectBodyReturned) {
     WebTarget webTarget = target(uri);
     if (queryParams != null) {
@@ -536,9 +536,8 @@ public class AbstractTestClass extends JerseyTestNg.ContainerPerClassTest {
       }
     }
     Response response = webTarget.request().post(entity);
-    String result = response.readEntity(String.class);
     Assert.assertEquals(response.getStatus(), expectedReturnStatus);
-    return result;
+    return response;
   }
 
   protected void delete(String uri, int expectedReturnStatus) {
diff --git a/helix-rest/src/test/java/org/apache/helix/rest/server/TestResourceAssignmentOptimizerAccessor.java b/helix-rest/src/test/java/org/apache/helix/rest/server/TestResourceAssignmentOptimizerAccessor.java
index a5369a8..e5a75f5 100644
--- a/helix-rest/src/test/java/org/apache/helix/rest/server/TestResourceAssignmentOptimizerAccessor.java
+++ b/helix-rest/src/test/java/org/apache/helix/rest/server/TestResourceAssignmentOptimizerAccessor.java
@@ -27,6 +27,7 @@ import java.util.Map;
 import java.util.Set;
 import javax.ws.rs.client.Entity;
 import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
 
 import com.fasterxml.jackson.core.type.TypeReference;
@@ -34,6 +35,7 @@ import org.apache.helix.HelixDataAccessor;
 import org.apache.helix.TestHelper;
 import org.apache.helix.manager.zk.ZKHelixDataAccessor;
 import org.apache.helix.model.IdealState;
+import org.apache.helix.rest.server.resources.helix.ResourceAssignmentOptimizerAccessor;
 import org.testng.Assert;
 import org.testng.annotations.AfterClass;
 import org.testng.annotations.BeforeClass;
@@ -90,23 +92,32 @@ public class TestResourceAssignmentOptimizerAccessor extends AbstractTestClass {
     String payload = "{\"InstanceChange\" : { \"AddInstances\" : [\"" + instance1
         + "\"], \"RemoveInstances\" : [ \"" + toRemoveInstance + "\"], \"SwapInstances\" : {\""
         + swapOldInstance + "\" : \"" + swapNewInstance + "\"} }}  ";
-    String body = post(urlBase, null, Entity.entity(payload, MediaType.APPLICATION_JSON_TYPE),
+    Response response = post(urlBase, null, Entity.entity(payload, MediaType.APPLICATION_JSON_TYPE),
         Response.Status.OK.getStatusCode(), true);
     Map<String, Map<String, Map<String, String>>> resourceAssignments = OBJECT_MAPPER
-        .readValue(body, new TypeReference<HashMap<String, Map<String, Map<String, String>>>>() {
-        });
+        .readValue(response.readEntity(String.class),
+            new TypeReference<HashMap<String, Map<String, Map<String, String>>>>() {
+            });
     Set<String> hostSet = new HashSet<>();
     resourceAssignments.forEach((k, v) -> v.forEach((kk, vv) -> hostSet.addAll(vv.keySet())));
     Assert.assertTrue(hostSet.contains(instance1));
     Assert.assertTrue(hostSet.contains(swapNewInstance));
     Assert.assertFalse(hostSet.contains(liveInstances.get(0)));
     Assert.assertFalse(hostSet.contains(liveInstances.get(1)));
+    // Validate header
+    MultivaluedMap<String, Object> headers = response.getHeaders();
+    Assert.assertTrue(headers.containsKey(ResourceAssignmentOptimizerAccessor.RESPONSE_HEADER_KEY));
+    Assert.assertFalse(
+        headers.get(ResourceAssignmentOptimizerAccessor.RESPONSE_HEADER_KEY).isEmpty());
+    Assert.assertEquals(headers.get(ResourceAssignmentOptimizerAccessor.RESPONSE_HEADER_KEY).get(0),
+        "{instanceFilter=[], resourceFilter=[], returnFormat=IdealStateFormat}");
 
-    // Test partitionAssignment host filter
+    // Test partitionAssignment InstanceFilter
     String payload2 = "{\"Options\" : { \"InstanceFilter\" : [\"" + liveInstances.get(0) + "\" , \""
         + liveInstances.get(1) + "\"] }}  ";
-    String body2 = post(urlBase, null, Entity.entity(payload2, MediaType.APPLICATION_JSON_TYPE),
+    Response response2 = post(urlBase, null, Entity.entity(payload2, MediaType.APPLICATION_JSON_TYPE),
         Response.Status.OK.getStatusCode(), true);
+    String body2 = response2.readEntity(String.class);
     Map<String, Map<String, Map<String, String>>> resourceAssignments2 = OBJECT_MAPPER
         .readValue(body2, new TypeReference<HashMap<String, Map<String, Map<String, String>>>>() {
         });
@@ -115,43 +126,93 @@ public class TestResourceAssignmentOptimizerAccessor extends AbstractTestClass {
     Assert.assertEquals(hostSet2.size(), 2);
     Assert.assertTrue(hostSet2.contains(liveInstances.get(0)));
     Assert.assertTrue(hostSet2.contains(liveInstances.get(1)));
+    // Validate header
+    MultivaluedMap<String, Object> headers2 = response2.getHeaders();
+    Assert
+        .assertTrue(headers2.containsKey(ResourceAssignmentOptimizerAccessor.RESPONSE_HEADER_KEY));
+    List partitionAssignmentMetadata2 =
+        headers2.get(ResourceAssignmentOptimizerAccessor.RESPONSE_HEADER_KEY);
+    Assert.assertFalse(
+        headers2.get(ResourceAssignmentOptimizerAccessor.RESPONSE_HEADER_KEY).isEmpty());
+    Assert.assertTrue(
+        partitionAssignmentMetadata2.get(0).equals(
+            "{instanceFilter=[" + liveInstances.get(0) + ", " + liveInstances.get(1)
+            + "], resourceFilter=[], returnFormat=IdealStateFormat}") ||
+        partitionAssignmentMetadata2.get(0).equals(
+            "{instanceFilter=[" + liveInstances.get(1) + ", " + liveInstances.get(0)
+                + "], resourceFilter=[], returnFormat=IdealStateFormat}"),
+        partitionAssignmentMetadata2.get(0).toString());
 
+    // Test partitionAssignment ResourceFilter
     String payload3 =
         "{\"Options\" : { \"ResourceFilter\" : [\"" + resources.get(0) + "\" , \"" + resources
             .get(1) + "\"] }}  ";
-    String body3 = post(urlBase, null, Entity.entity(payload3, MediaType.APPLICATION_JSON_TYPE),
-        Response.Status.OK.getStatusCode(), true);
+    Response response3 =
+        post(urlBase, null, Entity.entity(payload3, MediaType.APPLICATION_JSON_TYPE),
+            Response.Status.OK.getStatusCode(), true);
+    String body3 = response3.readEntity(String.class);
     Map<String, Map<String, Map<String, String>>> resourceAssignments3 = OBJECT_MAPPER
         .readValue(body3, new TypeReference<HashMap<String, Map<String, Map<String, String>>>>() {
         });
     Assert.assertEquals(resourceAssignments3.size(), 2);
     Assert.assertTrue(resourceAssignments3.containsKey(resources.get(0)));
     Assert.assertTrue(resourceAssignments3.containsKey(resources.get(1)));
+    // Validate header
+    MultivaluedMap<String, Object> headers3 = response3.getHeaders();
+    Assert
+        .assertTrue(headers3.containsKey(ResourceAssignmentOptimizerAccessor.RESPONSE_HEADER_KEY));
+    List partitionAssignmentMetadata3 =
+        headers3.get(ResourceAssignmentOptimizerAccessor.RESPONSE_HEADER_KEY);
+    Assert.assertFalse(
+        headers3.get(ResourceAssignmentOptimizerAccessor.RESPONSE_HEADER_KEY).isEmpty());
+    Assert.assertTrue(
+        partitionAssignmentMetadata3.get(0).equals(
+            "{instanceFilter=[], resourceFilter=[" + resources.get(0) + ", " + resources.get(1)
+                + "], returnFormat=IdealStateFormat}") ||
+        partitionAssignmentMetadata3.get(0).equals(
+                "{instanceFilter=[], resourceFilter=[" + resources.get(1) + ", " + resources.get(0)
+                    + "], returnFormat=IdealStateFormat}"),
+        partitionAssignmentMetadata3.get(0).toString());
 
-    // Test Option CurrentState format
-    // Test AddInstances, RemoveInstances and SwapInstances
+    // Test Option CurrentState format with AddInstances, RemoveInstances and SwapInstances
     String payload4 = "{\"InstanceChange\" : { \"AddInstances\" : [\"" + instance1
         + "\"], \"RemoveInstances\" : [ \"" + toRemoveInstance + "\"], \"SwapInstances\" : {\""
         + swapOldInstance + "\" : \"" + swapNewInstance
         + "\"} }, \"Options\" : { \"ReturnFormat\" : \"CurrentStateFormat\" , \"ResourceFilter\" : [\""
         + resources.get(0) + "\" , \"" + resources.get(1) + "\"]} } ";
-    String body4 = post(urlBase, null, Entity.entity(payload4, MediaType.APPLICATION_JSON_TYPE),
-        Response.Status.OK.getStatusCode(), true);
+    Response response4 =
+        post(urlBase, null, Entity.entity(payload4, MediaType.APPLICATION_JSON_TYPE),
+            Response.Status.OK.getStatusCode(), true);
+    String body4 = response4.readEntity(String.class);
     Map<String, Map<String, Map<String, String>>> resourceAssignments4 = OBJECT_MAPPER
         .readValue(body4, new TypeReference<HashMap<String, Map<String, Map<String, String>>>>() {
         });
     // Validate outer map key is instance
     Set<String> resource4 = new HashSet<>();
-    resourceAssignments4.forEach((k, v)  -> v.forEach((kk, vv) -> resource4.add(kk)));
+    resourceAssignments4.forEach((k, v) -> v.forEach((kk, vv) -> resource4.add(kk)));
     Assert.assertTrue(resource4.contains(resources.get(0)));
     Assert.assertTrue(resource4.contains(resources.get(1)));
-
     // First inner map key is resource
     Assert.assertTrue(resourceAssignments4.containsKey(instance1));
     Assert.assertTrue(resourceAssignments4.containsKey(swapNewInstance));
     Assert.assertFalse(resourceAssignments4.containsKey(liveInstances.get(0)));
     Assert.assertFalse(resourceAssignments4.containsKey(liveInstances.get(1)));
-
+    // Validate header
+    MultivaluedMap<String, Object> headers4 = response4.getHeaders();
+    Assert
+        .assertTrue(headers4.containsKey(ResourceAssignmentOptimizerAccessor.RESPONSE_HEADER_KEY));
+    List partitionAssignmentMetadata4 =
+        headers4.get(ResourceAssignmentOptimizerAccessor.RESPONSE_HEADER_KEY);
+    Assert.assertFalse(
+        headers4.get(ResourceAssignmentOptimizerAccessor.RESPONSE_HEADER_KEY).isEmpty());
+    Assert.assertTrue(
+        partitionAssignmentMetadata4.get(0).equals(
+            "{instanceFilter=[], resourceFilter=[" + resources.get(0) + ", " + resources.get(1)
+                + "], returnFormat=CurrentStateFormat}") ||
+        partitionAssignmentMetadata4.get(0).equals(
+                "{instanceFilter=[], resourceFilter=[" + resources.get(1) + ", " + resources.get(0)
+                    + "], returnFormat=CurrentStateFormat}"),
+        partitionAssignmentMetadata4.get(0).toString());
 
     System.out.println("End test :" + TestHelper.getTestMethodName());
   }
@@ -174,7 +235,7 @@ public class TestResourceAssignmentOptimizerAccessor extends AbstractTestClass {
         + "\"], \"RemoveInstances\" : [ \"" + toRemoveInstance + "\"], \"SwapInstances\" : {\""
         + swapOldInstance + "\" : \"" + swapNewInstance + "\"} }}  ";
     String body = post(urlBase, null, Entity.entity(payload, MediaType.APPLICATION_JSON_TYPE),
-        Response.Status.OK.getStatusCode(), true);
+        Response.Status.OK.getStatusCode(), true).readEntity(String.class);
     Map<String, Map<String, Map<String, String>>> resourceAssignments = OBJECT_MAPPER
         .readValue(body, new TypeReference<HashMap<String, Map<String, Map<String, String>>>>() {
         });
@@ -189,7 +250,7 @@ public class TestResourceAssignmentOptimizerAccessor extends AbstractTestClass {
     String payload2 = "{\"Options\" : { \"InstanceFilter\" : [\"" + liveInstances.get(0) + "\" , \""
         + liveInstances.get(1) + "\"] }}  ";
     String body2 = post(urlBase, null, Entity.entity(payload2, MediaType.APPLICATION_JSON_TYPE),
-        Response.Status.OK.getStatusCode(), true);
+        Response.Status.OK.getStatusCode(), true).readEntity(String.class);
     Map<String, Map<String, Map<String, String>>> resourceAssignments2 = OBJECT_MAPPER
         .readValue(body2, new TypeReference<HashMap<String, Map<String, Map<String, String>>>>() {
         });
@@ -203,7 +264,7 @@ public class TestResourceAssignmentOptimizerAccessor extends AbstractTestClass {
         "{\"Options\" : { \"ResourceFilter\" : [\"" + resources.get(0) + "\" , \"" + resources
             .get(1) + "\"] }}  ";
     String body3 = post(urlBase, null, Entity.entity(payload3, MediaType.APPLICATION_JSON_TYPE),
-        Response.Status.OK.getStatusCode(), true);
+        Response.Status.OK.getStatusCode(), true).readEntity(String.class);
     Map<String, Map<String, Map<String, String>>> resourceAssignments3 = OBJECT_MAPPER
         .readValue(body3, new TypeReference<HashMap<String, Map<String, Map<String, String>>>>() {
         });