You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@helix.apache.org by hu...@apache.org on 2019/05/25 01:19:37 UTC

[helix] 03/44: Single stoppable API impl

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

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

commit 7d7001b54ee3a7bc2954878c34447ba8a338c0c1
Author: Yi Wang <yw...@linkedin.com>
AuthorDate: Wed Mar 20 16:46:22 2019 -0700

    Single stoppable API impl
    
    RB=1603158
    G=helix-reviewers
    A=jxue,hulee
    
    Signed-off-by: Hunter Lee <hu...@linkedin.com>
---
 .../helix/manager/zk/ZKHelixDataAccessor.java      |  8 +++
 .../java/org/apache/helix/model/ClusterConfig.java | 11 ---
 .../org/apache/helix/model/TestClusterConfig.java  | 18 -----
 .../apache/helix/rest/client/CustomRestClient.java | 43 ++++++++++++
 .../helix/rest/client/CustomRestClientFactory.java | 35 ++++++++++
 .../helix/rest/client/CustomRestClientImpl.java    | 38 ++++++++++
 ...rWrapper.java => HelixDataAccessorWrapper.java} | 10 +--
 .../rest/server/json/instance/StoppableCheck.java  | 69 +++++++++++++++++++
 .../server/resources/helix/InstanceAccessor.java   | 35 ++++++++++
 .../helix/rest/server/service/InstanceService.java | 34 +++++++++
 .../rest/server/service/InstanceServiceImpl.java   | 80 ++++++++++++++++++++++
 .../helix/rest/server/TestInstanceAccessor.java    | 14 ++++
 .../server/json/instance/TestStoppableCheck.java   | 56 +++++++++++++++
 .../rest/server/util/JerseyUriRequestBuilder.java  |  3 +-
 14 files changed, 417 insertions(+), 37 deletions(-)

diff --git a/helix-core/src/main/java/org/apache/helix/manager/zk/ZKHelixDataAccessor.java b/helix-core/src/main/java/org/apache/helix/manager/zk/ZKHelixDataAccessor.java
index c871573..51f16ac 100644
--- a/helix-core/src/main/java/org/apache/helix/manager/zk/ZKHelixDataAccessor.java
+++ b/helix-core/src/main/java/org/apache/helix/manager/zk/ZKHelixDataAccessor.java
@@ -72,6 +72,14 @@ public class ZKHelixDataAccessor implements HelixDataAccessor {
     _propertyKeyBuilder = new PropertyKey.Builder(_clusterName);
   }
 
