You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@servicecomb.apache.org by li...@apache.org on 2021/03/31 09:14:03 UTC

[servicecomb-java-chassis] branch master updated: [SCB-2249] check invocation timeout support strategies (#2333)

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

liubao pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/servicecomb-java-chassis.git


The following commit(s) were added to refs/heads/master by this push:
     new 90cd821  [SCB-2249] check invocation timeout support strategies (#2333)
90cd821 is described below

commit 90cd8212242c311363103c76f35d5a618f2d6dfc
Author: wujimin <wu...@huawei.com>
AuthorDate: Wed Mar 31 17:13:55 2021 +0800

    [SCB-2249] check invocation timeout support strategies (#2333)
---
 .../impl/ConfigurableDatetimeAccessItem.java       |   2 +-
 .../accessLog/core/AccessLogGeneratorTest.java     |   3 +-
 .../element/impl/DatetimeConfigurableItemTest.java |   2 +-
 .../common/rest/AbstractRestInvocation.java        |   2 +-
 .../java/org/apache/servicecomb/core/Const.java    |   6 -
 .../core/invocation/InvocationStageTrace.java      |  12 +-
 .../invocation/InvocationTimeoutBootListener.java  | 105 ++++++------------
 .../core/invocation/InvocationTimeoutStrategy.java |  76 +++++++++++++
 .../invocation/timeout/PassingTimeStrategy.java    | 100 +++++++++++++++++
 .../invocation/timeout/ProcessingTimeStrategy.java | 123 +++++++++++++++++++++
 .../timeout/PassingTimeStrategyTest.java           |  78 +++++++++++++
 .../timeout/ProcessingTimeStrategyTest.java        |  89 +++++++++++++++
 .../src/main/resources/microservice.yaml           |  20 ++--
 .../src/main/resources/microservice.yaml           |   4 +-
 .../client/TestSpringMVCCommonSchemaInterface.java |   7 +-
 .../src/main/resources/microservice.yaml           |   5 +
 .../src/main/resources/microservice.yaml           |   4 +
 .../servicecomb/config/BootStrapProperties.java    |  28 +++--
 .../registry/config/AbstractPropertiesLoader.java  |   1 -
 .../test/scaffolding/time}/MockClock.java          |  22 ++--
 .../test/scaffolding/{ => time}/MockTicker.java    |  32 ++----
 .../{MockTicker.java => time/MockValues.java}      |  29 ++---
 .../loadbalance/TestServiceCombServerStats.java    |  11 +-
 .../registry/lightweight/StoreServiceTest.java     |   6 +-
 .../instance/TestInstanceCacheCheckerMock.java     |  10 +-
 .../TestInstanceCacheCheckerWithoutMock.java       |   5 +-
 26 files changed, 606 insertions(+), 176 deletions(-)

diff --git a/common/common-access-log/src/main/java/org/apache/servicecomb/common/accessLog/core/element/impl/ConfigurableDatetimeAccessItem.java b/common/common-access-log/src/main/java/org/apache/servicecomb/common/accessLog/core/element/impl/ConfigurableDatetimeAccessItem.java
index 31a38d7..c522927 100644
--- a/common/common-access-log/src/main/java/org/apache/servicecomb/common/accessLog/core/element/impl/ConfigurableDatetimeAccessItem.java
+++ b/common/common-access-log/src/main/java/org/apache/servicecomb/common/accessLog/core/element/impl/ConfigurableDatetimeAccessItem.java
@@ -94,7 +94,7 @@ public class ConfigurableDatetimeAccessItem implements AccessLogItem<RoutingCont
     long milliDuration = (finishEvent.getInvocation().getInvocationStageTrace().getStartSend() -
         finishEvent.getInvocation().getInvocationStageTrace().getStart()) / 1000_000;
     doAppendFormattedItem(
-        finishEvent.getInvocation().getInvocationStageTrace().getStartCurrentTime() + milliDuration, builder);
+        finishEvent.getInvocation().getInvocationStageTrace().getStartTimeMillis() + milliDuration, builder);
   }
 
   private void doAppendFormattedItem(long milliStartTime, StringBuilder builder) {
diff --git a/common/common-access-log/src/test/java/org/apache/servicecomb/common/accessLog/core/AccessLogGeneratorTest.java b/common/common-access-log/src/test/java/org/apache/servicecomb/common/accessLog/core/AccessLogGeneratorTest.java
index ca219df..bd0e880 100644
--- a/common/common-access-log/src/test/java/org/apache/servicecomb/common/accessLog/core/AccessLogGeneratorTest.java
+++ b/common/common-access-log/src/test/java/org/apache/servicecomb/common/accessLog/core/AccessLogGeneratorTest.java
@@ -34,7 +34,6 @@ import org.apache.servicecomb.core.definition.OperationMeta;
 import org.apache.servicecomb.core.event.InvocationFinishEvent;
 import org.apache.servicecomb.core.event.ServerAccessLogEvent;
 import org.apache.servicecomb.core.invocation.InvocationStageTrace;
-
 import org.junit.Assert;
 import org.junit.Test;
 import org.mockito.Mockito;
@@ -87,7 +86,7 @@ public class AccessLogGeneratorTest {
     when(stageTrace.getStartSend()).thenReturn(0L);
     when(stageTrace.getStart()).thenReturn(0L);
     when(stageTrace.getFinish()).thenReturn(0L);
-    when(stageTrace.getStartCurrentTime()).thenReturn(startMillisecond);
+    when(stageTrace.getStartTimeMillis()).thenReturn(startMillisecond);
     when(invocation.getOperationMeta()).thenReturn(operationMeta);
     when(invocation.getInvocationStageTrace()).thenReturn(stageTrace);
 
diff --git a/common/common-access-log/src/test/java/org/apache/servicecomb/common/accessLog/core/element/impl/DatetimeConfigurableItemTest.java b/common/common-access-log/src/test/java/org/apache/servicecomb/common/accessLog/core/element/impl/DatetimeConfigurableItemTest.java
index ef5b06d..c46e803 100644
--- a/common/common-access-log/src/test/java/org/apache/servicecomb/common/accessLog/core/element/impl/DatetimeConfigurableItemTest.java
+++ b/common/common-access-log/src/test/java/org/apache/servicecomb/common/accessLog/core/element/impl/DatetimeConfigurableItemTest.java
@@ -56,7 +56,7 @@ public class DatetimeConfigurableItemTest {
     when(invocation.getInvocationStageTrace()).thenReturn(invocationStageTrace);
     when(invocationStageTrace.getStartSend()).thenReturn(0L);
     when(invocationStageTrace.getStart()).thenReturn(0L);
-    when(invocationStageTrace.getStartCurrentTime()).thenReturn(START_MILLISECOND);
+    when(invocationStageTrace.getStartTimeMillis()).thenReturn(START_MILLISECOND);
 
     accessLogEvent = new ServerAccessLogEvent();
     accessLogEvent.setMilliStartTime(START_MILLISECOND);
diff --git a/common/common-rest/src/main/java/org/apache/servicecomb/common/rest/AbstractRestInvocation.java b/common/common-rest/src/main/java/org/apache/servicecomb/common/rest/AbstractRestInvocation.java
index 5041d6c..1b638a9 100644
--- a/common/common-rest/src/main/java/org/apache/servicecomb/common/rest/AbstractRestInvocation.java
+++ b/common/common-rest/src/main/java/org/apache/servicecomb/common/rest/AbstractRestInvocation.java
@@ -133,7 +133,7 @@ public abstract class AbstractRestInvocation {
       sendFailResponse(e);
       return;
     }
-    
+
     invocation.onStart(requestEx, start);
     invocation.getInvocationStageTrace().startSchedule();
     OperationMeta operationMeta = restOperationMeta.getOperationMeta();
diff --git a/core/src/main/java/org/apache/servicecomb/core/Const.java b/core/src/main/java/org/apache/servicecomb/core/Const.java
index e4940aa..fc90ac0 100644
--- a/core/src/main/java/org/apache/servicecomb/core/Const.java
+++ b/core/src/main/java/org/apache/servicecomb/core/Const.java
@@ -25,12 +25,6 @@ public final class Const {
 
   public static final String CSE_CONTEXT = "x-cse-context";
 
-  public static final String CONTEXT_TIME_ELAPSED = "x-scb-time";
-
-  public static final String CONTEXT_TIMED_OUT = "x-scb-timed-out";
-
-  public static final String CONTEXT_TIME_CURRENT = "x-scb-time-current";
-
   public static final String RESTFUL = "rest";
 
   public static final String HIGHWAY = "highway";
diff --git a/core/src/main/java/org/apache/servicecomb/core/invocation/InvocationStageTrace.java b/core/src/main/java/org/apache/servicecomb/core/invocation/InvocationStageTrace.java
index b5a81f1..396a478 100644
--- a/core/src/main/java/org/apache/servicecomb/core/invocation/InvocationStageTrace.java
+++ b/core/src/main/java/org/apache/servicecomb/core/invocation/InvocationStageTrace.java
@@ -88,7 +88,7 @@ public class InvocationStageTrace {
   private Invocation invocation;
 
   // current time for start invocation
-  private long startCurrentTime;
+  private long startTimeMillis;
 
   private long start;
 
@@ -139,7 +139,7 @@ public class InvocationStageTrace {
 
   public void start(long start) {
     // remember the current time to start invocation
-    this.startCurrentTime = System.currentTimeMillis();
+    this.startTimeMillis = System.currentTimeMillis();
     this.start = start;
   }
 
@@ -147,12 +147,12 @@ public class InvocationStageTrace {
     return start;
   }
 
-  public long getStartCurrentTime() {
-    return startCurrentTime;
+  public long getStartTimeMillis() {
+    return startTimeMillis;
   }
 
-  public InvocationStageTrace setStartCurrentTime(long startCurrentTime) {
-    this.startCurrentTime = startCurrentTime;
+  public InvocationStageTrace setStartTimeMillis(long startTimeMillis) {
+    this.startTimeMillis = startTimeMillis;
     return this;
   }
 
diff --git a/core/src/main/java/org/apache/servicecomb/core/invocation/InvocationTimeoutBootListener.java b/core/src/main/java/org/apache/servicecomb/core/invocation/InvocationTimeoutBootListener.java
index 86fed19..5c8ac67 100644
--- a/core/src/main/java/org/apache/servicecomb/core/invocation/InvocationTimeoutBootListener.java
+++ b/core/src/main/java/org/apache/servicecomb/core/invocation/InvocationTimeoutBootListener.java
@@ -17,12 +17,8 @@
 
 package org.apache.servicecomb.core.invocation;
 
-import static javax.ws.rs.core.Response.Status.REQUEST_TIMEOUT;
+import java.util.List;
 
-import org.apache.commons.lang3.StringUtils;
-import org.apache.servicecomb.core.BootListener;
-import org.apache.servicecomb.core.Const;
-import org.apache.servicecomb.core.Invocation;
 import org.apache.servicecomb.core.event.InvocationBusinessFinishEvent;
 import org.apache.servicecomb.core.event.InvocationBusinessMethodStartEvent;
 import org.apache.servicecomb.core.event.InvocationHandlersStartEvent;
@@ -30,122 +26,83 @@ import org.apache.servicecomb.core.event.InvocationRunInExecutorStartEvent;
 import org.apache.servicecomb.core.event.InvocationStartEvent;
 import org.apache.servicecomb.core.event.InvocationStartSendRequestEvent;
 import org.apache.servicecomb.core.event.InvocationTimeoutCheckEvent;
-import org.apache.servicecomb.core.exception.ExceptionCodes;
+import org.apache.servicecomb.core.invocation.timeout.PassingTimeStrategy;
 import org.apache.servicecomb.foundation.common.event.EnableExceptionPropagation;
-import org.apache.servicecomb.foundation.common.event.EventManager;
-import org.apache.servicecomb.swagger.invocation.exception.InvocationException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.core.env.Environment;
 import org.springframework.stereotype.Component;
 
+import com.google.common.eventbus.EventBus;
 import com.google.common.eventbus.Subscribe;
-import com.netflix.config.DynamicPropertyFactory;
 
 @SuppressWarnings({"UnstableApiUsage", "unused"})
 @Component
-public class InvocationTimeoutBootListener implements BootListener {
+public class InvocationTimeoutBootListener {
   private static final Logger LOGGER = LoggerFactory.getLogger(InvocationTimeoutBootListener.class);
 
-  public static final String ENABLE_TIMEOUT_CHECK = "servicecomb.invocation.enableTimeoutCheck";
+  public static final String PREFIX = "servicecomb.invocation.timeout.check";
 
-  public static boolean timeoutCheckEnabled() {
-    return DynamicPropertyFactory.getInstance().getBooleanProperty(ENABLE_TIMEOUT_CHECK, true).get();
-  }
+  public static final String STRATEGY = PREFIX + ".strategy";
 
-  @Override
-  public void onAfterTransport(BootEvent event) {
-    if (timeoutCheckEnabled()) {
-      EventManager.getEventBus().register(this);
-    }
-  }
+  public static final String ENABLED = PREFIX + ".enabled";
 
-  @Subscribe
-  public void onInvocationStartEvent(InvocationStartEvent event) {
-    Invocation invocation = event.getInvocation();
-
-    // not initialized
-    // 1. when first time received request
-    // 2. when first time send request not a user thread
-    // initialized
-    // 1. send request in the progress of processing request
-    if (invocation.getLocalContext(Const.CONTEXT_TIME_CURRENT) == null) {
-      invocation.addLocalContext(Const.CONTEXT_TIME_CURRENT, invocation.getInvocationStageTrace().getStart());
-    }
+  private final InvocationTimeoutStrategy strategy;
 
-    if (invocation.getLocalContext(Const.CONTEXT_TIME_ELAPSED) == null) {
-      String elapsed = invocation.getContext(Const.CONTEXT_TIME_ELAPSED);
-      if (StringUtils.isEmpty(elapsed)) {
-        invocation.addLocalContext(Const.CONTEXT_TIME_ELAPSED, 0L);
-        return;
-      }
-
-      try {
-        invocation.addLocalContext(Const.CONTEXT_TIME_ELAPSED, Long.parseLong(elapsed));
-      } catch (NumberFormatException e) {
-        LOGGER.error("Not expected number format exception, attacker?");
-        invocation.addLocalContext(Const.CONTEXT_TIME_ELAPSED, 0L);
-      }
+  public InvocationTimeoutBootListener(EventBus eventBus, List<InvocationTimeoutStrategy> strategies,
+      Environment environment) {
+    if (!environment.getProperty(ENABLED, boolean.class, false)) {
+      strategy = null;
+      return;
     }
+
+    String strategyName = environment.getProperty(STRATEGY, PassingTimeStrategy.NAME);
+    // if strategyName is wrong, then just throw exception
+    strategy = strategies.stream()
+        .filter(invocationTimeoutStrategy -> strategyName.equals(invocationTimeoutStrategy.name()))
+        .findFirst()
+        .orElseThrow(() -> new IllegalStateException("can not find InvocationTimeoutStrategy, name=" + strategyName));
+    eventBus.register(this);
   }
 
   @Subscribe
   @EnableExceptionPropagation
   public void onInvocationTimeoutCheckEvent(InvocationTimeoutCheckEvent event) {
-    ensureInvocationNotTimeout(event.getInvocation());
-  }
-
-  /**
-   * check if invocation is timeout.
-   *
-   * @throws InvocationException if timeout, throw an exception. Will not throw exception twice if this method called
-   *  after timeout.
-   */
-  private void ensureInvocationNotTimeout(Invocation invocation) {
-    if (invocation.getOperationMeta().getConfig().getNanoInvocationTimeout() > 0 && calculateElapsedTime(invocation) >
-        invocation.getOperationMeta().getConfig().getNanoInvocationTimeout()) {
-      if (invocation.getLocalContext(Const.CONTEXT_TIMED_OUT) != null) {
-        // already timed out, do not throw exception again
-        return;
-      }
-      invocation.addLocalContext(Const.CONTEXT_TIMED_OUT, true);
-      throw new InvocationException(REQUEST_TIMEOUT, ExceptionCodes.INVOCATION_TIMEOUT, "Invocation Timeout.");
-    }
+    strategy.checkTimeout(event.getInvocation());
   }
 
-  private long calculateElapsedTime(Invocation invocation) {
-    return System.nanoTime() - (long) invocation.getLocalContext(Const.CONTEXT_TIME_CURRENT)
-        + (long) invocation.getLocalContext(Const.CONTEXT_TIME_ELAPSED);
+  @Subscribe
+  public void onInvocationStartEvent(InvocationStartEvent event) {
+    strategy.start(event.getInvocation());
   }
 
   @Subscribe
   @EnableExceptionPropagation
   public void onInvocationRunInExecutorStartEvent(InvocationRunInExecutorStartEvent event) {
-    ensureInvocationNotTimeout(event.getInvocation());
+    strategy.startRunInExecutor(event.getInvocation());
   }
 
   @Subscribe
   @EnableExceptionPropagation
   public void onInvocationHandlersStartEvent(InvocationHandlersStartEvent event) {
-    ensureInvocationNotTimeout(event.getInvocation());
+    strategy.startHandlers(event.getInvocation());
   }
 
   @Subscribe
   @EnableExceptionPropagation
   public void onInvocationBusinessMethodStartEvent(InvocationBusinessMethodStartEvent event) {
-    ensureInvocationNotTimeout(event.getInvocation());
+    strategy.startBusinessMethod(event.getInvocation());
   }
 
   @Subscribe
   @EnableExceptionPropagation
   public void onInvocationBusinessFinishEvent(InvocationBusinessFinishEvent event) {
-    ensureInvocationNotTimeout(event.getInvocation());
+    strategy.finishBusinessMethod(event.getInvocation());
   }
 
   @Subscribe
   @EnableExceptionPropagation
   public void onInvocationStartSendRequestEvent(InvocationStartSendRequestEvent event) {
-    Invocation invocation = event.getInvocation();
-    ensureInvocationNotTimeout(invocation);
-    invocation.addContext(Const.CONTEXT_TIME_ELAPSED, Long.toString(calculateElapsedTime(invocation)));
+    strategy.beforeSendRequest(event.getInvocation());
   }
 }
diff --git a/core/src/main/java/org/apache/servicecomb/core/invocation/InvocationTimeoutStrategy.java b/core/src/main/java/org/apache/servicecomb/core/invocation/InvocationTimeoutStrategy.java
new file mode 100644
index 0000000..5e31c59
--- /dev/null
+++ b/core/src/main/java/org/apache/servicecomb/core/invocation/InvocationTimeoutStrategy.java
@@ -0,0 +1,76 @@
+/*
+ * 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.
+ */
+
+package org.apache.servicecomb.core.invocation;
+
+import static javax.ws.rs.core.Response.Status.REQUEST_TIMEOUT;
+
+import org.apache.servicecomb.core.Invocation;
+import org.apache.servicecomb.core.exception.ExceptionCodes;
+import org.apache.servicecomb.swagger.invocation.exception.InvocationException;
+
+public interface InvocationTimeoutStrategy {
+  // indicate whether invocation already timeout inside a process
+  // null is not timeout
+  // other value is timeout
+  String CHAIN_ALREADY_TIMED_OUT = "x-scb-chain-timed-out";
+
+  String name();
+
+  void start(Invocation invocation);
+
+  default void startRunInExecutor(Invocation invocation) {
+    checkTimeout(invocation);
+  }
+
+  default void startHandlers(Invocation invocation) {
+    checkTimeout(invocation);
+  }
+
+  default void startBusinessMethod(Invocation invocation) {
+    checkTimeout(invocation);
+  }
+
+  default void finishBusinessMethod(Invocation invocation) {
+    checkTimeout(invocation);
+  }
+
+  default void beforeSendRequest(Invocation invocation) {
+    checkTimeout(invocation);
+  }
+
+  default void checkTimeout(Invocation invocation) {
+    long nanoInvocationTimeout = invocation.getOperationMeta().getConfig().getNanoInvocationTimeout();
+    if (nanoInvocationTimeout <= 0 || alreadyTimeout(invocation)) {
+      return;
+    }
+
+    long nanoTime = calculateElapsedNanoTime(invocation);
+    if (nanoTime <= nanoInvocationTimeout) {
+      return;
+    }
+
+    invocation.addLocalContext(CHAIN_ALREADY_TIMED_OUT, true);
+    throw new InvocationException(REQUEST_TIMEOUT, ExceptionCodes.INVOCATION_TIMEOUT, "Invocation Timeout.");
+  }
+
+  default boolean alreadyTimeout(Invocation invocation) {
+    return invocation.getLocalContext(CHAIN_ALREADY_TIMED_OUT) != null;
+  }
+
+  long calculateElapsedNanoTime(Invocation invocation);
+}
diff --git a/core/src/main/java/org/apache/servicecomb/core/invocation/timeout/PassingTimeStrategy.java b/core/src/main/java/org/apache/servicecomb/core/invocation/timeout/PassingTimeStrategy.java
new file mode 100644
index 0000000..4e9ce74
--- /dev/null
+++ b/core/src/main/java/org/apache/servicecomb/core/invocation/timeout/PassingTimeStrategy.java
@@ -0,0 +1,100 @@
+/*
+ * 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.
+ */
+
+package org.apache.servicecomb.core.invocation.timeout;
+
+import java.time.Clock;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.apache.servicecomb.core.Invocation;
+import org.apache.servicecomb.core.invocation.InvocationTimeoutStrategy;
+import org.springframework.stereotype.Component;
+
+/**
+ * <pre>
+ * based on time synchronization
+ * any time to calculate timeout: now - start time of invocation chain
+ *
+ * consumer: c
+ * producer: p
+ * ---------------------------------------------------------------
+ * | process 2                                                   |
+ * |                 c-send(T5)                       c-send(T8) |
+ * |                    ↑                               ↑      |
+ * | p-start(T3) → c-start(T4) → p-operation(T6) → c-start(T7)|
+ * -----↑--------------------------------------------------------
+ *      ↑
+ * -----↑-----------------
+ * |    ↑     process 1  |
+ * |  c-send(T2)          |
+ * |    ↑                |
+ * |  c-start(T1)         |
+ * ------------------------
+ *
+ * T2 timeout: T2 - T1
+ * T3 timeout: T3 - T1
+ * T4 timeout: T4 - T1
+ * ......
+ * </pre>
+ */
+@Component
+public class PassingTimeStrategy implements InvocationTimeoutStrategy {
+  public static final String NAME = "passing-time";
+
+  // milliseconds
+  // depend on time synchronization
+  // transfer between processes
+  public static final String CHAIN_START_TIME = "x-scb-chain-start";
+
+  private Clock clock = Clock.systemDefaultZone();
+
+  public PassingTimeStrategy setClock(Clock clock) {
+    this.clock = clock;
+    return this;
+  }
+
+  @Override
+  public String name() {
+    return NAME;
+  }
+
+  @Override
+  public void start(Invocation invocation) {
+    if (invocation.getLocalContext(CHAIN_START_TIME) != null) {
+      return;
+    }
+
+    long startTimeMillis = invocation.getInvocationStageTrace().getStartTimeMillis();
+    String contextChainStartTime = invocation.getContext(CHAIN_START_TIME);
+    if (StringUtils.isEmpty(contextChainStartTime)) {
+      invocation.addContext(CHAIN_START_TIME, String.valueOf(startTimeMillis));
+      invocation.addLocalContext(CHAIN_START_TIME, startTimeMillis);
+      return;
+    }
+
+    long chainStartTime = NumberUtils.toLong(contextChainStartTime, startTimeMillis);
+    invocation.addLocalContext(CHAIN_START_TIME, chainStartTime);
+  }
+
+  @Override
+  public long calculateElapsedNanoTime(Invocation invocation) {
+    long passingTimeMillis = clock.millis() - invocation.<Long>getLocalContext(CHAIN_START_TIME);
+    return TimeUnit.MILLISECONDS.toNanos(passingTimeMillis);
+  }
+}
diff --git a/core/src/main/java/org/apache/servicecomb/core/invocation/timeout/ProcessingTimeStrategy.java b/core/src/main/java/org/apache/servicecomb/core/invocation/timeout/ProcessingTimeStrategy.java
new file mode 100644
index 0000000..8fbed64
--- /dev/null
+++ b/core/src/main/java/org/apache/servicecomb/core/invocation/timeout/ProcessingTimeStrategy.java
@@ -0,0 +1,123 @@
+/*
+ * 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.
+ */
+
+package org.apache.servicecomb.core.invocation.timeout;
+
+import org.apache.commons.lang3.math.NumberUtils;
+import org.apache.servicecomb.core.Invocation;
+import org.apache.servicecomb.core.invocation.InvocationTimeoutStrategy;
+import org.springframework.stereotype.Component;
+
+import com.google.common.base.Ticker;
+
+/**
+ * <pre>
+ * Cumulative Processing Time
+ * not depend on time synchronization
+ * but lost network and framework outside of servicecomb processing time
+ *
+ * consumer: c
+ * producer: p
+ * ---------------------------------------------------------------
+ * | process 2                                                   |
+ * |                 c-send(T5)                       c-send(T8) |
+ * |                    ↑                               ↑      |
+ * | p-start(T3) → c-start(T4) → p-operation(T6) → c-start(T7)|
+ * -----↑--------------------------------------------------------
+ *      ↑
+ * -----↑-----------------
+ * |    ↑     process 1  |
+ * |  c-send(T2)          |
+ * |    ↑                |
+ * |  c-start(T1)         |
+ * ------------------------
+ *
+ * T2 timeout: T2 - T1
+ * T3 timeout: (T2 - T1) + (T3 - T3)
+ * T4 timeout: (T2 - T1) + (T4 - T3)
+ * T5 timeout: (T2 - T1) + (T5 - T3)
+ * T6 timeout: (T2 - T1) + (T6 - T3)
+ * T7 timeout: (T2 - T1) + (T7 - T3)
+ * T8 timeout: (T2 - T1) + (T8 - T3)
+ * ......
+ * </pre>
+ */
+@Component
+public class ProcessingTimeStrategy implements InvocationTimeoutStrategy {
+  public static final String NAME = "processing-time";
+
+  // nanoseconds
+  // used inside one process
+  // not depend on time synchronization
+  public static final String CHAIN_START_TIME = "x-scb-process-chain-start";
+
+  // nanoseconds
+  // processing time of all previous process
+  // transfer between processes
+  public static final String CHAIN_PROCESSING = "x-scb-processing-time";
+
+  private Ticker ticker = Ticker.systemTicker();
+
+  public ProcessingTimeStrategy setTicker(Ticker ticker) {
+    this.ticker = ticker;
+    return this;
+  }
+
+  @Override
+  public String name() {
+    return NAME;
+  }
+
+  @Override
+  public void start(Invocation invocation) {
+    initProcessChainStart(invocation);
+    initChainProcessing(invocation);
+  }
+
+  private void initProcessChainStart(Invocation invocation) {
+    if (invocation.getLocalContext(CHAIN_START_TIME) != null) {
+      return;
+    }
+
+    invocation.addLocalContext(CHAIN_START_TIME, invocation.getInvocationStageTrace().getStart());
+  }
+
+  private void initChainProcessing(Invocation invocation) {
+    if (invocation.getLocalContext(CHAIN_PROCESSING) != null) {
+      return;
+    }
+
+    String contextChainProcessing = invocation.getContext(CHAIN_PROCESSING);
+    long chainProcessingTime = NumberUtils.toLong(contextChainProcessing, 0L);
+    invocation.addLocalContext(CHAIN_PROCESSING, chainProcessingTime);
+  }
+
+  @Override
+  public void beforeSendRequest(Invocation invocation) {
+    InvocationTimeoutStrategy.super.beforeSendRequest(invocation);
+
+    long processingTime = calculateElapsedNanoTime(invocation);
+    invocation.addContext(CHAIN_PROCESSING, Long.toString(processingTime));
+  }
+
+  @Override
+  public long calculateElapsedNanoTime(Invocation invocation) {
+    long chainStartTime = invocation.getLocalContext(CHAIN_START_TIME);
+    long previousProcessingTime = invocation.getLocalContext(CHAIN_PROCESSING);
+    return ticker.read() - chainStartTime + previousProcessingTime;
+  }
+}
diff --git a/core/src/test/java/org/apache/servicecomb/core/invocation/timeout/PassingTimeStrategyTest.java b/core/src/test/java/org/apache/servicecomb/core/invocation/timeout/PassingTimeStrategyTest.java
new file mode 100644
index 0000000..5aa9914
--- /dev/null
+++ b/core/src/test/java/org/apache/servicecomb/core/invocation/timeout/PassingTimeStrategyTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ */
+
+package org.apache.servicecomb.core.invocation.timeout;
+
+import static org.apache.servicecomb.core.invocation.timeout.PassingTimeStrategy.CHAIN_START_TIME;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.catchThrowable;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.servicecomb.core.Invocation;
+import org.apache.servicecomb.foundation.test.scaffolding.time.MockClock;
+import org.junit.jupiter.api.Test;
+
+import com.google.common.collect.ImmutableMap;
+
+class PassingTimeStrategyTest {
+  PassingTimeStrategy strategy = new PassingTimeStrategy();
+
+  @Test
+  void should_init_when_start_as_first_chain_node() {
+    Invocation invocation = new Invocation();
+    invocation.getInvocationStageTrace().setStartTimeMillis(10);
+
+    strategy.start(invocation);
+
+    assertThat(invocation.getContext(CHAIN_START_TIME)).isEqualTo("10");
+    assertThat(invocation.<Long>getLocalContext(CHAIN_START_TIME)).isEqualTo(10L);
+  }
+
+  @Test
+  void should_init_when_start_as_first_node_of_a_process_but_not_first_of_a_chain() {
+    Invocation invocation = new Invocation();
+    invocation.setContext(ImmutableMap.of(CHAIN_START_TIME, "10"));
+
+    strategy.start(invocation);
+
+    assertThat(invocation.getContext(CHAIN_START_TIME)).isEqualTo("10");
+    assertThat(invocation.<Long>getLocalContext(CHAIN_START_TIME)).isEqualTo(10L);
+  }
+
+  @Test
+  void should_do_nothing_when_start_not_as_first_node_of_a_process() {
+    Invocation invocation = new Invocation();
+    invocation.setContext(ImmutableMap.of());
+    invocation.setLocalContext(ImmutableMap.of(CHAIN_START_TIME, 10L));
+
+    Throwable throwable = catchThrowable(() -> strategy.start(invocation));
+
+    assertThat(throwable).isNull();
+  }
+
+  @Test
+  void should_calc_elapsed_time_as_passing_time() {
+    Invocation invocation = new Invocation();
+    invocation.addLocalContext(CHAIN_START_TIME, 10L);
+    strategy.setClock(new MockClock(100L));
+
+    long elapsedNanoTime = strategy.calculateElapsedNanoTime(invocation);
+
+    assertThat(elapsedNanoTime).isEqualTo(TimeUnit.MILLISECONDS.toNanos(90));
+  }
+}
\ No newline at end of file
diff --git a/core/src/test/java/org/apache/servicecomb/core/invocation/timeout/ProcessingTimeStrategyTest.java b/core/src/test/java/org/apache/servicecomb/core/invocation/timeout/ProcessingTimeStrategyTest.java
new file mode 100644
index 0000000..e348f10
--- /dev/null
+++ b/core/src/test/java/org/apache/servicecomb/core/invocation/timeout/ProcessingTimeStrategyTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.
+ */
+
+package org.apache.servicecomb.core.invocation.timeout;
+
+import static org.apache.servicecomb.core.invocation.timeout.ProcessingTimeStrategy.CHAIN_PROCESSING;
+import static org.apache.servicecomb.core.invocation.timeout.ProcessingTimeStrategy.CHAIN_START_TIME;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.catchThrowable;
+
+import org.apache.servicecomb.core.Invocation;
+import org.apache.servicecomb.foundation.test.scaffolding.time.MockTicker;
+import org.junit.jupiter.api.Test;
+
+import com.google.common.collect.ImmutableMap;
+
+class ProcessingTimeStrategyTest {
+  ProcessingTimeStrategy strategy = new ProcessingTimeStrategy();
+
+  @Test
+  void should_init_when_start_as_first_chain_node() {
+    Invocation invocation = new Invocation();
+    invocation.getInvocationStageTrace().start(10);
+
+    strategy.start(invocation);
+
+    assertThat(invocation.<Long>getLocalContext(CHAIN_START_TIME)).isEqualTo(10L);
+    assertThat(invocation.<Long>getLocalContext(CHAIN_PROCESSING)).isEqualTo(0L);
+  }
+
+  @Test
+  void should_do_nothing_when_not_first_node_of_a_process() {
+    Invocation invocation = new Invocation();
+    invocation.setLocalContext(ImmutableMap.of(
+        CHAIN_START_TIME, 10L,
+        CHAIN_PROCESSING, 0L
+    ));
+
+    Throwable throwable = catchThrowable(() -> strategy.start(invocation));
+
+    assertThat(throwable).isNull();
+  }
+
+  @Test
+  void should_calc_elapsed_time_as_processing_time() {
+    strategy.setTicker(new MockTicker(50L));
+
+    Invocation invocation = new Invocation();
+    invocation.addLocalContext(CHAIN_START_TIME, 10L);
+    invocation.addLocalContext(CHAIN_PROCESSING, 20L);
+
+    long elapsedNanoTime = strategy.calculateElapsedNanoTime(invocation);
+
+    assertThat(elapsedNanoTime).isEqualTo(60L);
+  }
+
+  @Test
+  void should_update_processing_time_before_send() {
+    strategy = new ProcessingTimeStrategy() {
+      @Override
+      public void checkTimeout(Invocation invocation) {
+
+      }
+    };
+    strategy.setTicker(new MockTicker(50L));
+
+    Invocation invocation = new Invocation();
+    invocation.addLocalContext(CHAIN_START_TIME, 10L);
+    invocation.addLocalContext(CHAIN_PROCESSING, 20L);
+
+    strategy.beforeSendRequest(invocation);
+
+    assertThat(invocation.getContext(CHAIN_PROCESSING)).isEqualTo("60");
+  }
+}
\ No newline at end of file
diff --git a/demo/demo-jaxrs/jaxrs-client/src/main/resources/microservice.yaml b/demo/demo-jaxrs/jaxrs-client/src/main/resources/microservice.yaml
index fd46e82..54b9929 100644
--- a/demo/demo-jaxrs/jaxrs-client/src/main/resources/microservice.yaml
+++ b/demo/demo-jaxrs/jaxrs-client/src/main/resources/microservice.yaml
@@ -44,18 +44,20 @@ servicecomb:
           timeout: 1000
 
   invocation:
-    enableTimeoutCheck: false
+    timeout:
+      check:
+        enabled: false
 
-# test configurations. you can choose any implementation. default using local.
+  # test configurations. you can choose any implementation. default using local.
 
   # using nacos configuration
-#  nacos:
-#    serverAddr: http://127.0.0.1:8848
-#    group: jaxrstest
-#    dataId: jaxrsclient
-#    namespace: public
-#    contentType: properties # can be properties, yaml, raw. If using raw, property [group].[dataId]=value.
-#    addPrefix: false # if true [group].[dataId] will added as properties/yaml items prefix. Will not influence raw.
+  #  nacos:
+  #    serverAddr: http://127.0.0.1:8848
+  #    group: jaxrstest
+  #    dataId: jaxrsclient
+  #    namespace: public
+  #    contentType: properties # can be properties, yaml, raw. If using raw, property [group].[dataId]=value.
+  #    addPrefix: false # if true [group].[dataId] will added as properties/yaml items prefix. Will not influence raw.
 
   # for integration test not install any config server
 jaxrstest:
diff --git a/demo/demo-jaxrs/jaxrs-server/src/main/resources/microservice.yaml b/demo/demo-jaxrs/jaxrs-server/src/main/resources/microservice.yaml
index 2eda948..a6cb992 100644
--- a/demo/demo-jaxrs/jaxrs-server/src/main/resources/microservice.yaml
+++ b/demo/demo-jaxrs/jaxrs-server/src/main/resources/microservice.yaml
@@ -39,4 +39,6 @@ servicecomb:
   executors.Provider.ReactiveSchema: servicecomb.executor.reactive
 
   invocation:
-    enableTimeoutCheck: false
\ No newline at end of file
+    timeout:
+      check:
+        enabled: false
\ No newline at end of file
diff --git a/demo/demo-springmvc/springmvc-client/src/main/java/org/apache/servicecomb/demo/springmvc/client/TestSpringMVCCommonSchemaInterface.java b/demo/demo-springmvc/springmvc-client/src/main/java/org/apache/servicecomb/demo/springmvc/client/TestSpringMVCCommonSchemaInterface.java
index e45da0d..a9f53ee 100644
--- a/demo/demo-springmvc/springmvc-client/src/main/java/org/apache/servicecomb/demo/springmvc/client/TestSpringMVCCommonSchemaInterface.java
+++ b/demo/demo-springmvc/springmvc-client/src/main/java/org/apache/servicecomb/demo/springmvc/client/TestSpringMVCCommonSchemaInterface.java
@@ -19,7 +19,7 @@ package org.apache.servicecomb.demo.springmvc.client;
 
 import java.util.concurrent.TimeUnit;
 
-import org.apache.servicecomb.core.Const;
+import org.apache.servicecomb.core.invocation.timeout.ProcessingTimeStrategy;
 import org.apache.servicecomb.demo.CategorizedTestCase;
 import org.apache.servicecomb.demo.CommonSchemaInterface;
 import org.apache.servicecomb.demo.TestMgr;
@@ -34,6 +34,7 @@ public class TestSpringMVCCommonSchemaInterface implements CategorizedTestCase {
   @RpcReference(schemaId = "SpringMVCCommonSchemaInterface", microserviceName = "springmvc")
   private CommonSchemaInterface client;
 
+  @Override
   public void testAllTransport() throws Exception {
     testInvocationTimeoutInServer();
     testInvocationTimeoutInServerUserCheck();
@@ -54,8 +55,8 @@ public class TestSpringMVCCommonSchemaInterface implements CategorizedTestCase {
   private void testInvocationAlreadyTimeoutInClient() {
     try {
       InvocationContext context = new InvocationContext();
-      context.addLocalContext(Const.CONTEXT_TIME_CURRENT, System.nanoTime());
-      context.addLocalContext(Const.CONTEXT_TIME_ELAPSED, TimeUnit.SECONDS.toNanos(1));
+      context.addLocalContext(ProcessingTimeStrategy.CHAIN_START_TIME, System.nanoTime());
+      context.addLocalContext(ProcessingTimeStrategy.CHAIN_PROCESSING, TimeUnit.SECONDS.toNanos(1));
       client.testInvocationTimeout(context, 1, "hello");
       TestMgr.fail("should timeout");
     } catch (InvocationException e) {
diff --git a/demo/demo-springmvc/springmvc-client/src/main/resources/microservice.yaml b/demo/demo-springmvc/springmvc-client/src/main/resources/microservice.yaml
index efd5bdf..a1d2dec 100644
--- a/demo/demo-springmvc/springmvc-client/src/main/resources/microservice.yaml
+++ b/demo/demo-springmvc/springmvc-client/src/main/resources/microservice.yaml
@@ -51,6 +51,11 @@ servicecomb:
     chain:
       Consumer:
         default: loadbalance,fault-injection-consumer,bizkeeper-consumer
+  invocation:
+    timeout:
+      check:
+        enabled: true
+        strategy: processing-time
   tracing:
     enabled: true
     samplingRate: 0.5
diff --git a/demo/demo-springmvc/springmvc-server/src/main/resources/microservice.yaml b/demo/demo-springmvc/springmvc-server/src/main/resources/microservice.yaml
index ef9141d..8696bff 100644
--- a/demo/demo-springmvc/springmvc-server/src/main/resources/microservice.yaml
+++ b/demo/demo-springmvc/springmvc-server/src/main/resources/microservice.yaml
@@ -61,6 +61,10 @@ servicecomb:
   highway:
     address: 0.0.0.0:7070?sslEnabled=true
   invocation:
+    timeout:
+      check:
+        enabled: true
+        strategy: processing-time
     SpringMVCCommonSchemaInterface:
       testInvocationTimeout:
         timeout: 1000
diff --git a/foundations/foundation-config/src/main/java/org/apache/servicecomb/config/BootStrapProperties.java b/foundations/foundation-config/src/main/java/org/apache/servicecomb/config/BootStrapProperties.java
index f1b5158..079bf86 100644
--- a/foundations/foundation-config/src/main/java/org/apache/servicecomb/config/BootStrapProperties.java
+++ b/foundations/foundation-config/src/main/java/org/apache/servicecomb/config/BootStrapProperties.java
@@ -20,8 +20,12 @@ package org.apache.servicecomb.config;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 
+import org.apache.commons.configuration.AbstractConfiguration;
 import org.apache.commons.configuration.Configuration;
+import org.apache.commons.configuration.PropertyConverter;
+import org.apache.commons.configuration.SubsetConfiguration;
 import org.springframework.util.StringUtils;
 
 /**
@@ -262,25 +266,33 @@ public class BootStrapProperties {
   }
 
   private static Map<String, String> readProperties(Configuration configuration, String newKey, String oldKey) {
-    Configuration subset = configuration.subset(newKey);
+    AbstractConfiguration subset = (AbstractConfiguration) configuration.subset(newKey);
     if (subset.isEmpty()) {
-      subset = configuration.subset(oldKey);
+      subset = (AbstractConfiguration) configuration.subset(oldKey);
     }
     return toStringMap(subset);
   }
 
-  private static Map<String, String> toStringMap(Configuration configuration) {
+  private static Map<String, String> toStringMap(AbstractConfiguration configuration) {
+    AbstractConfiguration root = findRoot(configuration);
     Map<String, String> map = new LinkedHashMap<>();
     configuration.getKeys().forEachRemaining(key -> {
-      try {
-        map.put(key, configuration.getString(key));
-      } catch (Exception e) {
-        map.put(key, String.valueOf(configuration.getProperty(key)));
-      }
+      Object value = configuration.getProperty(key);
+      // support placeholder
+      value = PropertyConverter.interpolate(value, root);
+      map.put(key, Objects.toString(value, null));
     });
     return map;
   }
 
+  private static AbstractConfiguration findRoot(AbstractConfiguration configuration) {
+    if (configuration instanceof SubsetConfiguration) {
+      return findRoot((AbstractConfiguration) ((SubsetConfiguration) configuration).getParent());
+    }
+
+    return configuration;
+  }
+
   private static void checkMicroserviceName(String name) {
     // the configuration we used
     // when resolve placeholder failed
diff --git a/foundations/foundation-registry/src/main/java/org/apache/servicecomb/registry/config/AbstractPropertiesLoader.java b/foundations/foundation-registry/src/main/java/org/apache/servicecomb/registry/config/AbstractPropertiesLoader.java
index 8df5f94..1944063 100644
--- a/foundations/foundation-registry/src/main/java/org/apache/servicecomb/registry/config/AbstractPropertiesLoader.java
+++ b/foundations/foundation-registry/src/main/java/org/apache/servicecomb/registry/config/AbstractPropertiesLoader.java
@@ -22,7 +22,6 @@ import java.util.Map;
 
 import org.apache.commons.configuration.Configuration;
 import org.apache.commons.lang3.StringUtils;
-import org.apache.servicecomb.config.BootStrapProperties;
 import org.apache.servicecomb.registry.api.PropertyExtended;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
diff --git a/foundations/foundation-common/src/main/java/org/apache/servicecomb/foundation/common/testing/MockClock.java b/foundations/foundation-test-scaffolding/src/main/java/org/apache/servicecomb/foundation/test/scaffolding/time/MockClock.java
similarity index 74%
rename from foundations/foundation-common/src/main/java/org/apache/servicecomb/foundation/common/testing/MockClock.java
rename to foundations/foundation-test-scaffolding/src/main/java/org/apache/servicecomb/foundation/test/scaffolding/time/MockClock.java
index 252e64b..ba0f3c6 100644
--- a/foundations/foundation-common/src/main/java/org/apache/servicecomb/foundation/common/testing/MockClock.java
+++ b/foundations/foundation-test-scaffolding/src/main/java/org/apache/servicecomb/foundation/test/scaffolding/time/MockClock.java
@@ -15,20 +15,28 @@
  * limitations under the License.
  */
 
-package org.apache.servicecomb.foundation.common.testing;
+package org.apache.servicecomb.foundation.test.scaffolding.time;
 
 import java.time.Clock;
 import java.time.Instant;
 import java.time.ZoneId;
 
-import org.apache.servicecomb.foundation.common.Holder;
-
 public class MockClock extends Clock {
+  private MockValues<Long> values;
+
+  public MockClock() {
+    this(0L);
+  }
 
-  Holder<Long> mockMillsHolder;
+  public MockClock(Long... values) {
+    this.setValues(values);
+  }
 
-  public MockClock(Holder<Long> h) {
-    this.mockMillsHolder = h;
+  public MockClock setValues(Long... values) {
+    this.values = new MockValues<Long>()
+        .setDefaultValue(0L)
+        .setValues(values);
+    return this;
   }
 
   @Override
@@ -48,6 +56,6 @@ public class MockClock extends Clock {
 
   @Override
   public long millis() {
-    return mockMillsHolder.value;
+    return values.read();
   }
 }
diff --git a/foundations/foundation-test-scaffolding/src/main/java/org/apache/servicecomb/foundation/test/scaffolding/MockTicker.java b/foundations/foundation-test-scaffolding/src/main/java/org/apache/servicecomb/foundation/test/scaffolding/time/MockTicker.java
similarity index 64%
copy from foundations/foundation-test-scaffolding/src/main/java/org/apache/servicecomb/foundation/test/scaffolding/MockTicker.java
copy to foundations/foundation-test-scaffolding/src/main/java/org/apache/servicecomb/foundation/test/scaffolding/time/MockTicker.java
index 52ceb92..9bd666d 100644
--- a/foundations/foundation-test-scaffolding/src/main/java/org/apache/servicecomb/foundation/test/scaffolding/MockTicker.java
+++ b/foundations/foundation-test-scaffolding/src/main/java/org/apache/servicecomb/foundation/test/scaffolding/time/MockTicker.java
@@ -15,42 +15,30 @@
  * limitations under the License.
  */
 
-package org.apache.servicecomb.foundation.test.scaffolding;
+package org.apache.servicecomb.foundation.test.scaffolding.time;
 
 import com.google.common.base.Ticker;
 
 public class MockTicker extends Ticker {
-  private long[] values;
-
-  private int index;
+  private MockValues<Long> values;
 
   public MockTicker() {
-    this(0);
+    this(0L);
   }
 
-  public MockTicker(long... values) {
-    this.values = values;
-    this.index = 0;
+  public MockTicker(Long... values) {
+    this.setValues(values);
   }
 
-  public MockTicker setValues(long... values) {
-    this.values = values;
-    this.index = 0;
+  public MockTicker setValues(Long... values) {
+    this.values = new MockValues<Long>()
+        .setDefaultValue(0L)
+        .setValues(values);
     return this;
   }
 
   @Override
   public long read() {
-    if (values.length == 0) {
-      return 0;
-    }
-
-    if (index >= values.length) {
-      return values[values.length - 1];
-    }
-
-    long value = values[index];
-    index++;
-    return value;
+    return values.read();
   }
 }
diff --git a/foundations/foundation-test-scaffolding/src/main/java/org/apache/servicecomb/foundation/test/scaffolding/MockTicker.java b/foundations/foundation-test-scaffolding/src/main/java/org/apache/servicecomb/foundation/test/scaffolding/time/MockValues.java
similarity index 66%
rename from foundations/foundation-test-scaffolding/src/main/java/org/apache/servicecomb/foundation/test/scaffolding/MockTicker.java
rename to foundations/foundation-test-scaffolding/src/main/java/org/apache/servicecomb/foundation/test/scaffolding/time/MockValues.java
index 52ceb92..457a8f0 100644
--- a/foundations/foundation-test-scaffolding/src/main/java/org/apache/servicecomb/foundation/test/scaffolding/MockTicker.java
+++ b/foundations/foundation-test-scaffolding/src/main/java/org/apache/servicecomb/foundation/test/scaffolding/time/MockValues.java
@@ -15,41 +15,36 @@
  * limitations under the License.
  */
 
-package org.apache.servicecomb.foundation.test.scaffolding;
+package org.apache.servicecomb.foundation.test.scaffolding.time;
 
-import com.google.common.base.Ticker;
+public class MockValues<T> {
+  private T defaultValue;
 
-public class MockTicker extends Ticker {
-  private long[] values;
+  private T[] values;
 
   private int index;
 
-  public MockTicker() {
-    this(0);
-  }
-
-  public MockTicker(long... values) {
-    this.values = values;
-    this.index = 0;
+  public MockValues<T> setDefaultValue(T defaultValue) {
+    this.defaultValue = defaultValue;
+    return this;
   }
 
-  public MockTicker setValues(long... values) {
+  public MockValues<T> setValues(T[] values) {
     this.values = values;
     this.index = 0;
     return this;
   }
 
-  @Override
-  public long read() {
-    if (values.length == 0) {
-      return 0;
+  public T read() {
+    if (values == null || values.length == 0) {
+      return defaultValue;
     }
 
     if (index >= values.length) {
       return values[values.length - 1];
     }
 
-    long value = values[index];
+    T value = values[index];
     index++;
     return value;
   }
diff --git a/handlers/handler-loadbalance/src/test/java/org/apache/servicecomb/loadbalance/TestServiceCombServerStats.java b/handlers/handler-loadbalance/src/test/java/org/apache/servicecomb/loadbalance/TestServiceCombServerStats.java
index a7aad86..dc6d5d9 100644
--- a/handlers/handler-loadbalance/src/test/java/org/apache/servicecomb/loadbalance/TestServiceCombServerStats.java
+++ b/handlers/handler-loadbalance/src/test/java/org/apache/servicecomb/loadbalance/TestServiceCombServerStats.java
@@ -22,8 +22,7 @@ import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
 import org.apache.servicecomb.core.Invocation;
-import org.apache.servicecomb.foundation.common.Holder;
-import org.apache.servicecomb.foundation.common.testing.MockClock;
+import org.apache.servicecomb.foundation.test.scaffolding.time.MockClock;
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
@@ -81,14 +80,14 @@ public class TestServiceCombServerStats {
 
   @Test
   public void testTimeWindow() {
-    ServiceCombServerStats stats = new ServiceCombServerStats(null, new MockClock(new Holder<>(1000L)));
+    ServiceCombServerStats stats = new ServiceCombServerStats(null, new MockClock(1000L));
     Assert.assertEquals(1000, stats.getLastVisitTime());
     stats.markSuccess();
     stats.markFailure();
     Assert.assertEquals(2, stats.getTotalRequests());
     Assert.assertEquals(50, stats.getFailedRate());
     Assert.assertEquals(50, stats.getSuccessRate());
-    stats.clock = new MockClock(new Holder<>(60000L + 2000L));
+    stats.clock = new MockClock(60000L + 2000L);
     stats.markSuccess();
     Assert.assertEquals(1, stats.getTotalRequests());
     Assert.assertEquals(0, stats.getFailedRate());
@@ -118,9 +117,9 @@ public class TestServiceCombServerStats {
     Invocation invocation = new Invocation();
     Assert.assertTrue(ServiceCombServerStats.applyForTryingChance(invocation));
     Assert.assertSame(invocation, ServiceCombServerStats.globalAllowIsolatedServerTryingFlag.get().getInvocation());
-    ServiceCombServerStats.globalAllowIsolatedServerTryingFlag.get().clock = new MockClock(new Holder<>(
+    ServiceCombServerStats.globalAllowIsolatedServerTryingFlag.get().clock = new MockClock(
         ServiceCombServerStats.globalAllowIsolatedServerTryingFlag.get().startTryingTimestamp + 60000
-    ));
+    );
 
     Invocation otherInvocation = new Invocation();
     Assert.assertTrue(ServiceCombServerStats.applyForTryingChance(otherInvocation));
diff --git a/service-registry/registry-lightweight/src/test/java/org/apache/servicecomb/registry/lightweight/StoreServiceTest.java b/service-registry/registry-lightweight/src/test/java/org/apache/servicecomb/registry/lightweight/StoreServiceTest.java
index 92593c8..6da1304 100644
--- a/service-registry/registry-lightweight/src/test/java/org/apache/servicecomb/registry/lightweight/StoreServiceTest.java
+++ b/service-registry/registry-lightweight/src/test/java/org/apache/servicecomb/registry/lightweight/StoreServiceTest.java
@@ -23,7 +23,7 @@ import static org.mockito.Matchers.any;
 
 import java.util.concurrent.CompletableFuture;
 
-import org.apache.servicecomb.foundation.test.scaffolding.MockTicker;
+import org.apache.servicecomb.foundation.test.scaffolding.time.MockTicker;
 import org.apache.servicecomb.registry.api.registry.Microservice;
 import org.apache.servicecomb.registry.api.registry.MicroserviceInstanceStatus;
 import org.apache.servicecomb.registry.lightweight.store.InstanceStore;
@@ -129,7 +129,7 @@ class StoreServiceTest extends TestBase {
 
     RegisterRequest request = self.buildRegisterRequest()
         .setStatus(MicroserviceInstanceStatus.TESTING);
-    ticker.setValues(1);
+    ticker.setValues(1L);
     InstanceStore instanceStore = service.register(request);
 
     assertThat(self.getInstance().getStatus()).isEqualTo(MicroserviceInstanceStatus.TESTING);
@@ -142,7 +142,7 @@ class StoreServiceTest extends TestBase {
     InstanceStore instanceStore = store.findInstanceStore(self.getInstanceId());
     assertThat(instanceStore.getLastHeartBeat()).isEqualTo(0);
 
-    ticker.setValues(1);
+    ticker.setValues(1L);
     should_register_microservice_and_instance_when_both_not_exist();
     assertThat(instanceStore.getLastHeartBeat()).isEqualTo(1);
   }
diff --git a/service-registry/registry-service-center/src/test/java/org/apache/servicecomb/serviceregistry/diagnosis/instance/TestInstanceCacheCheckerMock.java b/service-registry/registry-service-center/src/test/java/org/apache/servicecomb/serviceregistry/diagnosis/instance/TestInstanceCacheCheckerMock.java
index 474e56e..89e9ff8 100644
--- a/service-registry/registry-service-center/src/test/java/org/apache/servicecomb/serviceregistry/diagnosis/instance/TestInstanceCacheCheckerMock.java
+++ b/service-registry/registry-service-center/src/test/java/org/apache/servicecomb/serviceregistry/diagnosis/instance/TestInstanceCacheCheckerMock.java
@@ -20,17 +20,17 @@ import java.util.ArrayList;
 
 import org.apache.servicecomb.config.ConfigUtil;
 import org.apache.servicecomb.foundation.common.Holder;
-import org.apache.servicecomb.foundation.common.testing.MockClock;
 import org.apache.servicecomb.foundation.test.scaffolding.config.ArchaiusUtils;
+import org.apache.servicecomb.foundation.test.scaffolding.time.MockClock;
 import org.apache.servicecomb.registry.DiscoveryManager;
-import org.apache.servicecomb.serviceregistry.RegistryUtils;
-import org.apache.servicecomb.serviceregistry.ServiceRegistry;
+import org.apache.servicecomb.registry.api.registry.FindInstancesResponse;
 import org.apache.servicecomb.registry.api.registry.Microservice;
 import org.apache.servicecomb.registry.api.registry.MicroserviceInstance;
-import org.apache.servicecomb.registry.api.registry.FindInstancesResponse;
 import org.apache.servicecomb.registry.api.registry.MicroserviceInstances;
 import org.apache.servicecomb.registry.consumer.MicroserviceVersions;
 import org.apache.servicecomb.registry.definition.DefinitionConst;
+import org.apache.servicecomb.serviceregistry.RegistryUtils;
+import org.apache.servicecomb.serviceregistry.ServiceRegistry;
 import org.apache.servicecomb.serviceregistry.diagnosis.Status;
 import org.apache.servicecomb.serviceregistry.registry.LocalServiceRegistryFactory;
 import org.junit.After;
@@ -65,7 +65,7 @@ public class TestInstanceCacheCheckerMock {
     RegistryUtils.setServiceRegistry(serviceRegistry);
 
     checker = new InstanceCacheChecker(DiscoveryManager.INSTANCE.getAppManager());
-    checker.clock = new MockClock(new Holder<>(1L));
+    checker.clock = new MockClock(1L);
     expectedSummary.setStatus(Status.NORMAL);
     expectedSummary.setTimestamp(1);
   }
diff --git a/service-registry/registry-service-center/src/test/java/org/apache/servicecomb/serviceregistry/diagnosis/instance/TestInstanceCacheCheckerWithoutMock.java b/service-registry/registry-service-center/src/test/java/org/apache/servicecomb/serviceregistry/diagnosis/instance/TestInstanceCacheCheckerWithoutMock.java
index 66a6bdc..4a3b995 100644
--- a/service-registry/registry-service-center/src/test/java/org/apache/servicecomb/serviceregistry/diagnosis/instance/TestInstanceCacheCheckerWithoutMock.java
+++ b/service-registry/registry-service-center/src/test/java/org/apache/servicecomb/serviceregistry/diagnosis/instance/TestInstanceCacheCheckerWithoutMock.java
@@ -19,9 +19,8 @@ package org.apache.servicecomb.serviceregistry.diagnosis.instance;
 import java.util.Arrays;
 
 import org.apache.servicecomb.config.ConfigUtil;
-import org.apache.servicecomb.foundation.common.Holder;
-import org.apache.servicecomb.foundation.common.testing.MockClock;
 import org.apache.servicecomb.foundation.test.scaffolding.config.ArchaiusUtils;
+import org.apache.servicecomb.foundation.test.scaffolding.time.MockClock;
 import org.apache.servicecomb.registry.DiscoveryManager;
 import org.apache.servicecomb.registry.RegistrationManager;
 import org.apache.servicecomb.registry.consumer.MicroserviceVersionRule;
@@ -62,7 +61,7 @@ public class TestInstanceCacheCheckerWithoutMock {
     RegistryUtils.setServiceRegistry(serviceRegistry);
 
     checker = new InstanceCacheChecker(DiscoveryManager.INSTANCE.getAppManager());
-    checker.clock = new MockClock(new Holder<>(1L));
+    checker.clock = new MockClock(1L);
     expectedSummary.setStatus(Status.NORMAL);
     expectedSummary.setTimestamp(1);
   }