You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kylin.apache.org by li...@apache.org on 2015/09/19 01:46:46 UTC

[10/50] [abbrv] incubator-kylin git commit: KYLIN-958 disallow changing data model in the backend

KYLIN-958 disallow changing data model in the backend

Signed-off-by: shaofengshi <sh...@apache.org>


Project: http://git-wip-us.apache.org/repos/asf/incubator-kylin/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-kylin/commit/4a4c719d
Tree: http://git-wip-us.apache.org/repos/asf/incubator-kylin/tree/4a4c719d
Diff: http://git-wip-us.apache.org/repos/asf/incubator-kylin/diff/4a4c719d

Branch: refs/heads/master
Commit: 4a4c719d01fb2c5c38538c4f636ffc3282b4c78f
Parents: 0bc870d
Author: gaodayue <ga...@meituan.com>
Authored: Thu Aug 27 14:00:25 2015 +0800
Committer: Luke Han <lu...@apache.org>
Committed: Sun Sep 6 14:38:00 2015 +0800

----------------------------------------------------------------------
 .../common/persistence/HBaseResourceStore.java  |   2 +-
 .../org/apache/kylin/cube/CubeDescManager.java  |  14 +-
 .../kylin/metadata/model/DataModelDesc.java     |  41 +++++
 .../kylin/rest/controller/CubeController.java   |  76 +++++----
 .../apache/kylin/rest/service/CubeService.java  |   9 +-
 .../rest/controller/CubeControllerTest.java     | 171 ++++++++++++++-----
 6 files changed, 228 insertions(+), 85 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-kylin/blob/4a4c719d/common/src/main/java/org/apache/kylin/common/persistence/HBaseResourceStore.java