+  /* Copy constructor */
+  public ZKHelixDataAccessor(ZKHelixDataAccessor dataAccessor) {
+    _clusterName = dataAccessor._clusterName;
+    _instanceType = dataAccessor._instanceType;
+    _baseDataAccessor = dataAccessor._baseDataAccessor;
+    _propertyKeyBuilder = new PropertyKey.Builder(_clusterName);
+  }
+
   @Override
   public boolean createStateModelDef(StateModelDefinition stateModelDef) {
     String path = PropertyPathBuilder.stateModelDef(_clusterName, stateModelDef.getId());
diff --git a/helix-core/src/main/java/org/apache/helix/model/ClusterConfig.java b/helix-core/src/main/java/org/apache/helix/model/ClusterConfig.java
index ec9b1d3..bbbaa72 100644
--- a/helix-core/src/main/java/org/apache/helix/model/ClusterConfig.java
+++ b/helix-core/src/main/java/org/apache/helix/model/ClusterConfig.java
@@ -338,17 +338,6 @@ public class ClusterConfig extends HelixProperty {
   }
 
   /**
-   * Get cluster topology by level.
-   * E.g, {zone, rack, host, instance}
-   * @return
-   */
-  public String[] getTopologyLevel() {
-    String topology = getTopology();
-    String[] parts = topology.split(TOPOLOGY_SPLITTER);
-    return Arrays.copyOfRange(parts, 1, parts.length);
-  }
-
-  /**
    * Set cluster fault zone type, this should be set combined with {@link #setTopology(String)}.
    * @param faultZoneType
    */
diff --git a/helix-core/src/test/java/org/apache/helix/model/TestClusterConfig.java b/helix-core/src/test/java/org/apache/helix/model/TestClusterConfig.java
deleted file mode 100644
index 0b4fb0a..0000000
--- a/helix-core/src/test/java/org/apache/helix/model/TestClusterConfig.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package org.apache.helix.model;
-
-import org.testng.Assert;
-import org.testng.annotations.Test;
-
-
-public class TestClusterConfig {
-
-  @Test
-  public void testGetZoneId() {
-    ClusterConfig clusterConfig = new ClusterConfig("test");
-    clusterConfig.setTopology("/zone/rack/host/instance");
-
-    String[] levels = clusterConfig.getTopologyLevel();
-
-    Assert.assertEquals(levels.length, 4);
-  }
-}
diff --git a/helix-rest/src/main/java/org/apache/helix/rest/client/CustomRestClient.java b/helix-rest/src/main/java/org/apache/helix/rest/client/CustomRestClient.java
new file mode 100644
index 0000000..f8ce0e5
--- /dev/null
+++ b/helix-rest/src/main/java/org/apache/helix/rest/client/CustomRestClient.java
@@ -0,0 +1,43 @@
+package org.apache.helix.rest.client;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.Map;
+
+
+/**
+ * Interface for interacting with client side rest endpoints
+ */
+public interface CustomRestClient {
+  /**
+   * Get stoppable check result on instance
+   *
+   * @param customPayloads generic payloads required from client side and helix only works as proxy
+   * @return a map where key is custom stoppable check name and boolean value indicates if the check succeeds
+   */
+  Map<String, Boolean> getInstanceStoppableCheck(Map<String, String> customPayloads);
+  /**
+   * Get stoppable check result on partition
+   *
+   * @param customPayloads generic payloads required from client side and helix only works as proxy
+   * @return a map where key is custom stoppable check name and boolean value indicates if the check succeeds
+  */
+  Map<String, Boolean> getPartitionStoppableCheck(Map<String, String> customPayloads);
+}
diff --git a/helix-rest/src/main/java/org/apache/helix/rest/client/CustomRestClientFactory.java b/helix-rest/src/main/java/org/apache/helix/rest/client/CustomRestClientFactory.java
new file mode 100644
index 0000000..acbfef7
--- /dev/null
+++ b/helix-rest/src/main/java/org/apache/helix/rest/client/CustomRestClientFactory.java
@@ -0,0 +1,35 @@
+package org.apache.helix.rest.client;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * The memory efficient factory to create instances for {@link CustomRestClient}
+ */
+public class CustomRestClientFactory {
+  private static final String INSTANCE_HEALTH_STATUS = "/instanceHealthStatus";
+  private static final String PARTITION_HEALTH_STATUS = "/partitionHealthStatus";
+
+  private CustomRestClientFactory() {}
+
+  public static CustomRestClient get(String jsonContent) {
+    //TODO: add implementation
+    return new CustomRestClientImpl();
+  }
+}
diff --git a/helix-rest/src/main/java/org/apache/helix/rest/client/CustomRestClientImpl.java b/helix-rest/src/main/java/org/apache/helix/rest/client/CustomRestClientImpl.java
new file mode 100644
index 0000000..133a338
--- /dev/null
+++ b/helix-rest/src/main/java/org/apache/helix/rest/client/CustomRestClientImpl.java
@@ -0,0 +1,38 @@
+package org.apache.helix.rest.client;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.HashMap;
+import java.util.Map;
+
+
+//TODO: add implementation details
+class CustomRestClientImpl implements CustomRestClient {
+
+  @Override
+  public Map<String, Boolean> getInstanceStoppableCheck(Map<String, String> customPayloads) {
+    return new HashMap<>();
+  }
+
+  @Override
+  public Map<String, Boolean> getPartitionStoppableCheck(Map<String, String> customPayloads) {
+    return new HashMap<>();
+  }
+}
diff --git a/helix-rest/src/main/java/org/apache/helix/rest/common/ZKReadAccessorWrapper.java b/helix-rest/src/main/java/org/apache/helix/rest/common/HelixDataAccessorWrapper.java
similarity index 76%
rename from helix-rest/src/main/java/org/apache/helix/rest/common/ZKReadAccessorWrapper.java
rename to helix-rest/src/main/java/org/apache/helix/rest/common/HelixDataAccessorWrapper.java
index f8a4f4f..1a26831 100644
--- a/helix-rest/src/main/java/org/apache/helix/rest/common/ZKReadAccessorWrapper.java
+++ b/helix-rest/src/main/java/org/apache/helix/rest/common/HelixDataAccessorWrapper.java
@@ -4,11 +4,8 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
-import org.apache.helix.BaseDataAccessor;
 import org.apache.helix.HelixProperty;
-import org.apache.helix.InstanceType;
 import org.apache.helix.PropertyKey;
-import org.apache.helix.ZNRecord;
 import org.apache.helix.manager.zk.ZKHelixDataAccessor;
 
 
@@ -17,13 +14,12 @@ import org.apache.helix.manager.zk.ZKHelixDataAccessor;
  * The caches is of the value from get methods and short lived for the lifecycle of one rest request
  * TODO: add more cached read method based on needs
  */
-public class ZKReadAccessorWrapper extends ZKHelixDataAccessor {
+public final class HelixDataAccessorWrapper extends ZKHelixDataAccessor {
   private final Map<PropertyKey, HelixProperty> _propertyCache = new HashMap<>();
   private final Map<PropertyKey, List<String>> _batchNameCache = new HashMap<>();
 
-  public ZKReadAccessorWrapper(String clusterName, InstanceType instanceType,
-      BaseDataAccessor<ZNRecord> baseDataAccessor) {
-    super(clusterName, instanceType, baseDataAccessor);
+  public HelixDataAccessorWrapper(ZKHelixDataAccessor dataAccessor) {
+    super(dataAccessor);
   }
 
   @Override
diff --git a/helix-rest/src/main/java/org/apache/helix/rest/server/json/instance/StoppableCheck.java b/helix-rest/src/main/java/org/apache/helix/rest/server/json/instance/StoppableCheck.java
new file mode 100644
index 0000000..8dd1d31
--- /dev/null
+++ b/helix-rest/src/main/java/org/apache/helix/rest/server/json/instance/StoppableCheck.java
@@ -0,0 +1,69 @@
+package org.apache.helix.rest.server.json.instance;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.collect.ImmutableMap;
+
+
+public class StoppableCheck {
+  private static final String HELIX_CHECK_PREFIX = "Helix:";
+  private static final String CUSTOM_CHECK_PREFIX = "Custom:";
+
+  @JsonProperty("stoppable")
+  private boolean isStoppable;
+  @JsonProperty("failedChecks")
+  private List<String> failedChecks;
+
+  public StoppableCheck(boolean isStoppable, List<String> failedChecks) {
+    this.isStoppable = isStoppable;
+    this.failedChecks = failedChecks;
+  }
+
+  public static StoppableCheck mergeStoppableChecks(Map<String, Boolean> helixChecks, Map<String, Boolean> customChecks) {
+    Map<String, Boolean> mergedResult = ImmutableMap.<String, Boolean>builder()
+        .putAll(appendPrefix(helixChecks, HELIX_CHECK_PREFIX))
+        .putAll(appendPrefix(customChecks, CUSTOM_CHECK_PREFIX))
+        .build();
+
+    List<String> failedChecks = new ArrayList<>();
+    for (Map.Entry<String, Boolean> entry : mergedResult.entrySet()) {
+      if (!entry.getValue()) {
+        failedChecks.add(entry.getKey());
+      }
+    }
+
+    return new StoppableCheck(failedChecks.isEmpty(), failedChecks);
+  }
+
+  private static Map<String, Boolean> appendPrefix(Map<String, Boolean> checks, String prefix) {
+    Map<String, Boolean> result = new HashMap<>();
+    for (Map.Entry<String, Boolean> entry : checks.entrySet()) {
+      result.put(prefix + entry.getKey(), entry.getValue());
+    }
+
+    return result;
+  }
+}
diff --git a/helix-rest/src/main/java/org/apache/helix/rest/server/resources/helix/InstanceAccessor.java b/helix-rest/src/main/java/org/apache/helix/rest/server/resources/helix/InstanceAccessor.java
index cfed0e2..db44ff9 100644
--- a/helix-rest/src/main/java/org/apache/helix/rest/server/resources/helix/InstanceAccessor.java
+++ b/helix-rest/src/main/java/org/apache/helix/rest/server/resources/helix/InstanceAccessor.java
@@ -20,10 +20,12 @@ package org.apache.helix.rest.server.resources.helix;
  */
 
 import java.io.IOException;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
 import javax.ws.rs.GET;
 import javax.ws.rs.POST;
@@ -31,6 +33,7 @@ import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 
 import org.apache.helix.ConfigAccessor;
@@ -38,6 +41,7 @@ import org.apache.helix.HelixAdmin;
 import org.apache.helix.HelixDataAccessor;
 import org.apache.helix.HelixException;
 import org.apache.helix.ZNRecord;
+import org.apache.helix.manager.zk.ZKHelixDataAccessor;
 import org.apache.helix.model.ClusterConfig;
 import org.apache.helix.model.CurrentState;
 import org.apache.helix.model.Error;
@@ -48,6 +52,12 @@ import org.apache.helix.model.LiveInstance;
 import org.apache.helix.model.Message;
 import org.apache.helix.model.ParticipantHistory;
 import org.apache.helix.model.builder.HelixConfigScopeBuilder;
+import org.apache.helix.rest.client.CustomRestClient;
+import org.apache.helix.rest.client.CustomRestClientFactory;
+import org.apache.helix.rest.common.HelixDataAccessorWrapper;
+import org.apache.helix.rest.server.json.instance.StoppableCheck;
+import org.apache.helix.rest.server.service.InstanceService;
+import org.apache.helix.rest.server.service.InstanceServiceImpl;
 import org.codehaus.jackson.JsonNode;
 import org.codehaus.jackson.node.ArrayNode;
 import org.codehaus.jackson.node.JsonNodeFactory;
@@ -56,6 +66,8 @@ import org.eclipse.jetty.util.StringUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
+
 @Path("/clusters/{clusterId}/instances")
 public class InstanceAccessor extends AbstractHelixResource {
   private final static Logger _logger = LoggerFactory.getLogger(InstanceAccessor.class);
@@ -185,6 +197,29 @@ public class InstanceAccessor extends AbstractHelixResource {
     return JSONRepresentation(instanceMap);
   }
 
+  @POST
+  @Path("{instanceName}/stoppable")
+  @Consumes(MediaType.APPLICATION_JSON)
+  public Response isInstanceStoppable(String jsonContent,
+      @PathParam("clusterId") String clusterId, @PathParam("instanceName") String instanceName) throws IOException {
+    ObjectMapper objectMapper = new ObjectMapper();
+    HelixDataAccessor dataAccessor = getDataAccssor(clusterId);
+    // TODO reduce GC by dependency injection
+    InstanceService instanceService = new InstanceServiceImpl(
+        new HelixDataAccessorWrapper((ZKHelixDataAccessor) dataAccessor), getConfigAccessor());
+
+    Map<String, Boolean> helixStoppableCheck =
+        instanceService.getInstanceStoppableCheck(clusterId, instanceName);
+    CustomRestClient customClient = CustomRestClientFactory.get(jsonContent);
+    // TODO add the json content parse logic
+    Map<String, Boolean> customStoppableCheck =
+        customClient.getInstanceStoppableCheck(Collections.<String, String> emptyMap());
+    StoppableCheck stoppableCheck =
+        StoppableCheck.mergeStoppableChecks(helixStoppableCheck, customStoppableCheck);
+
+    return OK(objectMapper.writeValueAsString(stoppableCheck));
+  }
+
   @PUT
   @Path("{instanceName}")
   public Response addInstance(@PathParam("clusterId") String clusterId,
diff --git a/helix-rest/src/main/java/org/apache/helix/rest/server/service/InstanceService.java b/helix-rest/src/main/java/org/apache/helix/rest/server/service/InstanceService.java
new file mode 100644
index 0000000..664d9c0
--- /dev/null
+++ b/helix-rest/src/main/java/org/apache/helix/rest/server/service/InstanceService.java
@@ -0,0 +1,34 @@
+package org.apache.helix.rest.server.service;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.Map;
+
+
+public interface InstanceService {
+  /**
+   * Get the current instance stoppable checks based on Helix own business logic
+   *
+   * @param clusterId
+   * @param instanceName
+   * @return a map where key is stoppable check name and boolean value represents whether the check succeeds
+   */
+  Map<String, Boolean> getInstanceStoppableCheck(String clusterId, String instanceName);
+}
diff --git a/helix-rest/src/main/java/org/apache/helix/rest/server/service/InstanceServiceImpl.java b/helix-rest/src/main/java/org/apache/helix/rest/server/service/InstanceServiceImpl.java
new file mode 100644
index 0000000..d179c27
--- /dev/null
+++ b/helix-rest/src/main/java/org/apache/helix/rest/server/service/InstanceServiceImpl.java
@@ -0,0 +1,80 @@
+package org.apache.helix.rest.server.service;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.helix.ConfigAccessor;
+import org.apache.helix.HelixDataAccessor;
+import org.apache.helix.HelixException;
+import org.apache.helix.util.InstanceValidationUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+public class InstanceServiceImpl implements InstanceService {
+  private static final Logger _logger = LoggerFactory.getLogger(InstanceServiceImpl.class);
+
+  private final HelixDataAccessor _dataAccessor;
+  private final ConfigAccessor _configAccessor;
+
+  public InstanceServiceImpl(HelixDataAccessor dataAccessor, ConfigAccessor configAccessor) {
+    _dataAccessor = dataAccessor;
+    _configAccessor = configAccessor;
+  }
+
+  @Override
+  public Map<String, Boolean> getInstanceStoppableCheck(String clusterId, String instanceName) {
+    Map<String, Boolean> healthStatus = new HashMap<>();
+    healthStatus.put(HealthStatus.HAS_VALID_CONFIG.name(), InstanceValidationUtil.hasValidConfig(_dataAccessor, clusterId, instanceName));
+    if (!healthStatus.get(HealthStatus.HAS_VALID_CONFIG.name())) {
+      _logger.error("The instance {} doesn't have valid configuration", instanceName);
+      return healthStatus;
+    }
+
+    // Any exceptions occurred below due to invalid instance config shouldn't happen
+    healthStatus.put(HealthStatus.IS_ENABLED.name(), InstanceValidationUtil.isEnabled(_dataAccessor, _configAccessor, clusterId, instanceName));
+    healthStatus.put(HealthStatus.IS_ALIVE.name(), InstanceValidationUtil.isAlive(_dataAccessor, clusterId, instanceName));
+    healthStatus.put(HealthStatus.HAS_RESOURCE_ASSIGNED.name(), InstanceValidationUtil.hasResourceAssigned(_dataAccessor, clusterId, instanceName));
+    healthStatus.put(HealthStatus.HAS_DISABLED_PARTITIONS.name(), InstanceValidationUtil.hasDisabledPartitions(_dataAccessor, clusterId, instanceName));
+    healthStatus.put(HealthStatus.HAS_ERROR_PARTITIONS.name(), InstanceValidationUtil.hasErrorPartitions(_dataAccessor, clusterId, instanceName));
+
+    try {
+      boolean isStable = InstanceValidationUtil.isInstanceStable(_dataAccessor, instanceName);
+      healthStatus.put(HealthStatus.IS_STABLE.name(), isStable);
+    } catch (HelixException e) {
+      _logger.error("Failed to check instance is stable, message: {}", e.getMessage());
+      // TODO action on the stable check exception
+    }
+
+    return healthStatus;
+  }
+
+  private enum HealthStatus {
+    IS_ALIVE,
+    IS_ENABLED,
+    HAS_RESOURCE_ASSIGNED,
+    HAS_DISABLED_PARTITIONS,
+    HAS_VALID_CONFIG,
+    HAS_ERROR_PARTITIONS,
+    IS_STABLE
+  }
+}
diff --git a/helix-rest/src/test/java/org/apache/helix/rest/server/TestInstanceAccessor.java b/helix-rest/src/test/java/org/apache/helix/rest/server/TestInstanceAccessor.java
index be47cab..54f85d6 100644
--- a/helix-rest/src/test/java/org/apache/helix/rest/server/TestInstanceAccessor.java
+++ b/helix-rest/src/test/java/org/apache/helix/rest/server/TestInstanceAccessor.java
@@ -25,6 +25,7 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 import javax.ws.rs.client.Entity;
@@ -54,6 +55,19 @@ public class TestInstanceAccessor extends AbstractTestClass {
   private final static String INSTANCE_NAME = CLUSTER_NAME + "localhost_12918";
 
   @Test
+  public void testIsInstanceStoppable() throws IOException {
+    System.out.println("Start test :" + TestHelper.getTestMethodName());
+    Map<String, String> params = ImmutableMap.of("client", "espresso");
+    Entity entity =
+        Entity.entity(OBJECT_MAPPER.writeValueAsString(params), MediaType.APPLICATION_JSON_TYPE);
+    Response response = new JerseyUriRequestBuilder("clusters/{}/instances/{}/stoppable")
+        .format(CLUSTER_NAME, INSTANCE_NAME).post(this, entity);
+    String checkResult = response.readEntity(String.class);
+    Assert.assertEquals(checkResult,
+        "{\"stoppable\":false,\"failedChecks\":[\"Helix:HAS_DISABLED_PARTITIONS\",\"Helix:HAS_RESOURCE_ASSIGNED\",\"Helix:HAS_ERROR_PARTITIONS\",\"Helix:IS_ALIVE\"]}");
+  }
+
+  @Test (dependsOnMethods = "testIsInstanceStoppable")
   public void testGetAllMessages() throws IOException {
     System.out.println("Start test :" + TestHelper.getTestMethodName());
 
diff --git a/helix-rest/src/test/java/org/apache/helix/rest/server/json/instance/TestStoppableCheck.java b/helix-rest/src/test/java/org/apache/helix/rest/server/json/instance/TestStoppableCheck.java
new file mode 100644
index 0000000..25702cf
--- /dev/null
+++ b/helix-rest/src/test/java/org/apache/helix/rest/server/json/instance/TestStoppableCheck.java
@@ -0,0 +1,56 @@
+package org.apache.helix.rest.server.json.instance;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.Map;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+
+public class TestStoppableCheck {
+
+  @Test
+  public void whenSerializingStoppableCheck() throws JsonProcessingException {
+    StoppableCheck stoppableCheck = new StoppableCheck(false, ImmutableList.of("failedCheck"));
+
+    ObjectMapper mapper = new ObjectMapper();
+    String result = mapper.writeValueAsString(stoppableCheck);
+
+    Assert.assertEquals(result, "{\"stoppable\":false,\"failedChecks\":[\"failedCheck\"]}");
+  }
+
+  @Test
+  public void testMergeStoppableChecks() throws JsonProcessingException {
+    Map<String, Boolean> helixCheck = ImmutableMap.of("check0", false, "check1", false);
+    Map<String, Boolean> customCheck = ImmutableMap.of("check1", true, "check2", true);
+
+    StoppableCheck stoppableCheck = StoppableCheck.mergeStoppableChecks(helixCheck, customCheck);
+    ObjectMapper mapper = new ObjectMapper();
+    String result = mapper.writeValueAsString(stoppableCheck);
+
+    Assert.assertEquals(result, "{\"stoppable\":false,\"failedChecks\":[\"Helix:check1\",\"Helix:check0\"]}");
+  }
+}
diff --git a/helix-rest/src/test/java/org/apache/helix/rest/server/util/JerseyUriRequestBuilder.java b/helix-rest/src/test/java/org/apache/helix/rest/server/util/JerseyUriRequestBuilder.java
index a8fde73..e0642b3 100644
--- a/helix-rest/src/test/java/org/apache/helix/rest/server/util/JerseyUriRequestBuilder.java
+++ b/helix-rest/src/test/java/org/apache/helix/rest/server/util/JerseyUriRequestBuilder.java
@@ -107,9 +107,10 @@ public class JerseyUriRequestBuilder {
    * @param container
    * @param entity
    */
-  public void post(JerseyTestNg.ContainerPerClassTest container, Entity entity) {
+  public Response post(JerseyTestNg.ContainerPerClassTest container, Entity entity) {
     final Response response = buildWebTarget(container).request().post(entity);
     Assert.assertEquals(response.getStatus(), _expectedStatusCode);
+    return response;
   }
 
   /**