----------------------------------------------------------------------
diff --git a/common/src/main/java/org/apache/kylin/common/persistence/HBaseResourceStore.java b/common/src/main/java/org/apache/kylin/common/persistence/HBaseResourceStore.java
index 37b8f8d..1c4a7ba 100644
--- a/common/src/main/java/org/apache/kylin/common/persistence/HBaseResourceStore.java
+++ b/common/src/main/java/org/apache/kylin/common/persistence/HBaseResourceStore.java
@@ -237,7 +237,7 @@ public class HBaseResourceStore extends ResourceStore {
             boolean ok = table.checkAndPut(row, B_FAMILY, B_COLUMN_TS, bOldTS, put);
             if (!ok) {
                 long real = getResourceTimestamp(resPath);
-                throw new IllegalStateException("Overwriting conflict " + resPath + ", expect old TS " + oldTS + ", but it is " + real);
+                throw new IllegalStateException("Overwriting conflict " + resPath + ", expect old TS " + real + ", but it is " + oldTS);
             }
 
             table.flushCommits();

http://git-wip-us.apache.org/repos/asf/incubator-kylin/blob/4a4c719d/cube/src/main/java/org/apache/kylin/cube/CubeDescManager.java
----------------------------------------------------------------------
diff --git a/cube/src/main/java/org/apache/kylin/cube/CubeDescManager.java b/cube/src/main/java/org/apache/kylin/cube/CubeDescManager.java
index cd270cf..dfb1b88 100644
--- a/cube/src/main/java/org/apache/kylin/cube/CubeDescManager.java
+++ b/cube/src/main/java/org/apache/kylin/cube/CubeDescManager.java
@@ -22,7 +22,7 @@ import java.io.IOException;
 import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
 
-import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang.StringUtils;
 import org.apache.kylin.common.KylinConfig;
 import org.apache.kylin.common.persistence.JsonSerializer;
 import org.apache.kylin.common.persistence.ResourceStore;
@@ -253,6 +253,14 @@ public class CubeDescManager {
 
         desc.setSignature(desc.calculateSignature());
 
+        // drop cube segments if signature changes
+        CubeInstance cube = getCubeManager().getCube(desc.getName());
+        if (cube != null && !StringUtils.equals(desc.getSignature(), cube.getDescriptor().getSignature())) {
+            logger.info("Detect signature change of [" + desc.getName() + "], drop all existing segments");
+            cube.getSegments().clear();
+            getCubeManager().updateCube(cube);
+        }
+
         // Save Source
         String path = desc.getResourcePath();
         getStore().putResource(path, desc, CUBE_DESC_SERIALIZER);
@@ -269,6 +277,10 @@ public class CubeDescManager {
         return MetadataManager.getInstance(config);
     }
 
+    private CubeManager getCubeManager() {
+        return CubeManager.getInstance(config);
+    }
+
     private ResourceStore getStore() {
         return ResourceStore.getStore(this.config);
     }

http://git-wip-us.apache.org/repos/asf/incubator-kylin/blob/4a4c719d/metadata/src/main/java/org/apache/kylin/metadata/model/DataModelDesc.java
----------------------------------------------------------------------
diff --git a/metadata/src/main/java/org/apache/kylin/metadata/model/DataModelDesc.java b/metadata/src/main/java/org/apache/kylin/metadata/model/DataModelDesc.java
index a37d4c6..cb1e784 100644
--- a/metadata/src/main/java/org/apache/kylin/metadata/model/DataModelDesc.java
+++ b/metadata/src/main/java/org/apache/kylin/metadata/model/DataModelDesc.java
@@ -18,14 +18,17 @@
 
 package org.apache.kylin.metadata.model;
 
+import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.Map;
 
 import org.apache.commons.lang.ArrayUtils;
+import org.apache.commons.lang.StringUtils;
 import org.apache.kylin.common.persistence.ResourceStore;
 import org.apache.kylin.common.persistence.RootPersistentEntity;
+import org.apache.kylin.common.util.JsonUtil;
 import org.apache.kylin.common.util.StringUtil;
 import org.apache.kylin.metadata.MetadataConstants;
 
@@ -33,9 +36,12 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect;
 import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 @JsonAutoDetect(fieldVisibility = Visibility.NONE, getterVisibility = Visibility.NONE, isGetterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE)
 public class DataModelDesc extends RootPersistentEntity {
+    private static final Logger logger = LoggerFactory.getLogger(DataModelDesc.class);
 
     public static enum RealizationCapacity {
         SMALL, MEDIUM, LARGE
@@ -213,6 +219,41 @@ public class DataModelDesc extends RootPersistentEntity {
         }
     }
 
+    /**
+     * Check whether two data model are compatible or not. Compatible means
+     * having the same structure. Tow models could be compatible even they
+     * have different UUID or last modified time.
+     * @param that model to compare with
+     * @return true if compatible, false otherwise.
+     */
+    public boolean compatibleWith(DataModelDesc that) {
+        if (this == that)
+            return true;
+
+        if (that == null)
+            return false;
+
+        try {
+            String thisRepr = excludeHeaderInfo(this);
+            String thatRepr = excludeHeaderInfo(that);
+            return StringUtils.equals(thisRepr, thatRepr);
+
+        } catch (IOException e) {
+            logger.error("Failed to serialize DataModelDesc to string", e);
+            return false;
+        }
+    }
+
+    private String excludeHeaderInfo(DataModelDesc modelDesc) throws IOException {
+        // make a copy
+        String repr = JsonUtil.writeValueAsString(modelDesc);
+        DataModelDesc copy = JsonUtil.readValue(repr, DataModelDesc.class);
+
+        copy.setUuid(null);
+        copy.setLastModified(0);
+        return JsonUtil.writeValueAsString(copy);
+    }
+
     @Override
     public String toString() {
         return "DataModelDesc [name=" + name + "]";

http://git-wip-us.apache.org/repos/asf/incubator-kylin/blob/4a4c719d/server/src/main/java/org/apache/kylin/rest/controller/CubeController.java
----------------------------------------------------------------------
diff --git a/server/src/main/java/org/apache/kylin/rest/controller/CubeController.java b/server/src/main/java/org/apache/kylin/rest/controller/CubeController.java
index 9f29753..734ef32 100644
--- a/server/src/main/java/org/apache/kylin/rest/controller/CubeController.java
+++ b/server/src/main/java/org/apache/kylin/rest/controller/CubeController.java
@@ -330,56 +330,57 @@ public class CubeController extends BasicController {
     }
 
     /**
-     * Get available table list of the input database
+     * Update cube description. If cube signature has changed, all existing cube segments are dropped.
      *
      * @return Table metadata array
      * @throws JsonProcessingException
-     * @throws IOException
      */
     @RequestMapping(value = "", method = { RequestMethod.PUT })
     @ResponseBody
     public CubeRequest updateCubeDesc(@RequestBody CubeRequest cubeRequest) throws JsonProcessingException {
-
-        //Update Model 
-        MetadataManager metaManager = MetadataManager.getInstance(KylinConfig.getInstanceFromEnv());
-        DataModelDesc modelDesc = deserializeDataModelDesc(cubeRequest);
-        if (modelDesc == null) {
+        CubeDesc desc = deserializeCubeDesc(cubeRequest);
+        if (desc == null) {
             return cubeRequest;
         }
-        try {
 
-            DataModelDesc existingModel = metaManager.getDataModelDesc(modelDesc.getName());
-            if (existingModel == null) {
-                metaManager.createDataModelDesc(modelDesc);
-            } else {
+        final String cubeName = desc.getName();
+        if (StringUtils.isEmpty(cubeName)) {
+            return errorRequest(cubeRequest, "Missing cubeName");
+        }
 
-                //ignore overwriting conflict checking before splict MODEL & CUBE
-                modelDesc.setLastModified(existingModel.getLastModified());
-                metaManager.updateDataModelDesc(modelDesc);
+        MetadataManager metadataManager = MetadataManager.getInstance(KylinConfig.getInstanceFromEnv());
+        // KYLIN-958: disallow data model change
+        if (StringUtils.isNotEmpty(cubeRequest.getModelDescData())) {
+            DataModelDesc modelDesc = deserializeDataModelDesc(cubeRequest);
+            if (modelDesc == null) {
+                return cubeRequest;
             }
 
-        } catch (IOException e) {
-            // TODO Auto-generated catch block
-            logger.error("Failed to deal with the request:" + e.getLocalizedMessage(), e);
-            throw new InternalErrorException("Failed to deal with the request: " + e.getLocalizedMessage());
-        }
+            final String modeName = modelDesc.getName();
 
-        //update cube
-        CubeDesc desc = deserializeCubeDesc(cubeRequest);
+            if (!StringUtils.equals(desc.getModelName(), modeName)) {
+                return errorRequest(cubeRequest, "CubeDesc.model_name " + desc.getModelName() + " not consistent with model " + modeName);
+            }
+
+            DataModelDesc oldModelDesc = metadataManager.getDataModelDesc(modeName);
+            if (oldModelDesc == null) {
+                return errorRequest(cubeRequest, "Data model " + modeName + " not found");
+            }
+
+            if (!modelDesc.compatibleWith(oldModelDesc)) {
+                return errorRequest(cubeRequest, "Update data model is not allowed! Please create a new cube if needed");
+            }
 
-        if (desc == null) {
-            return cubeRequest;
         }
 
         // Check if the cube is editable
         if (!cubeService.isCubeDescEditable(desc)) {
             String error = "Cube desc " + desc.getName().toUpperCase() + " is not editable.";
-            updateRequest(cubeRequest, false, error);
-            return cubeRequest;
+            return errorRequest(cubeRequest, error);
         }
 
         try {
-            CubeInstance cube = cubeService.getCubeManager().getCube(cubeRequest.getCubeName());
+            CubeInstance cube = cubeService.getCubeManager().getCube(cubeName);
             cube.setRetentionRange(desc.getRetentionRange());
             String projectName = (null == cubeRequest.getProject()) ? ProjectInstance.DEFAULT_PROJECT_NAME : cubeRequest.getProject();
             desc = cubeService.updateCubeAndDesc(cube, desc, projectName);
@@ -395,7 +396,7 @@ public class CubeController extends BasicController {
             cubeRequest.setSuccessful(true);
         } else {
             logger.warn("Cube " + desc.getName() + " fail to create because " + desc.getError());
-            updateRequest(cubeRequest, false, omitMessage(desc.getError()));
+            errorRequest(cubeRequest, omitMessage(desc.getError()));
         }
         String descData = JsonUtil.writeValueAsIndentString(desc);
         cubeRequest.setCubeDescData(descData);
@@ -449,15 +450,15 @@ public class CubeController extends BasicController {
     private CubeDesc deserializeCubeDesc(CubeRequest cubeRequest) {
         CubeDesc desc = null;
         try {
-            logger.debug("Saving cube " + cubeRequest.getCubeDescData());
+            logger.debug("Deserialize cube desc " + cubeRequest.getCubeDescData());
             desc = JsonUtil.readValue(cubeRequest.getCubeDescData(), CubeDesc.class);
             //            desc.setRetentionRange(cubeRequest.getRetentionRange());
         } catch (JsonParseException e) {
             logger.error("The cube definition is not valid.", e);
-            updateRequest(cubeRequest, false, e.getMessage());
+            errorRequest(cubeRequest, e.getMessage());
         } catch (JsonMappingException e) {
             logger.error("The cube definition is not valid.", e);
-            updateRequest(cubeRequest, false, e.getMessage());
+            errorRequest(cubeRequest, e.getMessage());
         } catch (IOException e) {
             logger.error("Failed to deal with the request.", e);
             throw new InternalErrorException("Failed to deal with the request:" + e.getMessage(), e);
@@ -468,14 +469,14 @@ public class CubeController extends BasicController {
     private DataModelDesc deserializeDataModelDesc(CubeRequest cubeRequest) {
         DataModelDesc desc = null;
         try {
-            logger.debug("Saving MODEL " + cubeRequest.getModelDescData());
+            logger.debug("Deserialize data model " + cubeRequest.getModelDescData());
             desc = JsonUtil.readValue(cubeRequest.getModelDescData(), DataModelDesc.class);
         } catch (JsonParseException e) {
             logger.error("The data model definition is not valid.", e);
-            updateRequest(cubeRequest, false, e.getMessage());
+            errorRequest(cubeRequest, e.getMessage());
         } catch (JsonMappingException e) {
             logger.error("The data model definition is not valid.", e);
-            updateRequest(cubeRequest, false, e.getMessage());
+            errorRequest(cubeRequest, e.getMessage());
         } catch (IOException e) {
             logger.error("Failed to deal with the request.", e);
             throw new InternalErrorException("Failed to deal with the request:" + e.getMessage(), e);
@@ -496,10 +497,11 @@ public class CubeController extends BasicController {
         return buffer.toString();
     }
 
-    private void updateRequest(CubeRequest request, boolean success, String message) {
+    private CubeRequest errorRequest(CubeRequest request, String errmsg) {
         request.setCubeDescData("");
-        request.setSuccessful(success);
-        request.setMessage(message);
+        request.setSuccessful(false);
+        request.setMessage(errmsg);
+        return request;
     }
 
     public void setCubeService(CubeService cubeService) {

http://git-wip-us.apache.org/repos/asf/incubator-kylin/blob/4a4c719d/server/src/main/java/org/apache/kylin/rest/service/CubeService.java
----------------------------------------------------------------------
diff --git a/server/src/main/java/org/apache/kylin/rest/service/CubeService.java b/server/src/main/java/org/apache/kylin/rest/service/CubeService.java
index 21c6ec7..be356af 100644
--- a/server/src/main/java/org/apache/kylin/rest/service/CubeService.java
+++ b/server/src/main/java/org/apache/kylin/rest/service/CubeService.java
@@ -236,11 +236,11 @@ public class CubeService extends BasicService {
         }
 
         try {
-            if (!cube.getDescriptor().calculateSignature().equals(cube.getDescriptor().getSignature())) {
-                this.releaseAllSegments(cube);
+            CubeDesc updatedCubeDesc = getCubeDescManager().updateCubeDesc(desc);
+            if (!updatedCubeDesc.getError().isEmpty()) {
+                return updatedCubeDesc;
             }
 
-            CubeDesc updatedCubeDesc = getCubeDescManager().updateCubeDesc(desc);
             cube = getCubeManager().updateCube(cube);
 
             int cuboidCount = CuboidCLI.simulateCuboidGeneration(updatedCubeDesc);
@@ -388,9 +388,6 @@ public class CubeService extends BasicService {
         if (!cubingJobs.isEmpty()) {
             throw new JobException("Enable is not allowed with a running job.");
         }
-        if (!cube.getDescriptor().calculateSignature().equals(cube.getDescriptor().getSignature())) {
-            this.releaseAllSegments(cube);
-        }
 
         cube.setStatus(RealizationStatusEnum.READY);
         try {

http://git-wip-us.apache.org/repos/asf/incubator-kylin/blob/4a4c719d/server/src/test/java/org/apache/kylin/rest/controller/CubeControllerTest.java
----------------------------------------------------------------------
diff --git a/server/src/test/java/org/apache/kylin/rest/controller/CubeControllerTest.java b/server/src/test/java/org/apache/kylin/rest/controller/CubeControllerTest.java
index cf1a718..2f12851 100644
--- a/server/src/test/java/org/apache/kylin/rest/controller/CubeControllerTest.java
+++ b/server/src/test/java/org/apache/kylin/rest/controller/CubeControllerTest.java
@@ -19,32 +19,34 @@
 package org.apache.kylin.rest.controller;
 
 import java.io.IOException;
-import java.io.StringWriter;
 import java.util.List;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
+import org.apache.kylin.common.util.JsonUtil;
 import org.apache.kylin.cube.CubeInstance;
 import org.apache.kylin.cube.model.CubeDesc;
+import org.apache.kylin.cube.model.RowKeyColDesc;
 import org.apache.kylin.metadata.model.DataModelDesc;
 import org.apache.kylin.rest.request.CubeRequest;
 import org.apache.kylin.rest.service.CubeService;
 import org.apache.kylin.rest.service.JobService;
 import org.apache.kylin.rest.service.ServiceTestBase;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
+import org.junit.*;
 import org.springframework.beans.factory.annotation.Autowired;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.collect.Lists;
 
 /**
  * @author xduo
  */
 public class CubeControllerTest extends ServiceTestBase {
+    private static final String SRC_CUBE_NAME = "test_kylin_cube_with_slr_ready";
+    private static final String TEST_CUBE_NAME = SRC_CUBE_NAME + "_test_save";
 
     private CubeController cubeController;
     private CubeDescController cubeDescController;
     private ModelController modelController;
+    private CubeDesc srcCubeDesc;
 
     @Autowired
     CubeService cubeService;
@@ -52,29 +54,39 @@ public class CubeControllerTest extends ServiceTestBase {
     JobService jobService;
 
     @Before
-    public void setup() throws Exception {
+    public void setUp() throws Exception {
         super.setUp();
-
         cubeController = new CubeController();
         cubeController.setCubeService(cubeService);
         cubeController.setJobService(jobService);
+
         cubeDescController = new CubeDescController();
         cubeDescController.setCubeService(cubeService);
 
         modelController = new ModelController();
         modelController.setCubeService(cubeService);
+
+        srcCubeDesc = getCubeDescByName(SRC_CUBE_NAME);
+
+        saveTestCube(TEST_CUBE_NAME);
     }
 
-    @Test
-    public void testBasics() throws IOException {
-        CubeDesc[] cubes = (CubeDesc[]) cubeDescController.getCube("test_kylin_cube_with_slr_ready");
-        Assert.assertNotNull(cubes);
-        Assert.assertNotNull(cubeController.getSql("test_kylin_cube_with_slr_ready", "20130331080000_20131212080000"));
-        Assert.assertNotNull(cubeController.getCubes(null, null, 0, 5));
+    @After
+    public void tearDown() throws Exception {
+        cubeController.deleteCube(TEST_CUBE_NAME);
+        super.after();
+    }
+
+    private CubeDesc getCubeDescByName(String cubeDescName) {
+        CubeDesc[] cubes = cubeDescController.getCube(cubeDescName);
+        if (cubes == null || cubes.length < 1) {
+            throw new IllegalStateException("cube desc " + cubeDescName + " not existed");
+        }
+        return cubes[0];
+    }
 
-        CubeDesc cube = cubes[0];
+    private void saveTestCube(final String newCubeName) throws IOException {
         CubeDesc newCube = new CubeDesc();
-        String newCubeName = cube.getName() + "_test_save";
 
         try {
             cubeController.deleteCube(newCubeName);
@@ -83,45 +95,124 @@ public class CubeControllerTest extends ServiceTestBase {
         }
 
         newCube.setName(newCubeName);
-        newCube.setModelName(cube.getModelName());
-        newCube.setModel(cube.getModel());
-        newCube.setDimensions(cube.getDimensions());
-        newCube.setHBaseMapping(cube.getHBaseMapping());
-        newCube.setMeasures(cube.getMeasures());
-        newCube.setConfig(cube.getConfig());
-        newCube.setRowkey(cube.getRowkey());
-
-        String newModelName = newCubeName + "_model_desc";
-        newCube.getModel().setName(newModelName);//generate a random model
+        newCube.setModelName(newCubeName);
+        newCube.setModel(srcCubeDesc.getModel());
+        newCube.setDimensions(srcCubeDesc.getDimensions());
+        newCube.setHBaseMapping(srcCubeDesc.getHBaseMapping());
+        newCube.setMeasures(srcCubeDesc.getMeasures());
+        newCube.setConfig(srcCubeDesc.getConfig());
+        newCube.setRowkey(srcCubeDesc.getRowkey());
+
+        newCube.getModel().setName(newCubeName);
         newCube.getModel().setLastModified(0);
 
-        ObjectMapper cubeDescMapper = new ObjectMapper();
-        StringWriter cubeDescWriter = new StringWriter();
-        cubeDescMapper.writeValue(cubeDescWriter, newCube);
+        CubeRequest cubeRequest = new CubeRequest();
+        cubeRequest.setCubeDescData(JsonUtil.writeValueAsIndentString(newCube));
+        cubeRequest.setModelDescData(JsonUtil.writeValueAsIndentString(newCube.getModel()));
 
-        ObjectMapper modelDescMapper = new ObjectMapper();
-        StringWriter modelDescWriter = new StringWriter();
-        modelDescMapper.writeValue(modelDescWriter, newCube.getModel());
+        CubeRequest result = cubeController.saveCubeDesc(cubeRequest);
+        Assert.assertTrue(result.getSuccessful());
+    }
 
-        CubeRequest cubeRequest = new CubeRequest();
-        cubeRequest.setCubeDescData(cubeDescWriter.toString());
-        cubeRequest.setModelDescData(modelDescWriter.toString());
-        cubeRequest = cubeController.saveCubeDesc(cubeRequest);
+    @Test
+    public void testBasics() throws IOException {
+
+        Assert.assertNotNull(cubeController.getSql(SRC_CUBE_NAME, "20130331080000_20131212080000"));
+        Assert.assertNotNull(cubeController.getCubes(null, null, 0, 5));
 
-        DataModelDesc model = modelController.getModel(newModelName);
+        DataModelDesc model = modelController.getModel(TEST_CUBE_NAME);
         Assert.assertNotNull(model);
 
         List<String> notifyList = Lists.newArrayList();
         notifyList.add("john@example.com");
-        cubeController.updateNotifyList(newCubeName, notifyList);
-        cubeController.updateCubeCost(newCubeName, 80);
+        cubeController.updateNotifyList(TEST_CUBE_NAME, notifyList);
+        cubeController.updateCubeCost(TEST_CUBE_NAME, 80);
 
-        List<CubeInstance> cubeInstances = cubeController.getCubes(newCubeName, "default", 1, 0);
+        List<CubeInstance> cubeInstances = cubeController.getCubes(TEST_CUBE_NAME, "default", 1, 0);
 
         CubeInstance cubeInstance = cubeInstances.get(0);
         Assert.assertTrue(cubeInstance.getDescriptor().getNotifyList().contains("john@example.com"));
         Assert.assertTrue(cubeInstance.getCost() == 80);
-        cubeController.deleteCube(newCubeName);
     }
 
+    @Test
+    public void testUpdateCubeDesc() throws IOException {
+        CubeDesc newCubeDesc = getCubeDescByName(TEST_CUBE_NAME);
+
+        // -------------------------------------------------------
+        // negative case
+        // -------------------------------------------------------
+
+        // invalid cube desc
+        CubeRequest req = new CubeRequest();
+        req.setCubeDescData("invalid");
+        assertUpdateFail(req);
+
+        // invalid data model
+        req = new CubeRequest();
+        req.setCubeDescData(JsonUtil.writeValueAsIndentString(newCubeDesc));
+        req.setModelDescData("invalid");
+        assertUpdateFail(req);
+
+        // data model's model_name not consistent with model name
+        req = new CubeRequest();
+        req.setCubeDescData("{\"name\" : \"myCube\", \"model_name\" : \"anotherModelName\"}");
+        req.setModelDescData("{\"name\" : \"myCube\"}");
+        assertUpdateFail(req);
+
+        // non-existed data model
+        req = new CubeRequest();
+        req.setCubeDescData("{\"name\" : \"noSuchCube\", \"model_name\" : \"noSuchModel\"}");
+        req.setModelDescData("{\"name\" : \"noSuchModel\"}");
+        assertUpdateFail(req);
+
+        // modified data model
+        req = new CubeRequest();
+        req.setCubeDescData(JsonUtil.writeValueAsIndentString(newCubeDesc));
+
+        DataModelDesc modifiedModel = new DataModelDesc();
+        modifiedModel.setName(TEST_CUBE_NAME);
+        modifiedModel.setFactTable("anotherFactTable");
+        req.setModelDescData(JsonUtil.writeValueAsIndentString(modifiedModel));
+
+        assertUpdateFail(req);
+
+        // -------------------------------------------------------
+        // positive case
+        // -------------------------------------------------------
+        req = new CubeRequest();
+        req.setModelDescData(JsonUtil.writeValueAsIndentString(newCubeDesc.getModel()));
+
+        // no signature change
+        newCubeDesc.setDescription("hello cube");
+        req.setCubeDescData(JsonUtil.writeValueAsIndentString(newCubeDesc));
+        CubeRequest res = cubeController.updateCubeDesc(req);
+        Assert.assertTrue(res.getSuccessful());
+
+        CubeDesc resultDesc = getCubeDescByName(TEST_CUBE_NAME);
+        Assert.assertEquals("hello cube", resultDesc.getDescription());
+        Assert.assertEquals(newCubeDesc.getSignature(), resultDesc.getSignature());
+
+        // signature change (reverse row key column order)
+        newCubeDesc = getCubeDescByName(TEST_CUBE_NAME);
+        RowKeyColDesc[] rowkeyColumns = newCubeDesc.getRowkey().getRowKeyColumns();
+        for (int i = 0, j = rowkeyColumns.length - 1; i < j; i++, j--) {
+            RowKeyColDesc tmp = rowkeyColumns[i];
+            rowkeyColumns[i] = rowkeyColumns[j];
+            rowkeyColumns[j] = tmp;
+        }
+        req.setCubeDescData(JsonUtil.writeValueAsIndentString(newCubeDesc));
+        res = cubeController.updateCubeDesc(req);
+        Assert.assertTrue(res.getSuccessful());
+
+        resultDesc = getCubeDescByName(TEST_CUBE_NAME);
+        ;
+        Assert.assertNotEquals(newCubeDesc.getSignature(), resultDesc.getSignature());
+        Assert.assertEquals(newCubeDesc.calculateSignature(), resultDesc.getSignature());
+    }
+
+    private void assertUpdateFail(CubeRequest req) throws JsonProcessingException {
+        CubeRequest res = cubeController.updateCubeDesc(req);
+        Assert.assertFalse(res.getSuccessful());
+    }
 }